初始提交:彩票推测系统前端代码
511
src/App.vue
Normal file
@@ -0,0 +1,511 @@
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { userStore } from './store/user'
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import TheWelcome from './components/TheWelcome.vue'
|
||||
import BottomNavigation from './components/BottomNavigation.vue'
|
||||
import CozeChat from './components/CozeChat.vue'
|
||||
|
||||
// 获取当前路由
|
||||
const route = useRoute()
|
||||
|
||||
// 判断是否是后台管理路由
|
||||
const isAdminRoute = computed(() => {
|
||||
return route.path.startsWith('/admin')
|
||||
})
|
||||
|
||||
// 处理发现按钮点击事件
|
||||
const handleDiscoveryClick = () => {
|
||||
// 触发全局事件,让CozeChat组件处理
|
||||
window.dispatchEvent(new CustomEvent('showAIAssistant'))
|
||||
}
|
||||
|
||||
// 应用启动时获取用户信息
|
||||
onMounted(async () => {
|
||||
// 如果用户已登录但没有完整的用户信息,则从后端获取
|
||||
if (userStore.isLoggedIn && (!userStore.user?.id || typeof userStore.user.id === 'number')) {
|
||||
try {
|
||||
console.log('应用启动,尝试获取用户信息...')
|
||||
await userStore.fetchLoginUser()
|
||||
console.log('用户信息获取成功:', userStore.user)
|
||||
} catch (error) {
|
||||
console.warn('获取用户信息失败,可能是后端服务不可用:', error)
|
||||
// 不强制清除登录状态,让用户可以继续浏览页面
|
||||
// 如果是401错误才清除登录状态
|
||||
if (error.response?.status === 401) {
|
||||
userStore.logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是后台路由,给body添加admin-body类
|
||||
if (isAdminRoute.value) {
|
||||
document.body.classList.add('admin-body')
|
||||
} else {
|
||||
document.body.classList.remove('admin-body')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 后台管理路由直接显示内容,不显示底部导航 -->
|
||||
<template v-if="isAdminRoute">
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<!-- 前台用户路由显示应用容器和底部导航 -->
|
||||
<div v-else class="app-container">
|
||||
<!-- 全局顶部导航栏 -->
|
||||
<header class="global-header">
|
||||
<div class="header-content">
|
||||
<div class="app-logo">
|
||||
<span class="logo-text">精彩猪手</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<!-- 底部导航栏 -->
|
||||
<BottomNavigation @discovery-click="handleDiscoveryClick" />
|
||||
|
||||
<!-- Coze AI 聊天助手组件 -->
|
||||
<CozeChat />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 全局重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
background: #f0f2f5 !important;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background: #f5f5f5 !important;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* 应用容器 - 包含主内容和底部导航 */
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
max-width: 850px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 全局顶部导航栏 */
|
||||
.global-header {
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, #ff7b7b 0%, #ff6363 50%, #f85555 100%);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
box-shadow: 0 2px 12px rgba(248, 85, 85, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 装饰性波浪背景 */
|
||||
.global-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 60' preserveAspectRatio='none'%3E%3Cpath d='M0,30 Q100,10 200,30 T400,30 T600,30 T800,30 L800,60 L0,60 Z' fill='rgba(255,255,255,0.08)'/%3E%3Cpath d='M0,40 Q150,20 300,40 T600,40 T800,35 L800,60 L0,60 Z' fill='rgba(255,255,255,0.05)'/%3E%3Ccircle cx='50' cy='15' r='3' fill='rgba(255,255,255,0.15)'/%3E%3Ccircle cx='150' cy='45' r='2' fill='rgba(255,255,255,0.12)'/%3E%3Ccircle cx='300' cy='12' r='2.5' fill='rgba(255,255,255,0.1)'/%3E%3Ccircle cx='500' cy='48' r='2' fill='rgba(255,255,255,0.15)'/%3E%3Ccircle cx='650' cy='18' r='3' fill='rgba(255,255,255,0.1)'/%3E%3Ccircle cx='750' cy='40' r='2' fill='rgba(255,255,255,0.12)'/%3E%3C/svg%3E");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 1;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translateY(0);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.app-logo:hover {
|
||||
transform: translateY(-1px) scale(1.02);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3),
|
||||
0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
letter-spacing: 2px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logo-text::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.app-logo:hover .logo-text::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
background: #f0f2f5;
|
||||
position: relative;
|
||||
padding-bottom: 65px;
|
||||
min-height: calc(100vh - 130px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 通用容器样式 */
|
||||
.container {
|
||||
width: 100%;
|
||||
background: #f0f2f5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 页面头部样式 */
|
||||
.page-header {
|
||||
background: url('@/assets/banner/banner.png') center/cover no-repeat, linear-gradient(135deg, #e53e3e 0%, #ff6b6b 100%);
|
||||
color: white;
|
||||
padding: 60px 20px 30px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header::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"><circle cx="20" cy="20" r="2" fill="rgba(255,255,255,0.1)"/><circle cx="80" cy="40" r="1.5" fill="rgba(255,255,255,0.1)"/><circle cx="40" cy="70" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="70" cy="80" r="2.5" fill="rgba(255,255,255,0.1)"/></svg>');
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 14px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(229, 62, 62, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(229, 62, 62, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #d1d5db;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 球号样式 */
|
||||
.ball-red {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 3px;
|
||||
box-shadow: 0 2px 8px rgba(229, 62, 62, 0.3);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.ball-red:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.ball-blue {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 3px;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.ball-blue:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #e53e3e;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.1);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin: 16px;
|
||||
box-shadow: 0 4px 25px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #f0f0f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 35px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.app-container {
|
||||
max-width: 850px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
max-width: 100%;
|
||||
min-height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.global-header {
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding-bottom: 55px;
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
border-radius: 0;
|
||||
min-height: 100vh;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.global-header {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding-bottom: 55px;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: url('@/assets/banner/banner.png') center/cover no-repeat, linear-gradient(135deg, #e53e3e 0%, #ff6b6b 100%);
|
||||
padding: 50px 16px 25px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平滑滚动 */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* 选择高亮颜色 */
|
||||
::selection {
|
||||
background: rgba(229, 62, 62, 0.2);
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
380
src/api/dlt/index.js
Normal file
@@ -0,0 +1,380 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建大乐透专用的axios实例
|
||||
const dltApi = axios.create({
|
||||
// baseURL: 'http://localhost:8123/api',
|
||||
baseURL: 'https://www.yicaishuzhi.com/api',
|
||||
timeout: 300000, // 5分钟超时时间
|
||||
withCredentials: true, // 关键:支持跨域携带cookie和session
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
dltApi.interceptors.response.use(
|
||||
response => {
|
||||
const data = response.data
|
||||
|
||||
// 检查是否是session过期的响应
|
||||
if (data && data.success === false) {
|
||||
const message = data.message || ''
|
||||
if (message.includes('未登录') || message.includes('登录过期') || message.includes('无权限') || message.includes('Invalid session')) {
|
||||
console.log('检测到session过期,清除本地登录状态')
|
||||
|
||||
// 动态导入userStore避免循环依赖
|
||||
import('../../store/user.js').then(({ userStore }) => {
|
||||
userStore.logout()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
error => {
|
||||
console.error('大乐透API请求错误:', error)
|
||||
|
||||
// 检查HTTP状态码,401/403通常表示未授权/session过期
|
||||
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
|
||||
console.log('检测到401/403错误,可能是session过期或无权限')
|
||||
|
||||
// 动态导入userStore避免循环依赖
|
||||
import('../../store/user.js').then(({ userStore }) => {
|
||||
userStore.logout()
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 大乐透API接口方法
|
||||
export const dltLotteryApi = {
|
||||
// 获取近10期大乐透开奖期号
|
||||
getRecent10DrawIds() {
|
||||
return dltApi.get('/dlt-draw/recent-10-draw-ids')
|
||||
},
|
||||
|
||||
// 根据期号获取大乐透开奖号码
|
||||
getDrawNumbersById(drawId) {
|
||||
return dltApi.get(`/dlt-draw/draw/${drawId}/numbers`)
|
||||
},
|
||||
|
||||
// 大乐透前区首球分析
|
||||
analyzeFrontBalls(level, frontBalls, backBalls) {
|
||||
return dltApi.post('/dlt/ball-analysis/predict-first-ball', {
|
||||
level,
|
||||
redBalls: frontBalls, // 前区球
|
||||
blueBalls: backBalls // 后区球
|
||||
})
|
||||
},
|
||||
|
||||
// 大乐透前区随球分析
|
||||
analyzeFollowFrontBalls(level, wellRegardedBalls, previousFrontBalls, previousBackBalls) {
|
||||
return dltApi.post('/dlt/ball-analysis/predict-follower-ball', {
|
||||
level,
|
||||
wellRegardedBalls,
|
||||
previousFrontBalls,
|
||||
previousBackBalls
|
||||
})
|
||||
},
|
||||
|
||||
// 大乐透后区球分析
|
||||
analyzeBackBalls(level, nextFrontBalls, previousFrontBalls, previousBackBalls, nextBackBalls) {
|
||||
return dltApi.post('/dlt/ball-analysis/predict-back-ball', {
|
||||
level,
|
||||
nextFrontBalls,
|
||||
previousFrontBalls,
|
||||
previousBackBalls,
|
||||
nextBackBalls
|
||||
})
|
||||
},
|
||||
|
||||
// 大乐透后区随球分析
|
||||
analyzeFollowBackBalls(level, backFirstBall, nextFrontBalls, previousFrontBalls, previousBackBalls) {
|
||||
return dltApi.post('/dlt/ball-analysis/predict-follow-back-ball', {
|
||||
level,
|
||||
backFirstBall,
|
||||
nextFrontBalls,
|
||||
previousFrontBalls,
|
||||
previousBackBalls
|
||||
})
|
||||
},
|
||||
|
||||
// 创建大乐透推测记录
|
||||
createPredictRecord(userId, drawId, drawDate, frontBalls, backBalls) {
|
||||
return dltApi.post('/dlt/ball-analysis/create-predict', {
|
||||
userId,
|
||||
drawId,
|
||||
drawDate,
|
||||
frontBalls: frontBalls.join(','),
|
||||
backBalls: backBalls.join(',')
|
||||
})
|
||||
},
|
||||
|
||||
// 获取大乐透推测记录
|
||||
getPredictRecordsByUserId(userId, page = 1) {
|
||||
return dltApi.get(`/dlt/ball-analysis/predict-records/${userId}?page=${page}`)
|
||||
},
|
||||
|
||||
// 获取近期大乐透开奖记录
|
||||
getRecentDraws(limit = 10) {
|
||||
return dltApi.get(`/dlt-draw/recent-draws?limit=${limit}`)
|
||||
},
|
||||
|
||||
// 根据期号获取大乐透开奖记录
|
||||
getDrawById(drawId) {
|
||||
return dltApi.get(`/dlt-draw/draw/${drawId}`)
|
||||
},
|
||||
|
||||
// 根据日期范围查询大乐透开奖记录
|
||||
queryDraws(startDate, endDate) {
|
||||
return dltApi.get(`/dlt-draw/query-draws?startDate=${startDate}&endDate=${endDate}`)
|
||||
},
|
||||
|
||||
// 获取近100期大乐透开奖记录(用于表相查询)
|
||||
getRecent100Draws() {
|
||||
return dltApi.get('/dlt-draw/recent-100-draws')
|
||||
},
|
||||
|
||||
// 创建大乐透预测记录(新接口)
|
||||
createDltPredictRecord(userId, drawId, frontBalls, backBalls, drawDate = null) {
|
||||
const params = new URLSearchParams({
|
||||
userId: userId,
|
||||
drawId: drawId,
|
||||
frontBalls: frontBalls,
|
||||
backBalls: backBalls
|
||||
})
|
||||
|
||||
if (drawDate) {
|
||||
params.append('drawDate', drawDate)
|
||||
}
|
||||
|
||||
return dltApi.post(`/dlt/ball-analysis/create-dlt-predict?${params.toString()}`)
|
||||
},
|
||||
|
||||
// 前区与前区的组合性分析
|
||||
frontFrontCombinationAnalysis(masterBall, slaveBall) {
|
||||
return dltApi.get(`/dlt/ball-analysis/front-front-combination-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`)
|
||||
},
|
||||
|
||||
// 前区与后区的组合性分析
|
||||
frontBackCombinationAnalysis(masterBall, slaveBall) {
|
||||
return dltApi.get(`/dlt/ball-analysis/front-back-combination-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`)
|
||||
},
|
||||
|
||||
// 后区与后区的组合性分析
|
||||
backBackCombinationAnalysis(masterBall, slaveBall) {
|
||||
return dltApi.get(`/dlt/ball-analysis/back-back-combination-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`)
|
||||
},
|
||||
|
||||
// 后区与前区的组合性分析
|
||||
backFrontCombinationAnalysis(masterBall, slaveBall) {
|
||||
return dltApi.get(`/dlt/ball-analysis/back-front-combination-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`)
|
||||
},
|
||||
|
||||
// 前区与前区的持续性分析
|
||||
frontFrontPersistenceAnalysis(masterBall, slaveBall) {
|
||||
return dltApi.get(`/dlt/ball-analysis/front-front-persistence-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`)
|
||||
},
|
||||
|
||||
// 后区与后区的持续性分析
|
||||
backBackPersistenceAnalysis(masterBall, slaveBall) {
|
||||
return dltApi.get(`/dlt/ball-analysis/back-back-persistence-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`)
|
||||
},
|
||||
|
||||
// 前区与后区的持续性分析
|
||||
frontBackPersistenceAnalysis(masterBall, slaveBall) {
|
||||
return dltApi.get(`/dlt/ball-analysis/front-back-persistence-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`)
|
||||
},
|
||||
|
||||
// 后区与前区的持续性分析
|
||||
backFrontPersistenceAnalysis(masterBall, slaveBall) {
|
||||
return dltApi.get(`/dlt/ball-analysis/back-front-persistence-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`)
|
||||
},
|
||||
|
||||
// 根据用户ID获取大乐透预测记录(新接口)
|
||||
getDltPredictRecordsByUserId(userId, page = 1, pageSize = 10) {
|
||||
return dltApi.get(`/dlt-predict/predict-records/${userId}?page=${page}&pageSize=${pageSize}`)
|
||||
},
|
||||
|
||||
// 条件查询大乐透预测记录
|
||||
queryDltPredictRecords(userId, predictStatus, page = 1, pageSize = 10) {
|
||||
return dltApi.post('/dlt-predict/query-predict-records', {
|
||||
userId: userId,
|
||||
predictStatus: predictStatus,
|
||||
current: page,
|
||||
pageSize: pageSize
|
||||
})
|
||||
},
|
||||
|
||||
// 前区历史数据查询
|
||||
getFrontendHistoryAll() {
|
||||
return dltApi.get('/dlt/ball-active-analysis/frontend-history-all')
|
||||
},
|
||||
|
||||
getFrontendHistory100() {
|
||||
return dltApi.get('/dlt/ball-active-analysis/frontend-history-100')
|
||||
},
|
||||
|
||||
getFrontendHistoryTop() {
|
||||
return dltApi.get('/dlt/ball-active-analysis/frontend-history-top')
|
||||
},
|
||||
|
||||
getFrontendHistoryTop100() {
|
||||
return dltApi.get('/dlt/ball-active-analysis/frontend-history-top-100')
|
||||
},
|
||||
|
||||
// 后区历史数据查询
|
||||
getBackendHistoryAll() {
|
||||
return dltApi.get('/dlt/ball-active-analysis/backend-history-all')
|
||||
},
|
||||
|
||||
getBackendHistory100() {
|
||||
return dltApi.get('/dlt/ball-active-analysis/backend-history-100')
|
||||
},
|
||||
|
||||
getBackendHistoryTop() {
|
||||
return dltApi.get('/dlt/ball-active-analysis/backend-history-top')
|
||||
},
|
||||
|
||||
getBackendHistoryTop100() {
|
||||
return dltApi.get('/dlt/ball-active-analysis/backend-history-top-100')
|
||||
},
|
||||
|
||||
// Excel数据导入相关API
|
||||
// 上传Excel文件完整导入大乐透数据(D3-D12工作表)
|
||||
uploadDltExcelFile(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return dltApi.post('/dlt/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 上传Excel文件导入大乐透开奖数据(覆盖)(D1工作表)
|
||||
uploadDltDrawsFile(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return dltApi.post('/dlt/upload-draw-data', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 追加导入大乐透开奖数据(D1工作表)
|
||||
appendDltDrawsFile(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return dltApi.post('/dlt/append-draw-data', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 手动处理待开奖记录(双色球+大乐透)
|
||||
processPendingPredictions() {
|
||||
return dltApi.post('/dlt-predict/process-pending')
|
||||
},
|
||||
|
||||
// 获取用户大乐透预测统计
|
||||
getUserPredictStats(userId) {
|
||||
return dltApi.get(`/dlt-predict/user-predict-stat/${userId}`)
|
||||
},
|
||||
|
||||
// 获取用户大乐透奖金统计
|
||||
getPrizeStatistics(userId) {
|
||||
return dltApi.get('/dlt-predict/prize-statistics', {
|
||||
params: {
|
||||
userId: userId
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 大乐透命中率分析接口
|
||||
// 前区首球命中率分析
|
||||
getFrontFirstBallHitRate() {
|
||||
return dltApi.get('/dlt-predict/front-first-ball-hit-rate')
|
||||
},
|
||||
|
||||
// 前区球号命中率分析
|
||||
getFrontBallHitRate() {
|
||||
return dltApi.get('/dlt-predict/front-ball-hit-rate')
|
||||
},
|
||||
|
||||
// 后区首球命中率分析
|
||||
getBackFirstBallHitRate() {
|
||||
return dltApi.get('/dlt-predict/back-first-ball-hit-rate')
|
||||
},
|
||||
|
||||
// 后区球号命中率分析
|
||||
getBackBallHitRate() {
|
||||
return dltApi.get('/dlt-predict/back-ball-hit-rate')
|
||||
},
|
||||
|
||||
// 精推版大乐透第一步分析
|
||||
jtdltFirstStepAnalysis(level, frontBalls, backBalls) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('level', level)
|
||||
params.append('frontBalls', frontBalls)
|
||||
params.append('backBalls', backBalls)
|
||||
|
||||
return dltApi.post('/jtdlt/analysis/first-step', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 精推版大乐透第二步分析
|
||||
jtdltSecondStepAnalysis(level, previousFrontBalls, previousBackBalls, currentFirstBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('level', level)
|
||||
params.append('previousFrontBalls', previousFrontBalls)
|
||||
params.append('previousBackBalls', previousBackBalls)
|
||||
params.append('currentFirstBall', currentFirstBall)
|
||||
|
||||
return dltApi.post('/jtdlt/analysis/second-step', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 精推版大乐透第三步分析
|
||||
jtdltThirdStepAnalysis(level, previousFrontBalls, previousBackBalls, currentFrontBalls) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('level', level)
|
||||
params.append('previousFrontBalls', previousFrontBalls)
|
||||
params.append('previousBackBalls', previousBackBalls)
|
||||
params.append('currentFrontBalls', currentFrontBalls)
|
||||
|
||||
return dltApi.post('/jtdlt/analysis/third-step', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 精推版大乐透第四步分析
|
||||
jtdltFourthStepAnalysis(level, previousFrontBalls, previousBackBalls, currentFrontBalls, currentBackFirstBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('level', level)
|
||||
params.append('previousFrontBalls', previousFrontBalls)
|
||||
params.append('previousBackBalls', previousBackBalls)
|
||||
params.append('currentFrontBalls', currentFrontBalls)
|
||||
params.append('currentBackFirstBall', currentBackFirstBall)
|
||||
|
||||
return dltApi.post('/jtdlt/analysis/fourth-step', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default dltLotteryApi
|
||||
891
src/api/index.js
Normal file
@@ -0,0 +1,891 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
// baseURL: 'http://localhost:8123/api',
|
||||
baseURL: 'https://www.yicaishuzhi.com/api',
|
||||
timeout: 300000, // 5分钟超时时间(300秒)
|
||||
withCredentials: true, // 关键:支持跨域携带cookie和session
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 防止重复提示的标志
|
||||
let isKickedOut = false
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
const data = response.data
|
||||
|
||||
// 检查是否是session过期的响应
|
||||
if (data && data.success === false) {
|
||||
// 可以根据后端返回的错误码或消息判断是否是session过期
|
||||
const message = data.message || ''
|
||||
|
||||
// 专门处理账号在其他设备登录的情况
|
||||
if (message.includes('其他设备登录') || message.includes('当前会话已失效')) {
|
||||
if (!isKickedOut) {
|
||||
isKickedOut = true
|
||||
console.log('检测到账号在其他设备登录,正在踢出当前会话...')
|
||||
|
||||
// 动态导入 Element Plus 的消息组件
|
||||
import('element-plus').then(({ ElMessage }) => {
|
||||
ElMessage.warning({
|
||||
message: '您的账号已在其他设备登录,请重新登录',
|
||||
duration: 3000,
|
||||
showClose: true
|
||||
})
|
||||
})
|
||||
|
||||
// 检查当前路径是否为后台管理路径
|
||||
if (window.location.pathname.startsWith('/cpzsadmin') && window.location.pathname !== '/cpzsadmin/login') {
|
||||
// 后台管理会话
|
||||
import('../store/user.js').then(({ userStore }) => {
|
||||
userStore.adminLogout(true) // 标记为被踢出
|
||||
setTimeout(() => {
|
||||
window.location.href = '/cpzsadmin/login'
|
||||
isKickedOut = false // 重置标志
|
||||
}, 1500)
|
||||
})
|
||||
} else {
|
||||
// 前台用户会话
|
||||
import('../store/user.js').then(({ userStore }) => {
|
||||
userStore.logout(true) // 标记为被踢出
|
||||
setTimeout(() => {
|
||||
isKickedOut = false // 重置标志
|
||||
}, 3000)
|
||||
})
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 处理其他登录/权限相关的错误
|
||||
if (message.includes('未登录') || message.includes('登录过期') || message.includes('无权限') || message.includes('Invalid session')) {
|
||||
console.log('检测到session过期,清除本地登录状态')
|
||||
|
||||
// 检查当前路径是否为后台管理路径
|
||||
if (window.location.pathname.startsWith('/cpzsadmin') && window.location.pathname !== '/cpzsadmin/login') {
|
||||
console.log('后台管理会话过期,正在注销...')
|
||||
// 动态导入userStore避免循环依赖
|
||||
import('../store/user.js').then(({ userStore }) => {
|
||||
userStore.adminLogout()
|
||||
window.location.href = '/cpzsadmin/login'
|
||||
})
|
||||
} else {
|
||||
// 前台用户会话过期处理
|
||||
import('../store/user.js').then(({ userStore }) => {
|
||||
userStore.logout()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
error => {
|
||||
console.error('API请求错误:', error)
|
||||
|
||||
// 检查HTTP状态码,401/403通常表示未授权/session过期
|
||||
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
|
||||
console.log('检测到401/403错误,可能是session过期或无权限')
|
||||
|
||||
// 检查当前路径是否为后台管理路径
|
||||
if (window.location.pathname.startsWith('/cpzsadmin') && window.location.pathname !== '/cpzsadmin/login') {
|
||||
console.log('后台管理会话过期,正在注销...')
|
||||
// 动态导入userStore避免循环依赖
|
||||
import('../store/user.js').then(({ userStore }) => {
|
||||
userStore.adminLogout()
|
||||
window.location.href = '/cpzsadmin/login'
|
||||
})
|
||||
} else {
|
||||
// 前台用户会话过期处理
|
||||
import('../store/user.js').then(({ userStore }) => {
|
||||
userStore.logout()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// API接口方法
|
||||
export const lotteryApi = {
|
||||
// 用户登录
|
||||
userLogin(userAccount, userPassword) {
|
||||
return api.post('/user/login', {
|
||||
userAccount,
|
||||
userPassword
|
||||
})
|
||||
},
|
||||
|
||||
// 用户注册
|
||||
userRegister(userAccount, userPassword, checkPassword, userName) {
|
||||
return api.post('/user/register', {
|
||||
userAccount,
|
||||
userPassword,
|
||||
checkPassword,
|
||||
userName
|
||||
})
|
||||
},
|
||||
|
||||
// 用户注销
|
||||
userLogout() {
|
||||
return api.post('/user/logout')
|
||||
},
|
||||
|
||||
// 获取当前登录用户信息
|
||||
getLoginUser() {
|
||||
return api.get('/user/get/login')
|
||||
},
|
||||
|
||||
// 获取用户统计信息(总用户数和VIP用户数)
|
||||
getUserCount() {
|
||||
return api.get('/user/count')
|
||||
},
|
||||
|
||||
// 检查当前用户VIP是否过期
|
||||
checkVipExpire() {
|
||||
return api.get('/user/check-vip-expire')
|
||||
},
|
||||
|
||||
// 获取用户推测记录(支持分页)
|
||||
getPredictRecordsByUserId(userId, page = 1) {
|
||||
return api.get(`/ball-analysis/predict-records/${userId}?page=${page}`)
|
||||
},
|
||||
|
||||
// 按条件查询推测记录(支持分页和状态筛选)
|
||||
queryPredictRecords(userId, predictStatus, page = 1, pageSize = 10) {
|
||||
return api.post('/data-analysis/query-predict-records', {
|
||||
userId: userId,
|
||||
predictStatus,
|
||||
current: page,
|
||||
pageSize
|
||||
})
|
||||
},
|
||||
|
||||
// 获取近期开奖信息
|
||||
getRecentDraws(limit = 6) {
|
||||
return api.get(`/ball-analysis/recent-draws?limit=${limit}`)
|
||||
},
|
||||
|
||||
// 获取最新100条开奖信息(表相查询)
|
||||
getRecent100Draws() {
|
||||
return api.get('/ball-analysis/recent-100-draws')
|
||||
},
|
||||
|
||||
// 按日期范围查询开奖信息
|
||||
queryDraws(startDate, endDate) {
|
||||
const params = new URLSearchParams()
|
||||
if (startDate) params.append('startDate', startDate)
|
||||
if (endDate) params.append('endDate', endDate)
|
||||
return api.get(`/ball-analysis/query-draws?${params.toString()}`)
|
||||
},
|
||||
|
||||
// 根据期号查询开奖信息
|
||||
getDrawById(drawId) {
|
||||
return api.get(`/ball-analysis/draw/${drawId}`)
|
||||
},
|
||||
|
||||
// 创建推测记录
|
||||
createPredictRecord(userId, drawId, drawDate, redBalls, blueBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('userId', userId)
|
||||
params.append('drawId', drawId)
|
||||
params.append('drawDate', drawDate)
|
||||
params.append('redBalls', redBalls)
|
||||
params.append('blueBall', blueBall)
|
||||
|
||||
return api.post('/ball-analysis/create-predict', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 首球算法
|
||||
analyzeBalls(userId, level, redBalls, blueBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('userId', userId)
|
||||
params.append('level', level)
|
||||
params.append('redBalls', redBalls)
|
||||
params.append('blueBall', blueBall)
|
||||
|
||||
return api.post('/ball-analysis/analyze', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 精推版双色球第一步分析
|
||||
jtssqFirstStepAnalysis(level, redBalls, blueBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('level', level)
|
||||
params.append('redBalls', redBalls)
|
||||
params.append('blueBall', blueBall)
|
||||
|
||||
return api.post('/jtssq/analysis/first-step', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 精推版双色球第二步分析
|
||||
jtssqSecondStepAnalysis(level, redBalls, blueBall, nextFirstBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('level', level)
|
||||
params.append('redBalls', redBalls)
|
||||
params.append('blueBall', blueBall)
|
||||
params.append('nextFirstBall', nextFirstBall)
|
||||
|
||||
return api.post('/jtssq/analysis/second-step', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 精推版双色球第三步分析
|
||||
jtssqThirdStepAnalysis(level, redBalls, blueBall, nextRedBalls) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('level', level)
|
||||
params.append('redBalls', redBalls)
|
||||
params.append('blueBall', blueBall)
|
||||
params.append('nextRedBalls', nextRedBalls)
|
||||
|
||||
return api.post('/jtssq/analysis/third-step', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 跟随球号分析算法
|
||||
fallowBallAnalysis(userId, level, firstThreeRedBalls, lastSixRedBalls, blueBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('userId', userId)
|
||||
params.append('level', level)
|
||||
params.append('firstThreeRedBalls', firstThreeRedBalls)
|
||||
params.append('lastSixRedBalls', lastSixRedBalls)
|
||||
params.append('blueBall', blueBall)
|
||||
|
||||
return api.post('/ball-analysis/fallow', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 蓝球分析算法
|
||||
blueBallAnalysis(userId, level, predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('userId', userId)
|
||||
params.append('level', level)
|
||||
params.append('predictedRedBalls', predictedRedBalls)
|
||||
params.append('predictedBlueBalls', predictedBlueBalls)
|
||||
params.append('lastRedBalls', lastRedBalls)
|
||||
params.append('lastBlueBall', lastBlueBall)
|
||||
|
||||
return api.post('/ball-analysis/blue-ball', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 获取用户推测统计数据
|
||||
getUserPredictStat(userId, lotteryType) {
|
||||
return api.get(`/data-analysis/user-predict-stat/${userId}`, {
|
||||
params: { lotteryType }
|
||||
})
|
||||
},
|
||||
|
||||
// 获取首球命中率统计
|
||||
getFirstBallHitRate(lotteryType) {
|
||||
return api.get('/ball-analysis/first-ball-hit-rate', {
|
||||
params: { lotteryType }
|
||||
})
|
||||
},
|
||||
|
||||
// 获取蓝球命中率统计
|
||||
getBlueBallHitRate(lotteryType) {
|
||||
return api.get('/ball-analysis/blue-ball-hit-rate', {
|
||||
params: { lotteryType }
|
||||
})
|
||||
},
|
||||
|
||||
// 获取红球命中率统计
|
||||
getRedBallHitRate(lotteryType) {
|
||||
return api.get('/ball-analysis/red-ball-hit-rate', {
|
||||
params: { lotteryType }
|
||||
})
|
||||
},
|
||||
|
||||
// 获取奖金统计
|
||||
getPrizeStatistics() {
|
||||
return api.get('/ball-analysis/prize-statistics')
|
||||
},
|
||||
|
||||
// 激活会员码
|
||||
activateVipCode(userId, code) {
|
||||
return api.post('/user/activate-vip', {
|
||||
userId,
|
||||
code
|
||||
})
|
||||
},
|
||||
|
||||
// 批量生成会员码
|
||||
generateVipCodes(numCodes, vipExpireTime) {
|
||||
return api.post('/vip-code/generate', {
|
||||
numCodes,
|
||||
vipExpireTime
|
||||
})
|
||||
},
|
||||
|
||||
// 获取可用会员码
|
||||
getAvailableVipCode(vipExpireTime) {
|
||||
return api.get(`/vip-code/available?vipExpireTime=${vipExpireTime}`)
|
||||
},
|
||||
|
||||
// 上传Excel文件导入数据(T1-T7 sheet)
|
||||
uploadExcelFile(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return api.post('/excel/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 上传Excel文件导入开奖数据(T10工作表)
|
||||
uploadLotteryDrawsFile(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return api.post('/excel/upload-lottery-draws', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 上传Excel文件追加导入开奖数据(T10工作表)
|
||||
appendLotteryDrawsFile(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return api.post('/excel/append-lottery-draws', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 获取用户兑换记录
|
||||
getExchangeRecordsByUserId(userId) {
|
||||
return api.get(`/vip-exchange-record/user/${userId}`)
|
||||
},
|
||||
|
||||
// 获取用户列表
|
||||
getUserList(params) {
|
||||
console.log('调用获取用户列表接口:', params)
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
// 添加查询参数
|
||||
if (params?.userAccount) {
|
||||
queryParams.append('userAccount', params.userAccount)
|
||||
}
|
||||
|
||||
if (params?.userName) {
|
||||
queryParams.append('userName', params.userName)
|
||||
}
|
||||
|
||||
if (params?.phone) {
|
||||
queryParams.append('phone', params.phone)
|
||||
}
|
||||
|
||||
if (params?.userRole) {
|
||||
queryParams.append('userRole', params.userRole)
|
||||
}
|
||||
|
||||
if (params?.status !== undefined && params?.status !== '') {
|
||||
queryParams.append('status', params.status)
|
||||
}
|
||||
|
||||
if (params?.isVip !== undefined && params?.isVip !== '') {
|
||||
queryParams.append('isVip', params.isVip)
|
||||
}
|
||||
|
||||
// 分页参数
|
||||
if (params?.page) {
|
||||
queryParams.append('page', params.page)
|
||||
}
|
||||
|
||||
if (params?.size) {
|
||||
queryParams.append('size', params.size)
|
||||
}
|
||||
|
||||
return api.get(`/user/list${queryParams.toString() ? '?' + queryParams.toString() : ''}`)
|
||||
},
|
||||
|
||||
// 更新用户状态
|
||||
updateUserStatus(params) {
|
||||
console.log('调用更新用户状态接口:', params)
|
||||
// 使用Content-Type: application/json 调用接口
|
||||
return api.post('/user/update-status', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000 // 设置10秒超时
|
||||
}).then(response => {
|
||||
console.log('更新用户状态接口响应:', response)
|
||||
return response
|
||||
}).catch(error => {
|
||||
console.error('更新用户状态接口错误:', error)
|
||||
throw error
|
||||
})
|
||||
},
|
||||
|
||||
// 添加用户
|
||||
addUser(userForm) {
|
||||
console.log('调用添加用户接口:', userForm)
|
||||
return api.post('/user/add', userForm)
|
||||
},
|
||||
|
||||
// 更新用户
|
||||
updateUser(userForm) {
|
||||
console.log('调用更新用户接口:', userForm)
|
||||
return api.post('/user/update', userForm)
|
||||
},
|
||||
|
||||
// 删除用户
|
||||
deleteUser(userId) {
|
||||
console.log('调用删除用户接口:', userId)
|
||||
return api.post('/user/delete', { id: userId }, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 获取所有推测记录总数
|
||||
getTotalPredictCount() {
|
||||
console.log('调用getTotalPredictCount接口...')
|
||||
return api.get('/data-analysis/total-predict-count').then(response => {
|
||||
console.log('getTotalPredictCount接口响应:', response)
|
||||
return response
|
||||
}).catch(error => {
|
||||
console.error('getTotalPredictCount接口错误:', error)
|
||||
throw error
|
||||
})
|
||||
},
|
||||
|
||||
// 根据用户ID和操作模块获取操作历史
|
||||
getOperationHistoryByUserIdAndModule(userId, operationModule) {
|
||||
return api.get(`/operation-history/user/${userId}/module/${operationModule}`)
|
||||
},
|
||||
|
||||
// 管理员登录
|
||||
adminLogin(username, password) {
|
||||
// 模拟API请求,实际项目中应该调用真实的后端API
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
// 模拟验证管理员账号密码
|
||||
if (username === 'admin' && password === '123456') {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
id: 1,
|
||||
userName: '系统管理员',
|
||||
userAccount: 'admin',
|
||||
userRole: 'admin',
|
||||
avatar: null,
|
||||
createTime: new Date().toISOString()
|
||||
},
|
||||
message: '登录成功'
|
||||
})
|
||||
} else {
|
||||
resolve({
|
||||
success: false,
|
||||
data: null,
|
||||
message: '账号或密码错误'
|
||||
})
|
||||
}
|
||||
}, 1000) // 模拟网络延迟
|
||||
})
|
||||
},
|
||||
|
||||
// 获取会员码统计数据
|
||||
getVipCodeStats() {
|
||||
return api.get('/vip-code/stats')
|
||||
},
|
||||
|
||||
// 获取会员码统计数量
|
||||
getVipCodeCount() {
|
||||
console.log('调用getVipCodeCount接口...')
|
||||
return api.get('/vip-code/count').then(response => {
|
||||
console.log('getVipCodeCount接口响应:', response)
|
||||
return response
|
||||
}).catch(error => {
|
||||
console.error('getVipCodeCount接口错误:', error)
|
||||
throw error
|
||||
})
|
||||
},
|
||||
|
||||
// 获取会员码列表
|
||||
getVipCodeList(params) {
|
||||
// 构建查询参数
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
// 分页参数
|
||||
if (params.page) queryParams.append('current', params.page)
|
||||
if (params.size) queryParams.append('pageSize', params.size)
|
||||
|
||||
// 搜索关键词(会员码)
|
||||
if (params.keyword) queryParams.append('code', params.keyword)
|
||||
|
||||
// 状态筛选
|
||||
if (params.status !== undefined && params.status !== '') {
|
||||
// 直接使用status值作为isUse参数
|
||||
queryParams.append('isUse', params.status)
|
||||
}
|
||||
|
||||
// 有效期筛选
|
||||
if (params.expireTime) queryParams.append('vipExpireTime', params.expireTime)
|
||||
|
||||
// 时间范围筛选
|
||||
if (params.startTime) queryParams.append('startTime', params.startTime)
|
||||
if (params.endTime) queryParams.append('endTime', params.endTime)
|
||||
|
||||
// 发起请求
|
||||
return api.get(`/vip-code/list/page?${queryParams.toString()}`)
|
||||
},
|
||||
|
||||
// 删除会员码
|
||||
deleteVipCode(id) {
|
||||
return api.post(`/vip-code/delete/${id}`)
|
||||
},
|
||||
|
||||
// 获取红球历史数据全部记录
|
||||
getHistoryAll() {
|
||||
return api.get('/ball-analysis/history-all')
|
||||
},
|
||||
|
||||
// 获取红球最近100期数据记录
|
||||
getHistory100() {
|
||||
return api.get('/ball-analysis/history-100')
|
||||
},
|
||||
|
||||
// 获取红球历史数据排行记录
|
||||
getHistoryTop() {
|
||||
return api.get('/ball-analysis/history-top')
|
||||
},
|
||||
|
||||
// 获取红球100期数据排行记录
|
||||
getHistoryTop100() {
|
||||
return api.get('/ball-analysis/history-top-100')
|
||||
},
|
||||
|
||||
// 获取蓝球历史数据全部记录
|
||||
getBlueHistoryAll() {
|
||||
return api.get('/ball-analysis/blue-history-all')
|
||||
},
|
||||
|
||||
// 获取蓝球最近100期数据记录
|
||||
getBlueHistory100() {
|
||||
return api.get('/ball-analysis/blue-history-100')
|
||||
},
|
||||
|
||||
// 获取蓝球历史数据排行记录
|
||||
getBlueHistoryTop() {
|
||||
return api.get('/ball-analysis/blue-history-top')
|
||||
},
|
||||
|
||||
// 获取蓝球100期数据排行记录
|
||||
getBlueHistoryTop100() {
|
||||
return api.get('/ball-analysis/blue-history-top-100')
|
||||
},
|
||||
|
||||
// 发送短信验证码
|
||||
sendSmsCode(phoneNumber) {
|
||||
return api.post('/sms/sendCode', null, {
|
||||
params: {
|
||||
phoneNumber
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 手机号登录
|
||||
userPhoneLogin(phone, code) {
|
||||
return api.post('/user/phone/login', {
|
||||
phone,
|
||||
code
|
||||
})
|
||||
},
|
||||
|
||||
// 手机号注册
|
||||
userPhoneRegister(userAccount, userPassword, checkPassword, phone, code, userName) {
|
||||
return api.post('/user/phone/register', {
|
||||
userAccount,
|
||||
userPassword,
|
||||
checkPassword,
|
||||
phone,
|
||||
code,
|
||||
userName
|
||||
})
|
||||
},
|
||||
|
||||
// 重置密码
|
||||
resetPassword(phone, code, newPassword, confirmPassword) {
|
||||
console.log('调用重置密码接口:', { phone, code, newPassword, confirmPassword })
|
||||
return api.post('/user/reset-password', {
|
||||
phone,
|
||||
code,
|
||||
newPassword,
|
||||
confirmPassword
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 红球组合分析
|
||||
redBallCombinationAnalysis(masterBall, slaveBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('masterBall', masterBall)
|
||||
params.append('slaveBall', slaveBall)
|
||||
|
||||
return api.get(`/ball-analysis/red-ball-combination-analysis?${params.toString()}`)
|
||||
},
|
||||
|
||||
// 红球与蓝球的组合性分析
|
||||
redBlueCombinationAnalysis(masterBall, slaveBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('masterBall', masterBall)
|
||||
params.append('slaveBall', slaveBall)
|
||||
|
||||
return api.get(`/ball-analysis/red-blue-combination-analysis?${params.toString()}`)
|
||||
},
|
||||
|
||||
// 蓝球与红球的组合性分析
|
||||
blueRedCombinationAnalysis(masterBall, slaveBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('masterBall', masterBall)
|
||||
params.append('slaveBall', slaveBall)
|
||||
|
||||
return api.get(`/ball-analysis/blue-red-combination-analysis?${params.toString()}`)
|
||||
},
|
||||
|
||||
// 红球与红球的接续性分析
|
||||
redRedPersistenceAnalysis(masterBall, slaveBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('masterBall', masterBall)
|
||||
params.append('slaveBall', slaveBall)
|
||||
|
||||
return api.get(`/ball-analysis/red-red-persistence-analysis?${params.toString()}`)
|
||||
},
|
||||
|
||||
// 蓝球与蓝球的接续性分析
|
||||
blueBluePersistenceAnalysis(masterBall, slaveBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('masterBall', masterBall)
|
||||
params.append('slaveBall', slaveBall)
|
||||
|
||||
return api.get(`/ball-analysis/blue-blue-persistence-analysis?${params.toString()}`)
|
||||
},
|
||||
|
||||
// 红球与蓝球的接续性分析
|
||||
redBluePersistenceAnalysis(masterBall, slaveBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('masterBall', masterBall)
|
||||
params.append('slaveBall', slaveBall)
|
||||
|
||||
return api.get(`/ball-analysis/red-blue-persistence-analysis?${params.toString()}`)
|
||||
},
|
||||
|
||||
// 蓝球与红球的接续性分析
|
||||
blueRedPersistenceAnalysis(masterBall, slaveBall) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('masterBall', masterBall)
|
||||
params.append('slaveBall', slaveBall)
|
||||
|
||||
return api.get(`/ball-analysis/blue-red-persistence-analysis?${params.toString()}`)
|
||||
},
|
||||
|
||||
// 根据开奖期号查询开奖球号
|
||||
getDrawNumbersById(drawId) {
|
||||
return api.get(`/ball-analysis/draw/${drawId}/numbers`)
|
||||
},
|
||||
|
||||
// 获取近10期开奖期号
|
||||
getRecent10DrawIds() {
|
||||
return api.get('/ball-analysis/recent-10-draw-ids')
|
||||
},
|
||||
|
||||
// 根据操作模块获取操作历史(真实接口)
|
||||
getOperationHistoryByModule(operationModule) {
|
||||
console.log('调用根据操作模块获取操作历史接口:', operationModule)
|
||||
return api.get(`/operation-history/module/${operationModule}`)
|
||||
},
|
||||
|
||||
// 根据操作结果获取操作历史(真实接口)
|
||||
getOperationHistoryByResult(operationResult) {
|
||||
console.log('调用根据操作结果获取操作历史接口:', operationResult)
|
||||
return api.get(`/operation-history/result/${operationResult}`)
|
||||
},
|
||||
|
||||
// 获取操作历史列表(统一接口)
|
||||
getOperationHistoryList(params) {
|
||||
console.log('调用获取操作历史列表接口:', params)
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params?.operationModule !== undefined && params.operationModule !== '') {
|
||||
queryParams.append('operationModule', params.operationModule)
|
||||
}
|
||||
|
||||
if (params?.operationResult !== undefined && params.operationResult !== '') {
|
||||
queryParams.append('operationResult', params.operationResult)
|
||||
}
|
||||
|
||||
if (params?.keyword !== undefined && params.keyword !== '') {
|
||||
queryParams.append('keyword', params.keyword)
|
||||
}
|
||||
|
||||
return api.get(`/operation-history/list${queryParams.toString() ? '?' + queryParams.toString() : ''}`)
|
||||
},
|
||||
|
||||
// ==================== 公告管理接口 ====================
|
||||
|
||||
// 添加公告
|
||||
addAnnouncement(data) {
|
||||
console.log('调用添加公告接口:', data)
|
||||
return api.post('/announcement/add', data)
|
||||
},
|
||||
|
||||
// 查询公告列表(分页)
|
||||
getAnnouncementList(params) {
|
||||
console.log('调用查询公告列表接口:', params)
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params?.current) queryParams.append('current', params.current)
|
||||
if (params?.pageSize) queryParams.append('pageSize', params.pageSize)
|
||||
if (params?.title) queryParams.append('title', params.title)
|
||||
if (params?.status !== undefined && params?.status !== null && params?.status !== '') {
|
||||
queryParams.append('status', params.status)
|
||||
}
|
||||
if (params?.priority !== undefined && params?.priority !== null && params?.priority !== '') {
|
||||
queryParams.append('priority', params.priority)
|
||||
}
|
||||
if (params?.publisherId) queryParams.append('publisherId', params.publisherId)
|
||||
if (params?.publisherName) queryParams.append('publisherName', params.publisherName)
|
||||
if (params?.startTime) queryParams.append('startTime', params.startTime)
|
||||
if (params?.endTime) queryParams.append('endTime', params.endTime)
|
||||
|
||||
return api.get(`/announcement/list/page${queryParams.toString() ? '?' + queryParams.toString() : ''}`)
|
||||
},
|
||||
|
||||
// 根据ID查询公告详情
|
||||
getAnnouncementById(id) {
|
||||
console.log('调用根据ID查询公告接口:', id)
|
||||
return api.get(`/announcement/${id}`)
|
||||
},
|
||||
|
||||
// 更新公告
|
||||
updateAnnouncement(data) {
|
||||
console.log('调用更新公告接口:', data)
|
||||
return api.post('/announcement/update', data)
|
||||
},
|
||||
|
||||
// 删除公告
|
||||
deleteAnnouncement(id) {
|
||||
console.log('调用删除公告接口:', id)
|
||||
return api.delete(`/announcement/delete/${id}`)
|
||||
},
|
||||
|
||||
// 获取置顶公告
|
||||
getTopAnnouncements() {
|
||||
console.log('调用获取置顶公告接口')
|
||||
return api.get('/announcement/top')
|
||||
},
|
||||
|
||||
// 获取所有已发布公告
|
||||
getPublishedAnnouncements() {
|
||||
console.log('调用获取所有已发布公告接口')
|
||||
return api.get('/announcement/published')
|
||||
},
|
||||
|
||||
// ==================== 推测管理接口 ====================
|
||||
|
||||
// 管理员获取所有双色球推测记录
|
||||
getAllSsqPredictRecords(params) {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.userId) queryParams.append('userId', params.userId)
|
||||
if (params?.predictResult) queryParams.append('predictResult', params.predictResult)
|
||||
if (params?.current) queryParams.append('current', params.current)
|
||||
if (params?.pageSize) queryParams.append('pageSize', params.pageSize)
|
||||
|
||||
return api.get(`/ball-analysis/admin/all-records?${queryParams.toString()}`)
|
||||
},
|
||||
|
||||
// 管理员获取所有大乐透推测记录
|
||||
getAllDltPredictRecords(params) {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.userId) queryParams.append('userId', params.userId)
|
||||
if (params?.predictResult) queryParams.append('predictResult', params.predictResult)
|
||||
if (params?.current) queryParams.append('current', params.current)
|
||||
if (params?.pageSize) queryParams.append('pageSize', params.pageSize)
|
||||
|
||||
return api.get(`/dlt-predict/admin/all-records?${queryParams.toString()}`)
|
||||
},
|
||||
|
||||
// ==================== 奖金统计接口 ====================
|
||||
|
||||
// 管理员获取双色球中奖记录明细
|
||||
getAdminPrizeStatistics(params) {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.userId) queryParams.append('userId', params.userId)
|
||||
if (params?.prizeGrade) queryParams.append('prizeGrade', params.prizeGrade)
|
||||
if (params?.current) queryParams.append('current', params.current)
|
||||
if (params?.pageSize) queryParams.append('pageSize', params.pageSize)
|
||||
|
||||
return api.get(`/ball-analysis/admin/prize-statistics?${queryParams.toString()}`)
|
||||
},
|
||||
|
||||
// 管理员获取大乐透中奖记录明细
|
||||
getAdminDltPrizeStatistics(params) {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.userId) queryParams.append('userId', params.userId)
|
||||
if (params?.prizeGrade) queryParams.append('prizeGrade', params.prizeGrade)
|
||||
if (params?.current) queryParams.append('current', params.current)
|
||||
if (params?.pageSize) queryParams.append('pageSize', params.pageSize)
|
||||
|
||||
return api.get(`/dlt-predict/admin/prize-statistics?${queryParams.toString()}`)
|
||||
},
|
||||
|
||||
// 记录页面访问PV
|
||||
recordPageView(pagePath) {
|
||||
return api.post(`/pv/record?pagePath=${encodeURIComponent(pagePath)}`)
|
||||
},
|
||||
|
||||
// 获取总PV
|
||||
getTotalPageViews() {
|
||||
return api.get('/pv/total')
|
||||
},
|
||||
|
||||
// 获取今日PV
|
||||
getTodayPageViews() {
|
||||
return api.get('/pv/today')
|
||||
},
|
||||
|
||||
// 获取近N天PV统计
|
||||
getPageViewsByDays(days = 7) {
|
||||
return api.get(`/pv/stats?days=${days}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default api
|
||||
BIN
src/assets/3D.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
src/assets/7lecai.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
src/assets/7星彩.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
src/assets/backend.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
src/assets/banner/backend1.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
src/assets/banner/banner.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
9
src/assets/banner/banner.svg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
src/assets/banner/home.png
Normal file
|
After Width: | Height: | Size: 207 B |
BIN
src/assets/banner/kaijiang.png
Normal file
|
After Width: | Height: | Size: 257 B |
BIN
src/assets/banner/wode.png
Normal file
|
After Width: | Height: | Size: 218 B |
86
src/assets/base.css
Normal file
@@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
BIN
src/assets/daletou.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
4
src/assets/icon/ai.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg t="1753944963328" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6211" width="32" height="32">
|
||||
<path d="M603.904 244.992c134.4 0 243.3536 108.9536 243.3536 243.3536v202.8032c0 134.4-108.9536 243.3536-243.3536 243.3536H320c-134.4 0-243.3536-108.9536-243.3536-243.3536V488.3456c0-134.4 108.9536-243.3536 243.3536-243.3536h283.904z m0 81.1008H320c-89.6 0-162.2528 72.6528-162.2528 162.2528v202.8032c0 89.6 72.6528 162.2528 162.2528 162.2528h283.904c89.6 0 162.2528-72.6528 162.2528-162.2528V488.3456c0-89.6-72.6528-162.2528-162.2528-162.2528z" fill="#1296db" p-id="6212"></path>
|
||||
<path d="M340.224 508.6208c27.0336 0 40.5504 13.5168 40.5504 40.5504v81.1008c0 27.0336-13.5168 40.5504-40.5504 40.5504-27.0336 0-40.5504-13.5168-40.5504-40.5504v-81.1008c0-27.0336 13.5168-40.5504 40.5504-40.5504zM583.2192 501.3504c15.2576-11.4176 36.864-8.3456 48.2816 6.912a34.4832 34.4832 0 0 1-6.912 48.2816l-44.3904 33.28 44.3904 33.28a34.53952 34.53952 0 0 1 9.472 44.3392l-2.56 3.9424c-11.4176 15.2576-33.024 18.3296-48.2816 6.912l-59.4944-44.5952c-13.7728-10.3424-21.9136-26.5728-21.9136-43.8272s8.0896-33.4848 21.9136-43.8272l59.4944-44.5952zM883.5072 261.12l-19.7632 47.3088c-2.7648 6.656-9.2672 10.9568-16.4864 10.9568s-13.6704-4.3008-16.4864-10.9568L811.008 261.12a44.416 44.416 0 0 0-19.7632-21.9648l-34.8672-19.0976c-5.7344-3.1232-9.2672-9.1136-9.2672-15.6672s3.5328-12.544 9.2672-15.6672l34.8672-19.0464a44.89728 44.89728 0 0 0 19.7632-21.9648l19.7632-47.3088c2.7648-6.656 9.2672-10.9568 16.4864-10.9568s13.6704 4.3008 16.4864 10.9568l19.7632 47.3088a44.416 44.416 0 0 0 19.7632 21.9648l34.8672 19.0976c5.7344 3.1232 9.2672 9.1136 9.2672 15.6672s-3.584 12.544-9.2672 15.6672l-34.8672 19.0464c-8.9088 4.864-15.872 12.6464-19.7632 21.9648z" fill="#1296db" p-id="6213"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
src/assets/icon/bzzx.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1753948292582" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18768" width="32" height="32"><path d="M891.033093 98.392071 649.86931 98.392071c-56.248614 0-106.30472 27.178229-137.783303 68.977658-31.478582-41.799429-81.534688-68.977658-137.783303-68.977658L132.966907 98.392071c-38.015118 0-68.977658 30.96254-68.977658 68.977658l0 637.484294c0 38.015118 30.96254 68.977658 68.977658 68.977658l253.376785 0c33.88678 33.026709 78.782463 51.604233 125.742315 51.604233s91.855535-18.577524 125.742315-51.604233L891.033093 873.831682c38.015118 0 68.977658-30.96254 68.977658-68.977658L960.010751 167.36973C960.010751 129.354611 929.048211 98.392071 891.033093 98.392071zM891.033093 804.854023 622.863094 804.854023c-9.976818 0-19.609609 4.300353-26.146145 12.040988-21.501764 25.11406-52.464304 39.563245-84.630942 39.563245-32.166639 0-63.129179-14.449185-84.630942-39.563245-6.536536-7.740635-16.169326-12.040988-26.146145-12.040988l-268.342012 0L132.966907 167.36973l241.163783 0c56.936671 0 103.38048 46.44381 103.38048 103.38048l0 292.94003c0 19.093566 15.48127 34.402822 34.402822 34.402822 19.093566 0 34.402822-15.48127 34.402822-34.402822L546.316815 270.75021c0-56.936671 46.44381-103.38048 103.38048-103.38048L891.033093 167.36973 891.033093 804.854023z" fill="#2c2c2c" p-id="18769"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/icon/dhjl.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1753948236353" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17518" width="32" height="32"><path d="M828.61 878.22H195.46c-42.09 0-76.34-34.25-76.34-76.34V699.84c0-25.27 14.05-48.16 36.67-59.74 37.8-19.36 63.2-69.43 63.2-124.59 0-55.01-25.31-105.04-62.99-124.48-22.75-11.74-36.88-34.81-36.88-60.21V228.31c0-42.09 34.25-76.34 76.34-76.34h633.16c42.09 0 76.33 34.24 76.33 76.33v102.78c0 25.3-14.06 48.27-36.7 59.95-37.68 19.44-62.99 69.46-62.99 124.48 0 55 25.3 105.02 62.96 124.47 22.66 11.7 36.73 34.7 36.73 60.01v101.89c-0.01 42.1-34.25 76.34-76.34 76.34zM195.12 705.25v96.63c0 0.19 0.15 0.34 0.34 0.34h633.16c0.18 0 0.33-0.15 0.33-0.33v-96.74c-60.72-33.82-99.69-107.62-99.69-189.63 0-82.02 38.97-155.81 99.69-189.63V228.3c0-0.18-0.15-0.33-0.33-0.33H195.46c-0.19 0-0.34 0.15-0.34 0.34v97.47c60.82 33.78 99.87 107.63 99.87 189.73 0 82.16-39.03 155.96-99.87 189.74z" p-id="17519" fill="#2c2c2c"></path><path d="M643.78 421.79H380.22c-20.99 0-38-17.01-38-38s17.01-38 38-38h263.57c20.99 0 38 17.01 38 38-0.01 20.99-17.02 38-38.01 38zM643.78 616.58H380.22c-20.99 0-38-17.01-38-38s17.01-38 38-38h263.57c20.99 0 38 17.01 38 38-0.01 20.99-17.02 38-38.01 38z" p-id="17520" fill="#2c2c2c"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
src/assets/icon/gywm.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1753948313670" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19844" width="32" height="32"><path d="M513 5C232.9 5 5 232.9 5 513s227.9 508 508 508 508-227.9 508-508S793.1 5 513 5zM217.4 867.4c63.9-145 177.4-234.3 300.3-234.3 121.6 0 232.5 85.3 297.6 228.2-81.1 70.5-186.7 113.5-302.3 113.5-112.4 0-215.4-40.4-295.6-107.4zM513 560.8c-73.6 0-133.6-59.9-133.6-133.6s60-133.6 133.6-133.6 133.6 59.9 133.6 133.6-60 133.6-133.6 133.6z m337.3 266.6c-62.2-127.2-159.4-210.7-269.7-233.8 65.7-26.8 112.2-91.2 112.2-166.5 0-99.1-80.6-179.8-179.8-179.8s-179.8 80.6-179.8 179.8c0 76.6 48.3 142.1 116 167.9-109.9 25.5-207.1 111.6-267.5 239-80.6-83.1-130.5-196.3-130.5-321C51.2 258.4 258.4 51.2 513 51.2S974.8 258.4 974.8 513c0 121.5-47.5 231.9-124.5 314.4z" fill="#2c2c2c" p-id="19845"></path></svg>
|
||||
|
After Width: | Height: | Size: 840 B |
BIN
src/assets/icon/login/mima.png
Normal file
|
After Width: | Height: | Size: 381 B |
BIN
src/assets/icon/login/nicheng.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
src/assets/icon/login/shouji.png
Normal file
|
After Width: | Height: | Size: 372 B |
BIN
src/assets/icon/login/yingcang.png
Normal file
|
After Width: | Height: | Size: 431 B |
BIN
src/assets/icon/login/zhanghao.png
Normal file
|
After Width: | Height: | Size: 583 B |
BIN
src/assets/icon/login/zhanshi.png
Normal file
|
After Width: | Height: | Size: 775 B |
1
src/assets/icon/sjfx.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1753945117321" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7804" width="32" height="32"><path d="M259.462 675.528H118.034a64.55 64.55 0 0 0-62.502 66.128V957.83a64.55 64.55 0 0 0 62.502 66.127h141.428a64.592 64.592 0 0 0 62.715-66.127V741.656a64.592 64.592 0 0 0-62.715-66.128z m0 304.401H118.034a19.198 19.198 0 0 1-19.199-22.12v-213.53a21.332 21.332 0 0 1 19.199-24.488h141.428a22.12 22.12 0 0 1 20.691 24.488v213.55a20.052 20.052 0 0 1-20.691 22.121z m678.343-668.595H796.377a65.061 65.061 0 0 0-62.501 67.301V956.72a65.04 65.04 0 0 0 62.501 67.28h141.428a65.19 65.19 0 0 0 62.502-67.28V378.635a65.061 65.061 0 0 0-62.502-67.3z m2.133 665.78H796.377a16.425 16.425 0 0 1-17.705-20.33V385.205a19.198 19.198 0 0 1 17.705-23.827h141.428a19.54 19.54 0 0 1 17.919 23.827v571.58c0 14.227-3.627 20.328-15.786 20.328zM440.993 476.866a64.955 64.955 0 0 0-62.714 66.81v413.449a64.933 64.933 0 0 0 62.714 66.81h141.642a64.933 64.933 0 0 0 62.715-66.81V543.677a64.955 64.955 0 0 0-62.715-66.81H440.993zM582.635 977.88H440.993a17.77 17.77 0 0 1-19.198-20.755V543.677a19.05 19.05 0 0 1 19.198-22.376h141.642a19.326 19.326 0 0 1 19.625 22.376v413.449a18.046 18.046 0 0 1-19.625 20.755zM561.73 447.493L904.528 97.272l62.928 65.061L975.99 0.021h-173l63.356 67.045-333.84 334.053-176.411-181.767a41.831 41.831 0 0 0-58.662 0L36.334 488.13a29.694 29.694 0 0 0-3.627 45.842c13.44 13.908 34.13-1.131 49.703-17.215L330.922 264.81l173 179.889a39.271 39.271 0 0 0 28.584 12.052 38.14 38.14 0 0 0 29.224-9.172z" p-id="7805" fill="#2c2c2c"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/assets/icon/tcjl.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1753948072083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9272" width="32" height="32"><path d="M754.346667 212.906667v-81.066667s7.68-58.453333-65.706667-58.453333H126.293333s-48.213333-2.56-48.213333 48.213333v706.56s5.12 58.453333 60.586667 58.453333h144.213333s27.733333 5.12 27.733333 35.413334-27.733333 30.293333-27.733333 30.293333h-179.2S7.253333 934.4 7.253333 846.08V129.28S-2.986667 5.12 128.853333 5.12h582.4s103.68 0 106.24 96.426667 2.56 111.36 2.56 111.36-5.12 30.293333-32.853333 30.293333c-27.733333 2.56-32.853333-25.6-32.853333-30.293333z m0 0" p-id="9273" fill="#2c2c2c"></path><path d="M619.946667 506.453333v136.96s-2.56 15.36 12.8 30.293334l134.4 134.4s22.613333 10.24 40.533333-7.68c20.053333-20.053333-2.56-45.653333-2.56-45.653334l-111.36-116.48s-7.68-5.12-7.68-27.733333v-106.24s-10.24-27.733333-32.853333-27.733333c-23.04 2.133333-33.28 22.186667-33.28 29.866666z m0 0" p-id="9274" fill="#2c2c2c"></path><path d="M686.08 346.88c-184.746667 0-336.64 149.333333-336.64 336.64 0 184.746667 149.333333 336.64 336.64 336.64 184.746667 0 336.64-149.333333 336.64-336.64-2.56-187.306667-151.893333-336.64-336.64-336.64z m0 608c-149.333333 0-270.933333-121.6-270.933333-270.933333 0-149.333333 121.6-270.933333 270.933333-270.933334 149.333333 0 270.933333 121.6 270.933333 270.933334 0 149.333333-121.6 270.933333-270.933333 270.933333zM290.986667 478.72H199.68c-15.36 0-27.733333-12.8-27.733333-27.733333v-15.36c0-15.36 12.8-27.733333 27.733333-27.733334h91.306667c15.36 0 27.733333 12.8 27.733333 27.733334v15.36c0 17.493333-12.8 27.733333-27.733333 27.733333z m339.2-202.666667H199.68c-15.36 0-27.733333-12.8-27.733333-27.733333v-15.36c0-15.36 12.8-27.733333 27.733333-27.733333h427.946667c15.36 0 27.733333 12.8 27.733333 27.733333v15.36c0 14.933333-12.373333 27.733333-25.173333 27.733333z m0 0" p-id="9275" fill="#2c2c2c"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/icon/tuichu.png
Normal file
|
After Width: | Height: | Size: 633 B |
BIN
src/assets/icon/zuanshi.png
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
src/assets/kuaile8.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
1
src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
25
src/assets/main.css
Normal file
@@ -0,0 +1,25 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
margin: 0 auto;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: rgb(240, 242, 245);
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移除可能影响布局的媒体查询 */
|
||||
BIN
src/assets/pailie3.png
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
src/assets/pailie5.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
src/assets/shuangseqiu.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
src/assets/weixin.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
440
src/components/AlgorithmProcessModal.vue
Normal file
@@ -0,0 +1,440 @@
|
||||
<template>
|
||||
<Transition name="modal-fade">
|
||||
<div v-if="visible" class="custom-modal-overlay" @click.self="handleOverlayClick">
|
||||
<div class="custom-modal el-dialog--center" :class="themeClass">
|
||||
<div class="custom-modal-header">
|
||||
<h3 class="custom-modal-title">{{ title }}</h3>
|
||||
<button class="custom-modal-close" @click="handleClose">
|
||||
<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||
<path d="M810.666667 273.493333L750.506667 213.333333 512 451.84 273.493333 213.333333 213.333333 273.493333 451.84 512 213.333333 750.506667 273.493333 810.666667 512 572.16 750.506667 810.666667 810.666667 750.506667 572.16 512z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="custom-modal-body">
|
||||
<div class="algorithm-process-content">
|
||||
<p v-if="introText" class="process-intro">{{ introText }}</p>
|
||||
<div class="process-text">
|
||||
{{ formattedProcessText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="custom-modal-footer">
|
||||
<button class="custom-modal-button" @click="handleClose">{{ confirmText }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AlgorithmProcessModal',
|
||||
props: {
|
||||
// 弹窗显示状态
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 弹窗标题
|
||||
title: {
|
||||
type: String,
|
||||
default: '猪手专业算法筛选过程'
|
||||
},
|
||||
// 介绍文本
|
||||
introText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 确认按钮文本
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '我知道了'
|
||||
},
|
||||
// 筛选过程原始文本
|
||||
processText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否启用文本格式化(大乐透需要,双色球不需要)
|
||||
enableFormatting: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 主题类型(ssq: 双色球, dlt: 大乐透)
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'ssq',
|
||||
validator: (value) => ['ssq', 'dlt'].includes(value)
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close'],
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value)
|
||||
}
|
||||
},
|
||||
formattedProcessText() {
|
||||
if (!this.processText) {
|
||||
return '暂无筛选过程说明'
|
||||
}
|
||||
|
||||
if (this.enableFormatting) {
|
||||
return this.formatFilteringText(this.processText)
|
||||
}
|
||||
|
||||
return this.processText
|
||||
},
|
||||
themeClass() {
|
||||
return `${this.theme}-theme`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleVisibilityChange(value) {
|
||||
this.$emit('update:modelValue', value)
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.visible = false
|
||||
this.$emit('close')
|
||||
},
|
||||
|
||||
handleOverlayClick() {
|
||||
// 点击遮罩层关闭弹窗
|
||||
this.handleClose()
|
||||
},
|
||||
|
||||
// 格式化筛选过程文本(只在关键步骤处换行)
|
||||
formatFilteringText(text) {
|
||||
if (!text) return '暂无筛选过程说明'
|
||||
|
||||
return text
|
||||
// 先清理可能的重复词汇(只处理字母单词,不处理数字)
|
||||
.replace(/\b([a-zA-Z\u4e00-\u9fa5]+)\1+\b/g, '$1') // 清理连续重复的单词,但不处理数字
|
||||
.replace(/通过通过/g, '通过') // 特别处理"通过"重复
|
||||
.replace(/,,+/g, ',') // 清理连续逗号
|
||||
.replace(/。。+/g, '。') // 清理连续句号
|
||||
// 在步骤分隔处换行(如:第一步、第二步等关键词后)
|
||||
.replace(/(第[一二三四五六七八九十\d]+步[::,,])/g, '\n$1\n')
|
||||
// 在预测结果开始处换行
|
||||
.replace(/(最终推荐[::,,])/g, '\n$1')
|
||||
.replace(/(推荐结果[::,,])/g, '\n$1')
|
||||
.replace(/(筛选结果[::,,])/g, '\n$1')
|
||||
// 在重要的算法说明开始处换行
|
||||
.replace(/(算法说明[::,,])/g, '\n$1')
|
||||
.replace(/(分析过程[::,,])/g, '\n$1')
|
||||
// 在筛选步骤说明处换行
|
||||
.replace(/(筛选步骤[::,,])/g, '\n$1\n')
|
||||
// 在分号后面如果是重要内容开始则换行
|
||||
.replace(/;(\s*直接)/g, ';\n$1')
|
||||
.replace(/;(\s*筛选)/g, ';\n$1')
|
||||
.replace(/;(\s*推荐)/g, ';\n$1')
|
||||
// 在"其中"这种重要分段处适当换行(但要更谨慎)
|
||||
.replace(/([。;])(\s*其中)/g, '$1\n$2')
|
||||
// 在包含多个数字的长列表后换行(但保持在一行内的短序列不变)
|
||||
.replace(/(\d+(?:\s*,\s*\d+){4,}[\d\s,]*?)([,。])/g, '$1$2\n')
|
||||
// 清理开头和结尾的换行
|
||||
.replace(/^\n+/, '')
|
||||
.replace(/\n+$/, '')
|
||||
// 清理多余的连续换行
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
// 最后再清理一次可能的重复
|
||||
.replace(/通过通过/g, '通过')
|
||||
.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 弹窗过渡动画 */
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-fade-enter-active .custom-modal,
|
||||
.modal-fade-leave-active .custom-modal {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-fade-enter-from .custom-modal,
|
||||
.modal-fade-leave-to .custom-modal {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* 遮罩层 */
|
||||
.custom-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 弹窗容器 */
|
||||
.custom-modal {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 弹窗头部 */
|
||||
.custom-modal-header {
|
||||
padding: 20px 20px 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.custom-modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.custom-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
transition: color 0.3s ease;
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
.custom-modal-close:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
/* 弹窗主体 */
|
||||
.custom-modal-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 筛选过程内容样式 */
|
||||
.algorithm-process-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.process-intro {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.process-text {
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
line-height: 2.2;
|
||||
white-space: pre-line;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 双色球主题 */
|
||||
.custom-modal.ssq-theme .process-text {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #fff5f5 100%);
|
||||
}
|
||||
|
||||
.custom-modal.ssq-theme .process-text:hover {
|
||||
box-shadow: 0 4px 12px rgba(229, 62, 62, 0.15);
|
||||
}
|
||||
|
||||
.custom-modal.ssq-theme .custom-modal-button {
|
||||
background: linear-gradient(135deg, #e53e3e 0%, #ff6b6b 100%);
|
||||
box-shadow: 0 2px 8px rgba(229, 62, 62, 0.3);
|
||||
}
|
||||
|
||||
.custom-modal.ssq-theme .custom-modal-button:hover {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #e53e3e 100%);
|
||||
box-shadow: 0 4px 12px rgba(229, 62, 62, 0.4);
|
||||
}
|
||||
|
||||
/* 大乐透主题 */
|
||||
.custom-modal.dlt-theme .process-text {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #f7fcf7 100%);
|
||||
}
|
||||
|
||||
.custom-modal.dlt-theme .process-text:hover {
|
||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.15);
|
||||
}
|
||||
|
||||
.custom-modal.dlt-theme .custom-modal-button {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
|
||||
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.custom-modal.dlt-theme .custom-modal-button:hover {
|
||||
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
|
||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
/* 弹窗底部 */
|
||||
.custom-modal-footer {
|
||||
padding: 10px 20px 20px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.custom-modal-button {
|
||||
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.custom-modal-button:hover {
|
||||
background: linear-gradient(135deg, #66b1ff 0%, #409eff 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
|
||||
}
|
||||
|
||||
.custom-modal-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.custom-modal-overlay {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.custom-modal-header {
|
||||
padding: 15px 15px 8px;
|
||||
}
|
||||
|
||||
.custom-modal-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.custom-modal-close {
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
}
|
||||
|
||||
.custom-modal-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.process-intro {
|
||||
font-size: 15px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.process-text {
|
||||
font-size: 15px;
|
||||
line-height: 2.0;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.custom-modal-button {
|
||||
padding: 10px 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.custom-modal-overlay {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.custom-modal {
|
||||
width: 95%;
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
.custom-modal-header {
|
||||
padding: 12px 12px 6px;
|
||||
}
|
||||
|
||||
.custom-modal-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.custom-modal-close {
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
.custom-modal-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.process-intro {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.process-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-modal-footer {
|
||||
padding: 8px 12px 12px;
|
||||
}
|
||||
|
||||
.custom-modal-button {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
190
src/components/BottomNavigation.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<nav class="bottom-nav">
|
||||
<router-link to="/" class="nav-item" :class="{ active: $route.path === '/' }">
|
||||
<div class="nav-icon">
|
||||
<img :src="$route.path === '/' ? '/assets/bottom/home-1.svg' : '/assets/bottom/home-0.svg'" alt="首页" class="nav-icon-img" />
|
||||
</div>
|
||||
<div class="nav-text">首页</div>
|
||||
</router-link>
|
||||
|
||||
<div class="nav-item" @click="handleDiscoveryClick">
|
||||
<div class="nav-icon">
|
||||
<img src="/assets/bottom/faxian-0.svg" alt="交流" class="nav-icon-img" />
|
||||
</div>
|
||||
<div class="nav-text">交流</div>
|
||||
</div>
|
||||
|
||||
<router-link to="/lottery-info" class="nav-item" :class="{ active: $route.path === '/lottery-info' }">
|
||||
<div class="nav-icon">
|
||||
<img :src="$route.path === '/lottery-info' ? '/assets/bottom/kaijiang-1.svg' : '/assets/bottom/kaijiang-0.svg'" alt="开奖" class="nav-icon-img" />
|
||||
</div>
|
||||
<div class="nav-text">开奖</div>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/data-analysis" class="nav-item" :class="{ active: $route.path === '/data-analysis' }">
|
||||
<div class="nav-icon">
|
||||
<img :src="$route.path === '/data-analysis' ? '/assets/bottom/tuice-1.svg' : '/assets/bottom/tuice-0.svg'" alt="分析" class="nav-icon-img" />
|
||||
</div>
|
||||
<div class="nav-text">分析</div>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/profile" class="nav-item" :class="{ active: $route.path === '/profile' }">
|
||||
<div class="nav-icon">
|
||||
<img :src="$route.path === '/profile' ? '/assets/bottom/wode-1.svg' : '/assets/bottom/wode-0.svg'" alt="我的" class="nav-icon-img" />
|
||||
</div>
|
||||
<div class="nav-text">我的</div>
|
||||
</router-link>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BottomNavigation',
|
||||
methods: {
|
||||
handleDiscoveryClick() {
|
||||
// 触发自定义事件通知父组件
|
||||
this.$emit('discovery-click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 底部导航栏 */
|
||||
.bottom-nav {
|
||||
height: 70px;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
color: #8a8a8a;
|
||||
transition: all 0.3s ease;
|
||||
padding: 4px 8px;
|
||||
min-width: 60px;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: #ff6b35;
|
||||
background: rgba(255, 107, 53, 0.05);
|
||||
}
|
||||
|
||||
.nav-item:hover .nav-icon-img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: #ff6b35;
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
}
|
||||
|
||||
.nav-item.active .nav-text {
|
||||
color: #ff6b35;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
margin-bottom: 4px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.nav-icon-img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.bottom-nav {
|
||||
height: 65px;
|
||||
max-width: 100%;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 3px 6px;
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.nav-icon-img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.bottom-nav {
|
||||
height: 60px;
|
||||
max-width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 2px 4px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.nav-icon-img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
526
src/components/CozeChat.vue
Normal file
@@ -0,0 +1,526 @@
|
||||
<template>
|
||||
<!-- Coze AI 聊天助手容器 -->
|
||||
<div id="coze-chat-container"></div>
|
||||
|
||||
<!-- 登录提示弹窗 -->
|
||||
<div v-if="showLoginModal" class="modal-overlay" @click="hideLoginModal">
|
||||
<div class="modal-content login-prompt-modal" @click.stop>
|
||||
<div class="modal-icon">
|
||||
<svg class="login-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="8" r="3" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M12 14c-4 0-7 2-7 4v2h14v-2c0-2-3-4-7-4z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>需要登录</h3>
|
||||
<p>使用AI助手功能需要先登录您的账号</p>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" @click="hideLoginModal">取消</button>
|
||||
<button class="btn btn-primary" @click="goToLogin">立即登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { userStore } from '../store/user'
|
||||
|
||||
export default {
|
||||
name: 'CozeChat',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const cozeWebSDK = ref(null)
|
||||
const showLoginModal = ref(false)
|
||||
const router = useRouter()
|
||||
|
||||
// 加载Coze Chat SDK
|
||||
const loadCozeChatSDK = () => {
|
||||
// 检查是否已经存在script标签
|
||||
const existingScript = document.querySelector('script[src*="coze"]')
|
||||
if (existingScript) {
|
||||
console.log('Coze SDK已存在,直接初始化')
|
||||
if (window.CozeWebSDK) {
|
||||
initCozeChatClient()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 创建script标签
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/1.2.0-beta.10/libs/cn/index.js'
|
||||
script.async = true
|
||||
script.onload = () => {
|
||||
console.log('Coze SDK 加载完成')
|
||||
initCozeChatClient()
|
||||
.then(() => {
|
||||
console.log('Coze客户端初始化成功')
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to initialize Coze chat client:', error)
|
||||
})
|
||||
}
|
||||
script.onerror = (error) => {
|
||||
console.error('加载Coze SDK失败:', error)
|
||||
}
|
||||
document.body.appendChild(script)
|
||||
}
|
||||
|
||||
// 初始化Coze聊天客户端
|
||||
const initCozeChatClient = async () => {
|
||||
if (!window.CozeWebSDK) {
|
||||
console.error('Coze SDK not loaded')
|
||||
throw new Error('Coze SDK未加载')
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Initializing Coze chat client...')
|
||||
|
||||
// 获取token
|
||||
const tokenData = await fetchCozeToken()
|
||||
console.log('Token data received:', tokenData)
|
||||
|
||||
if (!tokenData || !tokenData.access_token) {
|
||||
console.error('Failed to get valid Coze token')
|
||||
throw new Error('无法获取有效的Coze token')
|
||||
}
|
||||
|
||||
console.log('Creating Coze chat client with token...')
|
||||
|
||||
// 检测设备类型
|
||||
const isMobile = checkDeviceType()
|
||||
console.log('设备类型:', isMobile ? '移动设备' : 'PC设备')
|
||||
|
||||
// 保存Coze SDK配置
|
||||
const config = {
|
||||
config: {
|
||||
bot_id: '7524958762676305971',
|
||||
},
|
||||
componentProps: {
|
||||
title: '精彩猪手',
|
||||
},
|
||||
auth: {
|
||||
type: 'token',
|
||||
token: tokenData.access_token,
|
||||
onRefreshToken: async () => {
|
||||
try {
|
||||
console.log('Refreshing token...')
|
||||
const refreshedData = await fetchCozeToken()
|
||||
if (refreshedData && refreshedData.access_token) {
|
||||
console.log('Token refreshed successfully')
|
||||
return refreshedData.access_token
|
||||
} else {
|
||||
throw new Error('Failed to refresh token')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
userInfo: {
|
||||
id: userStore.user?.id?.toString() || 'guest',
|
||||
url: 'https://wanlvzhisong-1387432270.cos.ap-guangzhou.myqcloud.com/2026/01/14/99595bd6-455b-43d9-857c-c5e6a485d94a.png',
|
||||
nickname: userStore.user?.username || '游客',
|
||||
},
|
||||
ui: {
|
||||
base: {
|
||||
icon: 'https://wanlvzhisong-1387432270.cos.ap-guangzhou.myqcloud.com/2026/01/14/fb2fb9fb-7b71-4fc0-81f9-9eb7215d0fc3.jpeg',
|
||||
layout: isMobile ? 'mobile' : 'pc',
|
||||
zIndex: 1000,
|
||||
lang:'zh-CN'
|
||||
},
|
||||
asstBtn: {
|
||||
isNeed: false, // 不展示默认悬浮球
|
||||
},
|
||||
footer: {
|
||||
isShow: false // 隐藏底部版权信息
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建聊天客户端实例
|
||||
cozeWebSDK.value = new window.CozeWebSDK.WebChatClient(config)
|
||||
console.log('Coze chat client initialized successfully')
|
||||
return cozeWebSDK.value
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing Coze chat:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 从API获取Coze token
|
||||
const fetchCozeToken = async () => {
|
||||
try {
|
||||
// 确保获取最新的用户信息
|
||||
let userId = 'guest'
|
||||
if (userStore.isLoggedIn && userStore.user?.id) {
|
||||
userId = userStore.user.id
|
||||
console.log('使用已登录用户ID:', userId)
|
||||
} else if (userStore.isLoggedIn) {
|
||||
// 如果用户已登录但没有用户信息,尝试获取
|
||||
try {
|
||||
await userStore.fetchLoginUser()
|
||||
userId = userStore.user?.id || 'guest'
|
||||
console.log('重新获取用户信息后的ID:', userId)
|
||||
} catch (error) {
|
||||
console.warn('获取用户信息失败,使用guest模式:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const jwtExpireSeconds = 3600 // 1小时
|
||||
const tokenDurationSeconds = 86400 // 24小时
|
||||
|
||||
// 根据环境确定API基础URL
|
||||
let baseUrl = ''
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
baseUrl = 'http://localhost:8123'
|
||||
} else {
|
||||
baseUrl = 'https://www.yicaishuzhi.com'
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/api/jwt/one-step-token?jwtExpireSeconds=${jwtExpireSeconds}&sessionName=${userId}&tokenDurationSeconds=${tokenDurationSeconds}`
|
||||
|
||||
console.log('Fetching Coze token, URL:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`API error: ${response.status}`, await response.text())
|
||||
throw new Error(`API error: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('Token API response:', data)
|
||||
|
||||
if (!data.success || !data.data) {
|
||||
throw new Error('API返回错误: ' + (data.message || '未知错误'))
|
||||
}
|
||||
|
||||
if (data.data.access_token) {
|
||||
console.log('Token received successfully:', data.data.access_token.substring(0, 10) + '...')
|
||||
return data.data
|
||||
} else if (typeof data.data === 'string') {
|
||||
console.log('Token string received:', data.data.substring(0, 10) + '...')
|
||||
return { access_token: data.data }
|
||||
} else {
|
||||
console.error('Invalid token format:', data.data)
|
||||
throw new Error('Invalid token format')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching Coze token:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为移动设备
|
||||
const checkDeviceType = () => {
|
||||
const isMobile = window.innerWidth <= 768
|
||||
console.log('设备类型检测:', isMobile ? '移动设备' : 'PC设备', '屏幕宽度:', window.innerWidth)
|
||||
return isMobile
|
||||
}
|
||||
|
||||
// 显示AI助手聊天窗口
|
||||
const showAIAssistant = async () => {
|
||||
// 检查用户是否已登录
|
||||
if (!userStore.isLoggedIn) {
|
||||
console.log('用户未登录,提示先登录')
|
||||
// 显示登录提示
|
||||
showLoginPrompt()
|
||||
return
|
||||
}
|
||||
|
||||
// 如果用户已登录但Coze SDK未初始化,强制重新初始化
|
||||
if (!cozeWebSDK.value) {
|
||||
console.log('用户已登录但Coze SDK未初始化,强制重新初始化')
|
||||
try {
|
||||
await forceReinitializeCozeSDK()
|
||||
// 重新初始化后直接显示聊天窗口
|
||||
if (cozeWebSDK.value) {
|
||||
cozeWebSDK.value.showChatBot()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('强制重新初始化失败:', error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果SDK已初始化,直接显示聊天窗口
|
||||
if (cozeWebSDK.value) {
|
||||
console.log('显示聊天窗口')
|
||||
try {
|
||||
cozeWebSDK.value.showChatBot()
|
||||
} catch (error) {
|
||||
console.error('显示聊天窗口失败:', error)
|
||||
// 重新初始化并显示
|
||||
await forceReinitializeCozeSDK()
|
||||
if (cozeWebSDK.value) {
|
||||
cozeWebSDK.value.showChatBot()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示登录提示
|
||||
const showLoginPrompt = () => {
|
||||
showLoginModal.value = true
|
||||
}
|
||||
|
||||
// 隐藏登录提示
|
||||
const hideLoginModal = () => {
|
||||
showLoginModal.value = false
|
||||
}
|
||||
|
||||
// 跳转到登录页
|
||||
const goToLogin = () => {
|
||||
hideLoginModal()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 强制重新初始化Coze SDK(用于用户登录后)
|
||||
const forceReinitializeCozeSDK = async () => {
|
||||
try {
|
||||
console.log('用户登录状态变化,强制重新初始化Coze SDK')
|
||||
|
||||
// 清理现有的Coze实例
|
||||
cozeWebSDK.value = null
|
||||
|
||||
// 移除已有的script标签
|
||||
const existingScripts = document.querySelectorAll('script[src*="coze"]')
|
||||
existingScripts.forEach(script => {
|
||||
try {
|
||||
document.body.removeChild(script)
|
||||
} catch (e) {
|
||||
console.warn('移除script标签失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
// 清除window上的CozeWebSDK
|
||||
if (window.CozeWebSDK) {
|
||||
delete window.CozeWebSDK
|
||||
}
|
||||
|
||||
// 重新加载并初始化SDK
|
||||
await loadCozeChatSDK()
|
||||
|
||||
console.log('Coze SDK 重新初始化完成')
|
||||
} catch (error) {
|
||||
console.error('强制重新初始化Coze SDK失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
// 加载Coze Chat SDK
|
||||
loadCozeChatSDK()
|
||||
|
||||
// 监听来自其他组件的AI助手调用事件
|
||||
window.addEventListener('showAIAssistant', showAIAssistant)
|
||||
|
||||
// 监听来自其他组件的Coze SDK重新初始化事件
|
||||
window.addEventListener('reinitializeCozeSDK', forceReinitializeCozeSDK)
|
||||
})
|
||||
|
||||
// 监听用户登录状态变化,重新初始化Coze SDK
|
||||
watch(() => userStore.isLoggedIn, async (newValue, oldValue) => {
|
||||
console.log('用户登录状态变化:', { oldValue, newValue })
|
||||
|
||||
// 只有从未登录变为已登录时才重新初始化
|
||||
if (!oldValue && newValue) {
|
||||
console.log('用户刚刚登录,需要重新初始化Coze SDK')
|
||||
// 延迟一下确保用户信息已经更新
|
||||
setTimeout(async () => {
|
||||
await forceReinitializeCozeSDK()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 如果用户退出登录,清理Coze实例
|
||||
if (oldValue && !newValue) {
|
||||
console.log('用户退出登录,清理Coze SDK')
|
||||
cozeWebSDK.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onBeforeUnmount(() => {
|
||||
// 移除事件监听器
|
||||
window.removeEventListener('showAIAssistant', showAIAssistant)
|
||||
window.removeEventListener('reinitializeCozeSDK', forceReinitializeCozeSDK)
|
||||
// 清理Coze SDK实例
|
||||
cozeWebSDK.value = null
|
||||
})
|
||||
|
||||
return {
|
||||
showLoginModal,
|
||||
hideLoginModal,
|
||||
goToLogin
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Coze AI 聊天助手样式 */
|
||||
#coze-chat-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* 登录提示弹窗样式 */
|
||||
.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: 9999;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
max-width: 380px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.login-prompt-modal {
|
||||
border: 2px solid rgba(255, 107, 53, 0.1);
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 20px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #ff6b35, #f7931e);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
.login-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #ff6b35, #f7931e);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(255, 107, 53, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 480px) {
|
||||
.modal-content {
|
||||
padding: 25px 20px;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.login-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
msg: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
94
src/components/TheWelcome.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup>
|
||||
import WelcomeItem from './WelcomeItem.vue'
|
||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||
import ToolingIcon from './icons/IconTooling.vue'
|
||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||
import CommunityIcon from './icons/IconCommunity.vue'
|
||||
import SupportIcon from './icons/IconSupport.vue'
|
||||
|
||||
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<DocumentationIcon />
|
||||
</template>
|
||||
<template #heading>Documentation</template>
|
||||
|
||||
Vue’s
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||
provides you with all information you need to get started.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<ToolingIcon />
|
||||
</template>
|
||||
<template #heading>Tooling</template>
|
||||
|
||||
This project is served and bundled with
|
||||
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||
recommended IDE setup is
|
||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||
+
|
||||
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
|
||||
you need to test your components and web pages, check out
|
||||
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||
and
|
||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||
/
|
||||
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||
|
||||
<br />
|
||||
|
||||
More instructions are available in
|
||||
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||
>.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<EcosystemIcon />
|
||||
</template>
|
||||
<template #heading>Ecosystem</template>
|
||||
|
||||
Get official tools and libraries for your project:
|
||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||
you need more resources, we suggest paying
|
||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||
a visit.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<CommunityIcon />
|
||||
</template>
|
||||
<template #heading>Community</template>
|
||||
|
||||
Got stuck? Ask your question on
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||
(our official Discord server), or
|
||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||
>StackOverflow</a
|
||||
>. You should also follow the official
|
||||
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||
Bluesky account or the
|
||||
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||
X account for latest news in the Vue world.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<SupportIcon />
|
||||
</template>
|
||||
<template #heading>Support Vue</template>
|
||||
|
||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||
us by
|
||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||
</WelcomeItem>
|
||||
</template>
|
||||
87
src/components/WelcomeItem.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
7
src/components/icons/IconCommunity.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
7
src/components/icons/IconDocumentation.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
7
src/components/icons/IconEcosystem.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
7
src/components/icons/IconSupport.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
19
src/components/icons/IconTooling.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
50
src/main.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import './assets/main.css'
|
||||
import './styles/global.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
// 引入用户状态管理,确保在应用启动时初始化
|
||||
import './store/user'
|
||||
|
||||
// 导入 Element Plus
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
// 导入 Element Plus 中文语言包
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
|
||||
// 导入 Toast 通知组件
|
||||
import Toast from 'vue-toastification'
|
||||
import 'vue-toastification/dist/index.css'
|
||||
|
||||
// Toast 配置选项
|
||||
const toastOptions = {
|
||||
position: "top-right",
|
||||
timeout: 3000,
|
||||
closeOnClick: true,
|
||||
pauseOnFocusLoss: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
draggablePercent: 0.6,
|
||||
showCloseButtonOnHover: false,
|
||||
hideProgressBar: false,
|
||||
closeButton: "button",
|
||||
icon: true,
|
||||
rtl: false
|
||||
}
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有 Element Plus 图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(router)
|
||||
app.use(Toast, toastOptions)
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
531
src/router/index.js
Normal file
@@ -0,0 +1,531 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import LotterySelection from '../views/LotterySelection.vue'
|
||||
import LotteryPremium from '../views/LotteryPremium.vue'
|
||||
import Home from '../views/ssq/Home.vue'
|
||||
import DltHome from '../views/dlt/Home.vue'
|
||||
import LotteryInfo from '../views/LotteryInfo.vue'
|
||||
import Profile from '../views/Profile.vue'
|
||||
import Login from '../views/Login.vue'
|
||||
import Register from '../views/Register.vue'
|
||||
import ResetPassword from '../views/ResetPassword.vue'
|
||||
import PredictRecords from '../views/PredictRecords.vue'
|
||||
import DltPredictRecords from '../views/dlt/PredictRecords.vue'
|
||||
|
||||
import ExcelImportManagement from '../views/ExcelImportManagement.vue'
|
||||
import ExchangeRecords from '../views/ExchangeRecords.vue'
|
||||
import TrendAnalysis from '../views/ssq/TrendAnalysis.vue'
|
||||
import SurfaceAnalysis from '../views/ssq/SurfaceAnalysis.vue'
|
||||
import LineAnalysis from '../views/ssq/LineAnalysis.vue'
|
||||
import SsqTableAnalysis from '../views/ssq/SsqTableAnalysis.vue'
|
||||
import DataAnalysis from '../views/DataAnalysis.vue'
|
||||
import HelpCenter from '../views/HelpCenter.vue'
|
||||
import AboutUs from '../views/AboutUs.vue'
|
||||
import UserAgreement from '../views/UserAgreement.vue'
|
||||
import UserGuide from '../views/UserGuide.vue'
|
||||
import MemberAgreement from '../views/MemberAgreement.vue'
|
||||
import HitAnalysis from '../views/ssq/HitAnalysis.vue'
|
||||
import DltHitAnalysis from '../views/dlt/HitAnalysis.vue'
|
||||
import UsageStats from '../views/ssq/UsageStats.vue'
|
||||
import DltUsageStats from '../views/dlt/UsageStats.vue'
|
||||
import PrizeStatistics from '../views/ssq/PrizeStatistics.vue'
|
||||
import DltPrizeStatistics from '../views/dlt/PrizeStatistics.vue'
|
||||
|
||||
// 双色球相关页面
|
||||
import SsqLottery from '../views/ssq/Lottery.vue'
|
||||
|
||||
// 大乐透相关页面
|
||||
import DltLottery from '../views/dlt/Lottery.vue'
|
||||
import DltTableAnalysis from '../views/dlt/DltTableAnalysis.vue'
|
||||
import DltSurfaceAnalysis from '../views/dlt/SurfaceAnalysis.vue'
|
||||
import DltLineAnalysis from '../views/dlt/LineAnalysis.vue'
|
||||
import DltTrendAnalysis from '../views/dlt/TrendAnalysis.vue'
|
||||
|
||||
// 精推版页面
|
||||
import JtSsqHome from '../views/jt/SsqHome.vue'
|
||||
import JtDltHome from '../views/jt/DltHome.vue'
|
||||
|
||||
|
||||
// 后台管理相关组件
|
||||
import AdminLogin from '../views/admin/AdminLogin.vue'
|
||||
import AdminLayout from '../views/admin/layout/AdminLayout.vue'
|
||||
import AdminVipCodeManagement from '../views/admin/VipCodeManagement.vue'
|
||||
import AdminExcelImportManagement from '../views/admin/ExcelImportManagement.vue'
|
||||
import AdminDltExcelImportManagement from '../views/admin/DltExcelImportManagement.vue'
|
||||
import AdminPredictionManagement from '../views/admin/PredictionManagement.vue'
|
||||
import AdminDltPredictionManagement from '../views/admin/DltPredictionManagement.vue'
|
||||
import AdminPrizeStatistics from '../views/admin/PrizeStatistics.vue'
|
||||
import AdminDltPrizeStatistics from '../views/admin/DltPrizeStatistics.vue'
|
||||
import AdminUsageStats from '../views/ssq/UsageStats.vue'
|
||||
import AdminDltUsageStats from '../views/dlt/UsageStats.vue'
|
||||
|
||||
const routes = [
|
||||
// 前台用户路由
|
||||
{
|
||||
path: '/',
|
||||
name: 'LotterySelection',
|
||||
component: LotterySelection
|
||||
},
|
||||
{
|
||||
path: '/lottery-premium',
|
||||
name: 'LotteryPremium',
|
||||
component: LotteryPremium
|
||||
},
|
||||
{
|
||||
path: '/shuangseqiu',
|
||||
name: 'Shuangseqiu',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/daletou',
|
||||
name: 'DaLeTou',
|
||||
component: DltHome
|
||||
},
|
||||
{
|
||||
path: '/jt/shuangseqiu',
|
||||
name: 'JtShuangseqiu',
|
||||
component: JtSsqHome
|
||||
},
|
||||
{
|
||||
path: '/jt/daletou',
|
||||
name: 'JtDaLeTou',
|
||||
component: JtDltHome
|
||||
},
|
||||
{
|
||||
path: '/lottery-info',
|
||||
name: 'LotteryInfo',
|
||||
component: LotteryInfo
|
||||
},
|
||||
{
|
||||
path: '/lottery-info/ssq',
|
||||
name: 'SsqLottery',
|
||||
component: SsqLottery
|
||||
},
|
||||
{
|
||||
path: '/lottery-info/dlt',
|
||||
name: 'DltLottery',
|
||||
component: DltLottery
|
||||
},
|
||||
{
|
||||
path: '/dlt-table-analysis',
|
||||
name: 'DltTableAnalysis',
|
||||
component: DltTableAnalysis,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/dlt-surface-analysis',
|
||||
name: 'DltSurfaceAnalysis',
|
||||
component: DltSurfaceAnalysis,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/dlt-line-analysis',
|
||||
name: 'DltLineAnalysis',
|
||||
component: DltLineAnalysis,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/dlt-trend-analysis',
|
||||
name: 'DltTrendAnalysis',
|
||||
component: DltTrendAnalysis,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'Profile',
|
||||
component: Profile
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/reset-password',
|
||||
name: 'ResetPassword',
|
||||
component: ResetPassword
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: Register
|
||||
},
|
||||
{
|
||||
path: '/predict-records',
|
||||
name: 'PredictRecords',
|
||||
component: PredictRecords
|
||||
},
|
||||
{
|
||||
path: '/dlt/predict-records',
|
||||
name: 'DltPredictRecords',
|
||||
component: DltPredictRecords,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
|
||||
{
|
||||
path: '/excel-import',
|
||||
name: 'ExcelImportManagement',
|
||||
component: ExcelImportManagement
|
||||
},
|
||||
{
|
||||
path: '/exchange-records',
|
||||
name: 'ExchangeRecords',
|
||||
component: ExchangeRecords
|
||||
},
|
||||
{
|
||||
path: '/trend-analysis',
|
||||
name: 'TrendAnalysis',
|
||||
component: TrendAnalysis
|
||||
},
|
||||
{
|
||||
path: '/surface-analysis',
|
||||
name: 'SurfaceAnalysis',
|
||||
component: SurfaceAnalysis
|
||||
},
|
||||
{
|
||||
path: '/line-analysis',
|
||||
name: 'LineAnalysis',
|
||||
component: LineAnalysis,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/data-analysis',
|
||||
name: 'DataAnalysis',
|
||||
component: DataAnalysis,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/hit-analysis',
|
||||
name: 'HitAnalysis',
|
||||
component: HitAnalysis,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/daletou/hit-analysis',
|
||||
name: 'DltHitAnalysis',
|
||||
component: DltHitAnalysis,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/usage-stats',
|
||||
name: 'UsageStats',
|
||||
component: UsageStats,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/daletou/usage-stats',
|
||||
name: 'DltUsageStats',
|
||||
component: DltUsageStats,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/prize-statistics',
|
||||
name: 'PrizeStatistics',
|
||||
component: PrizeStatistics,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/daletou/prize-statistics',
|
||||
name: 'DltPrizeStatistics',
|
||||
component: DltPrizeStatistics,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/help-center',
|
||||
name: 'HelpCenter',
|
||||
component: HelpCenter,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/about-us',
|
||||
name: 'AboutUs',
|
||||
component: AboutUs,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/user-agreement',
|
||||
name: 'UserAgreement',
|
||||
component: UserAgreement,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/user-guide',
|
||||
name: 'UserGuide',
|
||||
component: UserGuide,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/member-agreement',
|
||||
name: 'MemberAgreement',
|
||||
component: MemberAgreement,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/table-analysis',
|
||||
name: 'SsqTableAnalysis',
|
||||
component: SsqTableAnalysis
|
||||
},
|
||||
|
||||
|
||||
// 后台管理路由 - 完全隔离
|
||||
{
|
||||
path: '/cpzsadmin/login',
|
||||
name: 'AdminLogin',
|
||||
component: AdminLogin,
|
||||
meta: {
|
||||
title: '后台登录',
|
||||
requiresAuth: false,
|
||||
isAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/cpzsadmin',
|
||||
component: AdminLayout,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: '/cpzsadmin/dashboard'
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'AdminDashboard',
|
||||
component: () => import('../views/admin/Dashboard.vue'),
|
||||
meta: {
|
||||
title: '控制面板',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vip-code',
|
||||
name: 'AdminVipCodeManagement',
|
||||
component: AdminVipCodeManagement,
|
||||
meta: {
|
||||
title: '会员码管理',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'excel-import',
|
||||
meta: {
|
||||
title: '数据导入',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'AdminExcelImportManagement',
|
||||
component: AdminExcelImportManagement,
|
||||
meta: {
|
||||
title: '数据导入',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'ssq',
|
||||
name: 'AdminExcelImportSSQ',
|
||||
component: AdminExcelImportManagement,
|
||||
meta: {
|
||||
title: '双色球数据导入',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dlt',
|
||||
name: 'AdminExcelImportDLT',
|
||||
component: AdminDltExcelImportManagement,
|
||||
meta: {
|
||||
title: '大乐透数据导入',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'prediction',
|
||||
meta: {
|
||||
title: '推测管理',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'ssq',
|
||||
name: 'AdminPredictionSSQ',
|
||||
component: AdminPredictionManagement,
|
||||
meta: {
|
||||
title: '双色球推测管理',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dlt',
|
||||
name: 'AdminPredictionDLT',
|
||||
component: AdminDltPredictionManagement,
|
||||
meta: {
|
||||
title: '大乐透推测管理',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'prize-statistics',
|
||||
meta: {
|
||||
title: '奖金统计',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'ssq',
|
||||
name: 'AdminPrizeStatisticsSSQ',
|
||||
component: AdminPrizeStatistics,
|
||||
meta: {
|
||||
title: '双色球奖金统计',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dlt',
|
||||
name: 'AdminPrizeStatisticsDLT',
|
||||
component: AdminDltPrizeStatistics,
|
||||
meta: {
|
||||
title: '大乐透奖金统计',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'usage-stats',
|
||||
meta: {
|
||||
title: '使用统计',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'ssq',
|
||||
name: 'AdminUsageStatsSSQ',
|
||||
component: AdminUsageStats,
|
||||
meta: {
|
||||
title: '双色球使用统计',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dlt',
|
||||
name: 'AdminUsageStatsDLT',
|
||||
component: AdminDltUsageStats,
|
||||
meta: {
|
||||
title: '大乐透使用统计',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'user-list',
|
||||
name: 'AdminUserList',
|
||||
component: () => import('../views/admin/UserList.vue'),
|
||||
meta: {
|
||||
title: '用户列表',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'operation-history',
|
||||
name: 'AdminOperationHistory',
|
||||
component: () => import('../views/admin/OperationHistory.vue'),
|
||||
meta: {
|
||||
title: '操作历史',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'announcement',
|
||||
name: 'AdminAnnouncementManagement',
|
||||
component: () => import('../views/admin/AnnouncementManagement.vue'),
|
||||
meta: {
|
||||
title: '公告管理',
|
||||
requiresAuth: true,
|
||||
isAdmin: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 404 页面
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('../views/NotFound.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫 - 权限控制
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 设置页面标题
|
||||
if (to.meta.title) {
|
||||
document.title = to.meta.title + ' - 彩票推测系统'
|
||||
}
|
||||
|
||||
// 后台管理路由权限检查
|
||||
if (to.meta.isAdmin) {
|
||||
// 如果是登录页面,直接放行
|
||||
if (to.name === 'AdminLogin') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 从store中导入userStore
|
||||
import('../store/user.js').then(({ userStore }) => {
|
||||
// 检查是否已登录(使用session存储)
|
||||
if (!userStore.isAdminLoggedIn()) {
|
||||
ElMessage.error('请先登录后台管理系统')
|
||||
next('/cpzsadmin/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是管理员或VIP用户
|
||||
const adminInfo = JSON.parse(sessionStorage.getItem('adminInfo') || '{}')
|
||||
if (adminInfo.userRole === 'user') {
|
||||
ElMessage.error('您没有权限访问后台管理系统')
|
||||
next('/cpzsadmin/login')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}).catch(error => {
|
||||
console.error('加载用户状态出错:', error)
|
||||
next('/cpzsadmin/login')
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
283
src/store/user.js
Normal file
@@ -0,0 +1,283 @@
|
||||
import { reactive } from 'vue'
|
||||
import { lotteryApi } from '../api/index.js'
|
||||
|
||||
// 用户状态管理
|
||||
export const userStore = reactive({
|
||||
// 用户信息
|
||||
user: null,
|
||||
isLoggedIn: false,
|
||||
isKickedOut: false, // 标记是否被其他设备踢出
|
||||
lastCheckTime: null, // 最后一次检查登录状态的时间
|
||||
|
||||
// 获取登录用户信息
|
||||
async fetchLoginUser() {
|
||||
try {
|
||||
const response = await lotteryApi.getLoginUser()
|
||||
if (response.success === true) {
|
||||
const userData = response.data
|
||||
// 更新用户信息,保留现有的本地数据结构
|
||||
this.user = {
|
||||
id: String(userData.id), // 确保ID始终为字符串,避免精度丢失
|
||||
username: userData.userName || userData.userAccount || userData.username || userData.name,
|
||||
email: userData.email,
|
||||
phone: userData.phone,
|
||||
nickname: userData.nickname || userData.userName || userData.userAccount || userData.username || userData.name,
|
||||
avatar: userData.userAvatar || userData.avatar || null,
|
||||
userType: userData.userType || 'trial',
|
||||
isVip: userData.isVip,
|
||||
expireDate: userData.vipExpire || userData.expireDate || '2025-06-30',
|
||||
registeredAt: userData.createTime || userData.registeredAt || new Date().toISOString(),
|
||||
status: userData.status !== undefined ? userData.status : 0, // 添加status字段,默认为0 (正常)
|
||||
stats: {
|
||||
predictCount: userData.stats?.predictCount || 0,
|
||||
hitCount: userData.stats?.hitCount || 0,
|
||||
hitRate: userData.stats?.hitRate || 0
|
||||
}
|
||||
}
|
||||
this.isLoggedIn = true
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('user', JSON.stringify(this.user))
|
||||
localStorage.setItem('isLoggedIn', 'true')
|
||||
|
||||
return this.user
|
||||
} else {
|
||||
console.error('获取用户信息失败:', response.message)
|
||||
this.logout()
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息出错:', error)
|
||||
this.logout()
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// 登录
|
||||
login(userInfo) {
|
||||
this.user = {
|
||||
id: userInfo.id || Date.now(),
|
||||
username: userInfo.username,
|
||||
email: userInfo.email,
|
||||
phone: userInfo.phone,
|
||||
nickname: userInfo.nickname || userInfo.username,
|
||||
avatar: userInfo.avatar || null,
|
||||
userType: userInfo.userType || 'trial', // trial: 体验版, premium: 正式版
|
||||
expireDate: userInfo.expireDate || '2025-06-30',
|
||||
registeredAt: userInfo.registeredAt || new Date().toISOString(),
|
||||
stats: {
|
||||
predictCount: userInfo.stats?.predictCount || 0,
|
||||
hitCount: userInfo.stats?.hitCount || 0,
|
||||
hitRate: userInfo.stats?.hitRate || 0
|
||||
}
|
||||
}
|
||||
this.isLoggedIn = true
|
||||
this.isKickedOut = false // 重置被踢出状态
|
||||
this.lastCheckTime = Date.now() // 记录登录时间
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('user', JSON.stringify(this.user))
|
||||
localStorage.setItem('isLoggedIn', 'true')
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout(isKickedOut = false) {
|
||||
// 如果是被踢出,标记状态
|
||||
if (isKickedOut) {
|
||||
this.isKickedOut = true
|
||||
console.log('[安全] 账号在其他设备登录,当前会话已被踢出')
|
||||
}
|
||||
|
||||
this.user = null;
|
||||
this.isLoggedIn = false;
|
||||
this.lastCheckTime = null;
|
||||
|
||||
// 清除所有本地存储的用户信息
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('userInfo');
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
|
||||
// 清除可能存在的其他相关存储
|
||||
sessionStorage.removeItem('user');
|
||||
sessionStorage.removeItem('userInfo');
|
||||
sessionStorage.removeItem('isLoggedIn');
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
updateUser(updates) {
|
||||
if (this.user) {
|
||||
Object.assign(this.user, updates)
|
||||
localStorage.setItem('user', JSON.stringify(this.user))
|
||||
}
|
||||
},
|
||||
|
||||
// 从本地存储恢复用户状态
|
||||
restoreFromStorage() {
|
||||
const savedUser = localStorage.getItem('user')
|
||||
const savedLoginState = localStorage.getItem('isLoggedIn')
|
||||
|
||||
if (savedUser && savedLoginState === 'true') {
|
||||
const user = JSON.parse(savedUser)
|
||||
// 确保ID始终为字符串,避免精度丢失
|
||||
if (user.id) {
|
||||
user.id = String(user.id)
|
||||
}
|
||||
this.user = user
|
||||
this.isLoggedIn = true
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户统计
|
||||
updateStats(stats) {
|
||||
if (this.user) {
|
||||
this.user.stats = { ...this.user.stats, ...stats }
|
||||
localStorage.setItem('user', JSON.stringify(this.user))
|
||||
}
|
||||
},
|
||||
|
||||
// 检查用户是否为付费用户
|
||||
isPremiumUser() {
|
||||
return this.user?.userType === 'premium'
|
||||
},
|
||||
|
||||
// 获取用户ID
|
||||
getUserId() {
|
||||
return this.user?.id
|
||||
},
|
||||
|
||||
// 设置用户信息(用于管理员登录)
|
||||
setUserInfo(userInfo) {
|
||||
this.user = {
|
||||
id: String(userInfo.id), // 确保ID始终为字符串,避免精度丢失
|
||||
username: userInfo.userAccount || userInfo.userName,
|
||||
nickname: userInfo.userName || userInfo.userAccount,
|
||||
avatar: userInfo.avatar || null,
|
||||
userRole: userInfo.userRole || 'user',
|
||||
status: userInfo.status !== undefined ? userInfo.status : 0, // 添加status字段,默认为0 (正常)
|
||||
createTime: userInfo.createTime || new Date().toISOString()
|
||||
}
|
||||
this.isLoggedIn = true
|
||||
|
||||
// 使用sessionStorage而不是localStorage存储管理员登录状态
|
||||
sessionStorage.setItem('adminInfo', JSON.stringify(this.user))
|
||||
sessionStorage.setItem('adminLoggedIn', 'true')
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo() {
|
||||
// 如果内存中有用户信息,直接返回
|
||||
if (this.user) {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
// 尝试从sessionStorage中获取管理员信息
|
||||
const adminInfo = sessionStorage.getItem('adminInfo');
|
||||
if (adminInfo) {
|
||||
try {
|
||||
const parsedAdminInfo = JSON.parse(adminInfo);
|
||||
// 将解析后的信息赋值给this.user,确保内存中也有
|
||||
this.user = parsedAdminInfo;
|
||||
return parsedAdminInfo;
|
||||
} catch (e) {
|
||||
console.error('解析管理员信息失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从localStorage中获取普通用户信息
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
if (userInfo) {
|
||||
try {
|
||||
return JSON.parse(userInfo);
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都没有,返回空对象
|
||||
return {};
|
||||
},
|
||||
|
||||
// 检查管理员是否已登录
|
||||
isAdminLoggedIn() {
|
||||
return sessionStorage.getItem('adminLoggedIn') === 'true'
|
||||
},
|
||||
|
||||
// 管理员登出
|
||||
adminLogout(isKickedOut = false) {
|
||||
// 如果是被踢出,标记状态
|
||||
if (isKickedOut) {
|
||||
this.isKickedOut = true
|
||||
console.log('[安全] 管理员账号在其他设备登录,当前会话已被踢出')
|
||||
}
|
||||
|
||||
this.user = null;
|
||||
this.isLoggedIn = false;
|
||||
this.lastCheckTime = null;
|
||||
|
||||
// 清除所有session存储的管理员信息
|
||||
sessionStorage.removeItem('adminInfo');
|
||||
sessionStorage.removeItem('adminLoggedIn');
|
||||
},
|
||||
|
||||
// 主动检查登录状态是否有效
|
||||
async checkLoginStatus() {
|
||||
// 如果没有登录,直接返回
|
||||
if (!this.isLoggedIn) {
|
||||
return { valid: false, reason: 'not_logged_in' }
|
||||
}
|
||||
|
||||
// 避免频繁检查(10秒内只检查一次)
|
||||
const now = Date.now()
|
||||
if (this.lastCheckTime && (now - this.lastCheckTime) < 10000) {
|
||||
return { valid: true, reason: 'recently_checked' }
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用后端接口验证登录状态
|
||||
const response = await lotteryApi.getLoginUser()
|
||||
|
||||
if (response.success === true) {
|
||||
// 登录状态有效,更新检查时间
|
||||
this.lastCheckTime = now
|
||||
this.isKickedOut = false
|
||||
return { valid: true, reason: 'verified' }
|
||||
} else {
|
||||
// 检查是否是被踢出
|
||||
const message = response.message || ''
|
||||
if (message.includes('其他设备登录') || message.includes('当前会话已失效')) {
|
||||
this.logout(true) // 标记为被踢出
|
||||
return { valid: false, reason: 'kicked_out', message }
|
||||
} else {
|
||||
this.logout(false)
|
||||
return { valid: false, reason: 'session_expired', message }
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Store] 检查登录状态失败:', error)
|
||||
// 网络错误等情况,不清除登录状态,让用户继续使用
|
||||
return { valid: true, reason: 'check_failed', error }
|
||||
}
|
||||
},
|
||||
|
||||
// 重置被踢出状态(用于重新登录后)
|
||||
resetKickedOutStatus() {
|
||||
this.isKickedOut = false
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化时从本地存储恢复状态
|
||||
// 检查是否有管理员登录信息,优先恢复管理员状态
|
||||
const adminInfo = sessionStorage.getItem('adminInfo');
|
||||
if (adminInfo) {
|
||||
try {
|
||||
userStore.user = JSON.parse(adminInfo);
|
||||
userStore.isLoggedIn = true;
|
||||
} catch (e) {
|
||||
console.error('恢复管理员状态失败:', e);
|
||||
// 如果管理员信息恢复失败,尝试恢复普通用户状态
|
||||
userStore.restoreFromStorage();
|
||||
}
|
||||
} else {
|
||||
// 没有管理员信息,尝试恢复普通用户状态
|
||||
userStore.restoreFromStorage();
|
||||
}
|
||||
258
src/styles/global.css
Normal file
@@ -0,0 +1,258 @@
|
||||
/* 全局样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 强制设置根元素背景 */
|
||||
:root {
|
||||
background: #f0f2f5 !important;
|
||||
}
|
||||
|
||||
html {
|
||||
background: #f0f2f5 !important;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
background: #f0f2f5 !important;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 后台管理系统样式 */
|
||||
.admin-layout,
|
||||
.admin-login {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
overflow: hidden !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* 后台管理系统覆盖全局背景 */
|
||||
body.admin-body {
|
||||
background: #f0f2f5 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 页面头部样式 */
|
||||
.page-header {
|
||||
text-align: center;
|
||||
padding: 60px 20px 30px;
|
||||
background: linear-gradient(135deg, #e53e3e 0%, #ff6b6b 100%);
|
||||
color: white;
|
||||
margin-bottom: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header::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="pattern" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse"><circle cx="20" cy="20" r="1.5" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23pattern)"/></svg>');
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(229, 62, 62, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #e9ecef;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* 通用模态框样式 */
|
||||
.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;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
margin-bottom: 20px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
.container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 主页特殊容器 */
|
||||
.home-container {
|
||||
background: #f0f2f5;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 各页面容器 */
|
||||
.lottery-container,
|
||||
.profile-container,
|
||||
.login-page-container,
|
||||
.register-page-container {
|
||||
background: #f0f2f5;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
padding: 20px 15px 15px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 25px 20px;
|
||||
}
|
||||
|
||||
.lottery-container,
|
||||
.profile-container,
|
||||
.login-page-container,
|
||||
.register-page-container {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 错误提示样式 */
|
||||
.error-message {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 4px solid #e53e3e;
|
||||
}
|
||||
|
||||
/* 成功提示样式 */
|
||||
.success-message {
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
/* 隐藏子页面顶部背景图的浮动元素 */
|
||||
.page-header-modern::before {
|
||||
display: none !important;
|
||||
}
|
||||
797
src/views/AboutUs.vue
Normal file
763
src/views/DataAnalysis.vue
Normal file
@@ -0,0 +1,763 @@
|
||||
<template>
|
||||
<div class="data-analysis-container">
|
||||
|
||||
|
||||
<!-- 彩票种类选择区域 -->
|
||||
<el-card class="lottery-types" shadow="never">
|
||||
<h2 class="section-title">选择彩票种类</h2>
|
||||
|
||||
<div class="lottery-options">
|
||||
<!-- 双色球 -->
|
||||
<div
|
||||
class="lottery-option"
|
||||
:class="{ active: currentLotteryType === 'ssq' }"
|
||||
@click="switchLotteryType('ssq')"
|
||||
>
|
||||
<div class="lottery-option-image">
|
||||
<img :src="currentLotteryType === 'ssq' ? '/assets/fenxi/ssq-1.svg' : '/assets/fenxi/ssq-0.svg'" alt="双色球" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 快乐8 -->
|
||||
<div
|
||||
class="lottery-option"
|
||||
:class="{ active: currentLotteryType === 'kl8' }"
|
||||
@click="switchLotteryType('kl8')"
|
||||
>
|
||||
<div class="lottery-option-image">
|
||||
<img :src="currentLotteryType === 'kl8' ? '/assets/fenxi/kuaile8-1.svg' : '/assets/fenxi/kuaile-0.svg'" alt="快乐8" />
|
||||
<div class="coming-soon-badge">即将开放</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 七乐彩 -->
|
||||
<div
|
||||
class="lottery-option"
|
||||
:class="{ active: currentLotteryType === 'qlc' }"
|
||||
@click="switchLotteryType('qlc')"
|
||||
>
|
||||
<div class="lottery-option-image">
|
||||
<img :src="currentLotteryType === 'qlc' ? '/assets/fenxi/7lecai-1.svg' : '/assets/fenxi/7lecai-0.svg'" alt="七乐彩" />
|
||||
<div class="coming-soon-badge">即将开放</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 福彩3D -->
|
||||
<div
|
||||
class="lottery-option"
|
||||
:class="{ active: currentLotteryType === '3d' }"
|
||||
@click="switchLotteryType('3d')"
|
||||
>
|
||||
<div class="lottery-option-image">
|
||||
<img :src="currentLotteryType === '3d' ? '/assets/fenxi/3D-1.svg' : '/assets/fenxi/3D-0.svg'" alt="福彩3D" />
|
||||
<div class="coming-soon-badge">即将开放</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 大乐透 -->
|
||||
<div
|
||||
class="lottery-option"
|
||||
:class="{ active: currentLotteryType === 'dlt' }"
|
||||
@click="switchLotteryType('dlt')"
|
||||
>
|
||||
<div class="lottery-option-image">
|
||||
<img :src="currentLotteryType === 'dlt' ? '/assets/fenxi/daletou-1.svg' : '/assets/fenxi/daletou-0.svg'" alt="大乐透" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 排列3 -->
|
||||
<div
|
||||
class="lottery-option"
|
||||
:class="{ active: currentLotteryType === 'pl3' }"
|
||||
@click="switchLotteryType('pl3')"
|
||||
>
|
||||
<div class="lottery-option-image">
|
||||
<img :src="currentLotteryType === 'pl3' ? '/assets/fenxi/pailie3-1.svg' : '/assets/fenxi/pailie3-0.svg'" alt="排列3" />
|
||||
<div class="coming-soon-badge">即将开放</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 七星彩 -->
|
||||
<div
|
||||
class="lottery-option"
|
||||
:class="{ active: currentLotteryType === 'qxc' }"
|
||||
@click="switchLotteryType('qxc')"
|
||||
>
|
||||
<div class="lottery-option-image">
|
||||
<img :src="currentLotteryType === 'qxc' ? '/assets/fenxi/7xingcai-1.svg' : '/assets/fenxi/7xingcai-0.svg'" alt="七星彩" />
|
||||
<div class="coming-soon-badge">即将开放</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 排列5 -->
|
||||
<div
|
||||
class="lottery-option"
|
||||
:class="{ active: currentLotteryType === 'pl5' }"
|
||||
@click="switchLotteryType('pl5')"
|
||||
>
|
||||
<div class="lottery-option-image">
|
||||
<img :src="currentLotteryType === 'pl5' ? '/assets/fenxi/pailie-1.svg' : '/assets/fenxi/pailie5.svg'" alt="排列5" />
|
||||
<div class="coming-soon-badge">即将开放</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 分析类型选择 - 只有在选择彩票种类后才显示 -->
|
||||
<div v-if="currentLotteryType" class="analysis-section">
|
||||
<div class="analysis-options">
|
||||
<!-- 活跃性分析 -->
|
||||
<div class="analysis-option" @click="goToAnalysis('trend')">
|
||||
<div class="analysis-icon">
|
||||
<img src="/assets/fenxi/fenxi-3.svg" alt="活跃性分析" class="analysis-svg-icon" />
|
||||
</div>
|
||||
<div class="analysis-name">活跃性分析</div>
|
||||
<div class="analysis-desc">分析号码的活跃度和热度趋势</div>
|
||||
</div>
|
||||
|
||||
<!-- 组合性分析 -->
|
||||
<div class="analysis-option" @click="goToAnalysis('surface')">
|
||||
<div class="analysis-icon">
|
||||
<img src="/assets/fenxi/fenxi-4.svg" alt="组合性分析" class="analysis-svg-icon" />
|
||||
</div>
|
||||
<div class="analysis-name">组合性分析</div>
|
||||
<div class="analysis-desc">分析号码之间的组合关系和规律</div>
|
||||
</div>
|
||||
|
||||
<!-- 接续性分析 -->
|
||||
<div class="analysis-option" @click="goToAnalysis('line')">
|
||||
<div class="analysis-icon">
|
||||
<img src="/assets/fenxi/fenxi-2.svg" alt="接续性分析" class="analysis-svg-icon" />
|
||||
</div>
|
||||
<div class="analysis-name">接续性分析</div>
|
||||
<div class="analysis-desc">分析号码的连续性和接续规律</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 开发中提示 -->
|
||||
<div v-if="showTip" class="tip-overlay" @click="hideTip">
|
||||
<div class="tip-content" @click.stop>
|
||||
<div class="tip-icon">🚧</div>
|
||||
<h3>功能开发中</h3>
|
||||
<p>该彩票种类的分析功能正在开发中,目前仅支持双色球分析,请稍后再试或选择双色球进行分析。</p>
|
||||
<button class="tip-button" @click="hideTip">我知道了</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElCard } from 'element-plus'
|
||||
|
||||
export default {
|
||||
name: 'DataAnalysis',
|
||||
components: {
|
||||
ElCard
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentLotteryType: 'ssq', // 默认选中双色球
|
||||
showTip: false, // 提示框显示状态
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 从路由查询参数中恢复彩票类型选择
|
||||
if (this.$route.query.lotteryType) {
|
||||
this.currentLotteryType = this.$route.query.lotteryType
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
// 切换彩票类型
|
||||
switchLotteryType(type) {
|
||||
// 双色球和大乐透功能可用,其他彩票类型显示开发中提示
|
||||
if (type !== 'ssq' && type !== 'dlt') {
|
||||
this.showDevelopingTip();
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentLotteryType = type;
|
||||
},
|
||||
|
||||
// 跳转到具体分析页面
|
||||
goToAnalysis(analysisType) {
|
||||
let routes = {};
|
||||
|
||||
if (this.currentLotteryType === 'ssq') {
|
||||
routes = {
|
||||
'table': '/table-analysis',
|
||||
'surface': '/surface-analysis',
|
||||
'trend': '/trend-analysis',
|
||||
'line': '/line-analysis'
|
||||
};
|
||||
} else if (this.currentLotteryType === 'dlt') {
|
||||
routes = {
|
||||
'table': '/dlt-table-analysis',
|
||||
'surface': '/dlt-surface-analysis',
|
||||
'trend': '/dlt-trend-analysis',
|
||||
'line': '/dlt-line-analysis'
|
||||
};
|
||||
}
|
||||
|
||||
if (routes[analysisType]) {
|
||||
this.$router.push({
|
||||
path: routes[analysisType],
|
||||
query: { lotteryType: this.currentLotteryType }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 显示开发中提示
|
||||
showDevelopingTip() {
|
||||
this.showTip = true;
|
||||
},
|
||||
|
||||
// 隐藏提示
|
||||
hideTip() {
|
||||
this.showTip = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-analysis-container {
|
||||
padding: 20px 20px 0px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.lottery-types {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.analysis-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 去掉Element Plus卡片的默认内边距 */
|
||||
.lottery-types :deep(.el-card__body) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 彩票类型选择区域 */
|
||||
.section-title {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
margin: 0 0 15px 0;
|
||||
padding: 15px 0 0 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lottery-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
justify-items: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0 10px 20px;
|
||||
}
|
||||
|
||||
.lottery-option {
|
||||
width: auto;
|
||||
max-width: 85px;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lottery-option:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.lottery-option-image {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lottery-option-image img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 即将开放标识 */
|
||||
.coming-soon-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -8px;
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8787);
|
||||
color: white;
|
||||
font-size: 8px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 6px rgba(255, 107, 107, 0.3);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 分析选项样式 */
|
||||
.analysis-options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.analysis-option {
|
||||
border-radius: 12px;
|
||||
padding: 40px 20px;
|
||||
min-height: 150px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
border: none;
|
||||
color: white;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 活跃性分析 - 蓝青色渐变 */
|
||||
.analysis-option:nth-child(1) {
|
||||
background: linear-gradient(135deg, rgba(74, 220, 221, 1), rgba(41, 144, 240, 1));
|
||||
}
|
||||
|
||||
/* 组合性分析 - 紫色渐变 */
|
||||
.analysis-option:nth-child(2) {
|
||||
background: linear-gradient(135deg, rgba(190, 131, 253, 1), rgba(122, 93, 249, 1));
|
||||
}
|
||||
|
||||
/* 接续性分析 - 黄色渐变 */
|
||||
.analysis-option:nth-child(3) {
|
||||
background: linear-gradient(135deg, rgba(251, 214, 45, 1), rgba(255, 176, 3, 1));
|
||||
}
|
||||
|
||||
|
||||
|
||||
.analysis-option > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.analysis-option:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 第一个卡片的图标在左侧 */
|
||||
.analysis-option:nth-child(1) .analysis-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0px;
|
||||
transform: translateY(-50%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 后两个卡片的图标在右侧 */
|
||||
.analysis-option:nth-child(2) .analysis-icon,
|
||||
.analysis-option:nth-child(3) .analysis-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0px;
|
||||
transform: translateY(-50%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.analysis-svg-icon {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
object-fit: contain;
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.5;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 第一个卡片图案扩大140% */
|
||||
.analysis-option:nth-child(1) .analysis-svg-icon {
|
||||
width: 432px;
|
||||
height: 432px;
|
||||
}
|
||||
|
||||
/* 第二个卡片图案缩小10% */
|
||||
.analysis-option:nth-child(2) .analysis-svg-icon {
|
||||
width: 162px;
|
||||
height: 162px;
|
||||
}
|
||||
|
||||
/* 第三个卡片图案扩大140% */
|
||||
.analysis-option:nth-child(3) .analysis-svg-icon {
|
||||
width: 432px;
|
||||
height: 432px;
|
||||
}
|
||||
|
||||
/* 第一个卡片的文字样式(图标在左侧) */
|
||||
.analysis-option:nth-child(1) .analysis-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin-bottom: 10px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.analysis-option:nth-child(1) .analysis-desc {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
line-height: 1.5;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 后两个卡片的文字样式(图标在右侧) */
|
||||
.analysis-option:nth-child(2) .analysis-name,
|
||||
.analysis-option:nth-child(3) .analysis-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin-bottom: 10px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.analysis-option:nth-child(2) .analysis-desc,
|
||||
.analysis-option:nth-child(3) .analysis-desc {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
line-height: 1.5;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 开发中提示框 */
|
||||
.tip-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tip-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
box-shadow: 0 16px 48px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tip-content h3 {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin: 0 0 12px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tip-content p {
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.tip-button {
|
||||
background: linear-gradient(135deg, #ff6b35, #f7931e);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 24px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tip-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
.tip-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.data-analysis-container {
|
||||
padding: 10px 10px 0px 10px;
|
||||
}
|
||||
|
||||
.lottery-options {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
padding: 0 10px 20px;
|
||||
}
|
||||
|
||||
.lottery-option {
|
||||
max-width: 75px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.lottery-option-image {
|
||||
width: 63px;
|
||||
height: 63px;
|
||||
}
|
||||
|
||||
.coming-soon-badge {
|
||||
font-size: 7px;
|
||||
padding: 1px 3px;
|
||||
top: -3px;
|
||||
right: -6px;
|
||||
}
|
||||
|
||||
.analysis-options {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.analysis-option {
|
||||
padding: 35px 15px;
|
||||
min-height: 130px;
|
||||
}
|
||||
|
||||
.analysis-svg-icon {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 第一个卡片图案扩大140% */
|
||||
.analysis-option:nth-child(1) .analysis-svg-icon {
|
||||
width: 336px;
|
||||
height: 336px;
|
||||
}
|
||||
|
||||
/* 第二个卡片图案缩小10% */
|
||||
.analysis-option:nth-child(2) .analysis-svg-icon {
|
||||
width: 126px;
|
||||
height: 126px;
|
||||
}
|
||||
|
||||
/* 第三个卡片图案扩大140% */
|
||||
.analysis-option:nth-child(3) .analysis-svg-icon {
|
||||
width: 336px;
|
||||
height: 336px;
|
||||
}
|
||||
|
||||
/* 第一个卡片的图标在左侧 */
|
||||
.analysis-option:nth-child(1) .analysis-icon {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
/* 后两个卡片的图标在右侧 */
|
||||
.analysis-option:nth-child(2) .analysis-icon,
|
||||
.analysis-option:nth-child(3) .analysis-icon {
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
/* 所有卡片的文字统一对齐 */
|
||||
.analysis-option:nth-child(1) .analysis-name,
|
||||
.analysis-option:nth-child(1) .analysis-desc,
|
||||
.analysis-option:nth-child(2) .analysis-name,
|
||||
.analysis-option:nth-child(2) .analysis-desc,
|
||||
.analysis-option:nth-child(3) .analysis-name,
|
||||
.analysis-option:nth-child(3) .analysis-desc {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.lottery-options {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
padding: 0 10px 20px;
|
||||
}
|
||||
|
||||
.lottery-option {
|
||||
max-width: 63px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.lottery-option-image {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.coming-soon-badge {
|
||||
font-size: 6px;
|
||||
padding: 1px 2px;
|
||||
top: -2px;
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
.analysis-options {
|
||||
padding: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
.analysis-option {
|
||||
padding: 30px 12px;
|
||||
min-height: 110px;
|
||||
}
|
||||
|
||||
.analysis-svg-icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 第一个卡片图案扩大140% */
|
||||
.analysis-option:nth-child(1) .analysis-svg-icon {
|
||||
width: 288px;
|
||||
height: 288px;
|
||||
}
|
||||
|
||||
/* 第二个卡片图案缩小10% */
|
||||
.analysis-option:nth-child(2) .analysis-svg-icon {
|
||||
width: 108px;
|
||||
height: 108px;
|
||||
}
|
||||
|
||||
/* 第三个卡片图案扩大140% */
|
||||
.analysis-option:nth-child(3) .analysis-svg-icon {
|
||||
width: 288px;
|
||||
height: 288px;
|
||||
}
|
||||
|
||||
/* 第一个卡片的图标在左侧 */
|
||||
.analysis-option:nth-child(1) .analysis-icon {
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
/* 后两个卡片的图标在右侧 */
|
||||
.analysis-option:nth-child(2) .analysis-icon,
|
||||
.analysis-option:nth-child(3) .analysis-icon {
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
/* 所有卡片的文字统一对齐 */
|
||||
.analysis-option:nth-child(1) .analysis-name,
|
||||
.analysis-option:nth-child(2) .analysis-name,
|
||||
.analysis-option:nth-child(3) .analysis-name {
|
||||
font-size: 16px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.analysis-option:nth-child(1) .analysis-desc,
|
||||
.analysis-option:nth-child(2) .analysis-desc,
|
||||
.analysis-option:nth-child(3) .analysis-desc {
|
||||
font-size: 12px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 弹窗响应式样式 */
|
||||
.tip-overlay {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.tip-content {
|
||||
padding: 24px 20px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tip-content h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tip-content p {
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tip-button {
|
||||
padding: 8px 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
769
src/views/ExcelImportManagement.vue
Normal file
@@ -0,0 +1,769 @@
|
||||
<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>上传包含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: 'ExcelImportManagement',
|
||||
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('/')
|
||||
}, 3000)
|
||||
}
|
||||
} else {
|
||||
this.showError('获取用户信息失败,请重新登录')
|
||||
setTimeout(() => {
|
||||
this.$router.push('/login')
|
||||
}, 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('权限检查失败:', error)
|
||||
this.showError('权限验证失败,请重新登录')
|
||||
setTimeout(() => {
|
||||
this.$router.push('/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: 20px;
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.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: 30px;
|
||||
}
|
||||
|
||||
/* 权限检查样式 */
|
||||
.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 {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 功能区域 */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Element Plus 按钮样式已由组件提供 */
|
||||
|
||||
/* 减少卡片内边距 */
|
||||
:deep(.el-card__body) {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
/* 结果消息 */
|
||||
.result-message {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.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);
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
/* 信息说明 */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.import-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.function-area .el-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.function-area .el-col {
|
||||
flex: 0 0 33.333333%;
|
||||
max-width: 33.333333%;
|
||||
padding: 0 5px;
|
||||
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>
|
||||
924
src/views/ExchangeRecords.vue
Normal file
@@ -0,0 +1,924 @@
|
||||
<template>
|
||||
<div class="exchange-records-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header-modern">
|
||||
<div class="header-content">
|
||||
<button class="back-btn" @click="goBack">
|
||||
<svg viewBox="0 0 24 24" class="back-icon">
|
||||
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="header-info">
|
||||
<div class="header-text">
|
||||
<h1 class="header-title">兑换记录</h1>
|
||||
<p class="header-subtitle">查看您的会员兑换历史</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="records-container">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
<p class="loading-text">正在加载兑换记录...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-else-if="errorMessage" class="error-container">
|
||||
<div class="error-content">
|
||||
<div class="error-icon">❌</div>
|
||||
<h3 class="error-title">加载失败</h3>
|
||||
<p class="error-message">{{ errorMessage }}</p>
|
||||
<button class="retry-btn" @click="loadExchangeRecords">
|
||||
<span>重新加载</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else-if="records.length === 0" class="empty-container">
|
||||
<div class="empty-content">
|
||||
<div class="empty-icon">📋</div>
|
||||
<h3 class="empty-title">暂无兑换记录</h3>
|
||||
<p class="empty-message">您还没有任何兑换记录,快去兑换会员码吧</p>
|
||||
<button class="action-btn primary" @click="goToProfile">
|
||||
<span>去兑换会员码</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 兑换记录列表 -->
|
||||
<div v-else class="records-list">
|
||||
<div class="list-header">
|
||||
<span class="record-count">{{ records.length }}条记录</span>
|
||||
</div>
|
||||
|
||||
<div class="records-grid">
|
||||
<div
|
||||
v-for="record in paginatedRecords"
|
||||
:key="record.id"
|
||||
class="record-card-modern"
|
||||
>
|
||||
<div class="record-header">
|
||||
<div class="status-badge" :class="getStatusClass(record.isUse)">
|
||||
<!-- <span class="status-icon">{{ getStatusIcon(record.isUse) }}</span> -->
|
||||
<span class="status-text">{{ getStatusText(record.isUse) }}</span>
|
||||
</div>
|
||||
<div class="record-date">{{ formatDate(record.exchangeTime) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="record-body">
|
||||
<div class="record-info-grid">
|
||||
<div class="info-item">
|
||||
<label class="info-label">订单号</label>
|
||||
<span class="info-value">{{ record.orderNo || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label class="info-label">兑换类型</label>
|
||||
<span class="info-value type-value">{{ getExchangeTypeText(record.type) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label class="info-label">兑换模式</label>
|
||||
<span class="info-value">{{ getExchangeModeText(record.exchangeMode) }}</span>
|
||||
</div>
|
||||
<div v-if="record.orderAmount" class="info-item">
|
||||
<label class="info-label">订单金额</label>
|
||||
<span class="info-value amount-value">¥{{ record.orderAmount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端紧凑布局 -->
|
||||
<div class="record-info-compact">
|
||||
<div class="compact-left">
|
||||
<div class="compact-item">
|
||||
<span class="compact-label">订单号</span>
|
||||
<span class="compact-value">{{ record.orderNo || '-' }}</span>
|
||||
</div>
|
||||
<div class="compact-item">
|
||||
<span class="compact-label">兑换模式</span>
|
||||
<span class="compact-value">{{ getExchangeModeText(record.exchangeMode) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compact-right">
|
||||
<div class="compact-item">
|
||||
<span class="compact-label">兑换类型</span>
|
||||
<span class="compact-value type-value">{{ getExchangeTypeText(record.type) }}</span>
|
||||
</div>
|
||||
<div v-if="record.orderAmount" class="compact-item">
|
||||
<span class="compact-label">订单金额</span>
|
||||
<span class="compact-value amount-value">¥{{ record.orderAmount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<div v-if="totalPages > 1" class="pagination-container">
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:total="records.length"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
@current-change="goToPage"
|
||||
:pager-count="5"
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { lotteryApi } from '../api/index.js'
|
||||
import { userStore } from '../store/user.js'
|
||||
import {
|
||||
ElPageHeader,
|
||||
ElCard,
|
||||
ElButton,
|
||||
ElIcon,
|
||||
ElResult,
|
||||
ElEmpty,
|
||||
ElTag,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElPagination
|
||||
} from 'element-plus'
|
||||
import { ShoppingBag, Refresh } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'ExchangeRecords',
|
||||
components: {
|
||||
ElPageHeader,
|
||||
ElCard,
|
||||
ElButton,
|
||||
ElIcon,
|
||||
ElResult,
|
||||
ElEmpty,
|
||||
ElTag,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElPagination,
|
||||
ShoppingBag,
|
||||
Refresh
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
errorMessage: '',
|
||||
records: [],
|
||||
// 分页相关
|
||||
currentPage: 1,
|
||||
pageSize: 5
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// 总页数
|
||||
totalPages() {
|
||||
return Math.ceil(this.records.length / this.pageSize)
|
||||
},
|
||||
|
||||
// 当前页的记录
|
||||
paginatedRecords() {
|
||||
const start = (this.currentPage - 1) * this.pageSize
|
||||
const end = start + this.pageSize
|
||||
return this.records.slice(start, end)
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.loadExchangeRecords()
|
||||
},
|
||||
methods: {
|
||||
// 加载兑换记录
|
||||
async loadExchangeRecords() {
|
||||
const userId = userStore.getUserId()
|
||||
|
||||
if (!userId) {
|
||||
this.errorMessage = '请先登录'
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.errorMessage = ''
|
||||
|
||||
try {
|
||||
const response = await lotteryApi.getExchangeRecordsByUserId(userId)
|
||||
|
||||
if (response && response.success) {
|
||||
this.records = response.data || []
|
||||
// 按兑换时间倒序排列
|
||||
this.records.sort((a, b) => new Date(b.exchangeTime) - new Date(a.exchangeTime))
|
||||
} else {
|
||||
this.errorMessage = response?.message || '获取兑换记录失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取兑换记录失败:', error)
|
||||
this.errorMessage = error?.response?.data?.message || '网络连接失败,请稍后重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 获取状态样式类
|
||||
getStatusClass(isUse) {
|
||||
switch (isUse) {
|
||||
case 1:
|
||||
return 'success'
|
||||
case 0:
|
||||
return 'failure'
|
||||
default:
|
||||
return 'pending'
|
||||
}
|
||||
},
|
||||
|
||||
// 获取状态文本
|
||||
getStatusText(isUse) {
|
||||
switch (isUse) {
|
||||
case 1:
|
||||
return '成功'
|
||||
case 0:
|
||||
return '失败'
|
||||
default:
|
||||
return '处理中'
|
||||
}
|
||||
},
|
||||
|
||||
// 获取状态图标
|
||||
getStatusIcon(isUse) {
|
||||
switch (isUse) {
|
||||
case 1:
|
||||
return '✅'
|
||||
case 0:
|
||||
return '❌'
|
||||
default:
|
||||
return '⏳'
|
||||
}
|
||||
},
|
||||
|
||||
// 获取兑换类型文本
|
||||
getExchangeTypeText(type) {
|
||||
switch (type) {
|
||||
case '月度会员':
|
||||
return '月度会员'
|
||||
case '年度会员':
|
||||
return '年度会员'
|
||||
default:
|
||||
return type || '未知类型'
|
||||
}
|
||||
},
|
||||
|
||||
// 获取兑换模式文本
|
||||
getExchangeModeText(mode) {
|
||||
switch (mode) {
|
||||
case 1:
|
||||
return '兑换码兑换'
|
||||
case 2:
|
||||
return '在线支付'
|
||||
default:
|
||||
return mode || '未知模式'
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
|
||||
const date = new Date(dateStr)
|
||||
const formatted = date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
return formatted.replace(/\//g, '-')
|
||||
},
|
||||
|
||||
// 跳转到个人中心
|
||||
goToProfile() {
|
||||
this.$router.push('/profile')
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
|
||||
// 跳转到指定页
|
||||
goToPage(page) {
|
||||
if (page >= 1 && page <= this.totalPages) {
|
||||
this.currentPage = page
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面容器 */
|
||||
.exchange-records-page {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f7fa 100%);
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 现代化页面头部 */
|
||||
.page-header-modern {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
box-shadow: 0 8px 32px rgba(79, 172, 254, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header-modern::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 1440 320" preserveAspectRatio="none"><defs><linearGradient id="waveGrad" x1="0%25" y1="0%25" x2="100%25" y2="0%25"><stop offset="0%25" style="stop-color:rgba(255,255,255,0.1);stop-opacity:1" /><stop offset="50%25" style="stop-color:rgba(255,255,255,0.2);stop-opacity:1" /><stop offset="100%25" style="stop-color:rgba(255,255,255,0.1);stop-opacity:1" /></linearGradient></defs><path fill="url(%23waveGrad)" fill-opacity="1" d="M0,96L48,112C96,128,192,160,288,160C384,160,480,128,576,112C672,96,768,96,864,112C960,128,1056,160,1152,165.3C1248,171,1344,149,1392,138.7L1440,128L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path><circle cx="200" cy="60" r="4" fill="rgba(255,255,255,0.3)"><animate attributeName="cy" values="60;80;60" dur="3s" repeatCount="indefinite"/></circle><circle cx="400" cy="100" r="3" fill="rgba(255,255,255,0.25)"><animate attributeName="cy" values="100;120;100" dur="4s" repeatCount="indefinite"/></circle><circle cx="600" cy="70" r="5" fill="rgba(255,255,255,0.2)"><animate attributeName="cy" values="70;90;70" dur="3.5s" repeatCount="indefinite"/></circle><circle cx="800" cy="90" r="3.5" fill="rgba(255,255,255,0.3)"><animate attributeName="cy" values="90;110;90" dur="4.5s" repeatCount="indefinite"/></circle><circle cx="1000" cy="80" r="4" fill="rgba(255,255,255,0.25)"><animate attributeName="cy" values="80;100;80" dur="3.8s" repeatCount="indefinite"/></circle><circle cx="1200" cy="95" r="3" fill="rgba(255,255,255,0.2)"><animate attributeName="cy" values="95;115;95" dur="4.2s" repeatCount="indefinite"/></circle><g opacity="0.15"><rect x="100" y="40" width="60" height="60" fill="none" stroke="white" stroke-width="2" rx="8"><animateTransform attributeName="transform" type="rotate" from="0 130 70" to="360 130 70" dur="20s" repeatCount="indefinite"/></rect><circle cx="900" cy="50" r="25" fill="none" stroke="white" stroke-width="2"><animate attributeName="r" values="25;30;25" dur="3s" repeatCount="indefinite"/></circle><polygon points="1300,60 1320,100 1280,100" fill="none" stroke="white" stroke-width="2"><animateTransform attributeName="transform" type="rotate" from="0 1300 80" to="360 1300 80" dur="15s" repeatCount="indefinite"/></polygon></g></svg>') no-repeat center center;
|
||||
background-size: cover;
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.header-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 2px 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 内容容器 */
|
||||
.records-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 20px;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #e1e8ed;
|
||||
border-left: 4px solid #4facfe;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.error-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin: 0 0 12px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: linear-gradient(135deg, #4facfe, #00f2fe);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 16px rgba(79, 172, 254, 0.3);
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(79, 172, 254, 0.4);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin: 0 0 12px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: linear-gradient(135deg, #4facfe, #00f2fe);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(79, 172, 254, 0.3);
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(79, 172, 254, 0.4);
|
||||
}
|
||||
|
||||
/* 记录列表 */
|
||||
.records-list {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 12px 24px 12px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.record-count {
|
||||
background: linear-gradient(135deg, #4facfe, #00f2fe);
|
||||
color: white;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 记录网格 */
|
||||
.records-grid {
|
||||
padding: 16px 24px 24px 24px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 现代化记录卡片 */
|
||||
.record-card-modern {
|
||||
background: #fafbfc;
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.record-card-modern::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.record-card-modern:hover {
|
||||
border-color: #e0e0e0;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 记录头部 */
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
background: linear-gradient(135deg, #10ac84, #00d2d3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.failure {
|
||||
background: linear-gradient(135deg, #ee5a24, #ff3742);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: linear-gradient(135deg, #ffeaa7, #fab1a0);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.record-date {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 记录信息网格 */
|
||||
.record-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.type-value {
|
||||
color: #4facfe;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
color: #e74c3c;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 移动端紧凑布局 */
|
||||
.record-info-compact {
|
||||
display: none; /* 默认隐藏,仅在移动端显示 */
|
||||
}
|
||||
|
||||
.compact-left, .compact-right {
|
||||
flex: 1;
|
||||
min-width: 0; /* 允许flex项目收缩 */
|
||||
}
|
||||
|
||||
.compact-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
min-width: 0; /* 防止溢出 */
|
||||
}
|
||||
|
||||
.compact-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.compact-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.compact-value {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
word-break: break-all; /* 长文本换行 */
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.compact-value.type-value {
|
||||
color: #4facfe;
|
||||
}
|
||||
|
||||
.compact-value.amount-value {
|
||||
color: #e74c3c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 分页容器 */
|
||||
.pagination-container {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Element Plus 覆盖样式 */
|
||||
:deep(.el-pagination.is-background .el-pager li:not(.is-disabled).is-active) {
|
||||
background: linear-gradient(135deg, #4facfe, #00f2fe);
|
||||
border-color: #4facfe;
|
||||
}
|
||||
|
||||
:deep(.el-pagination.is-background .el-pager li:not(.is-disabled):hover) {
|
||||
background: linear-gradient(135deg, #4facfe, #00f2fe);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
margin-left: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.records-container {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
padding: 10px 20px 10px 20px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.records-grid {
|
||||
padding: 20px 20px 24px 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.record-info-grid {
|
||||
display: none; /* 移动端隐藏原始网格布局 */
|
||||
}
|
||||
|
||||
/* 移动端显示紧凑布局 */
|
||||
.record-info-compact {
|
||||
display: flex !important;
|
||||
gap: 24px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等屏幕尺寸额外优化 */
|
||||
@media (max-width: 600px) {
|
||||
.record-info-grid {
|
||||
display: none; /* 在600px以下也隐藏原始网格 */
|
||||
}
|
||||
|
||||
.record-info-compact {
|
||||
display: flex !important;
|
||||
gap: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header-content {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.records-container {
|
||||
padding: 20px 12px;
|
||||
}
|
||||
|
||||
.records-list {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
padding: 8px 16px 8px 16px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.record-count {
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.records-grid {
|
||||
padding: 16px 16px 20px 16px;
|
||||
}
|
||||
|
||||
.record-card-modern {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
/* 小屏幕进一步优化紧凑布局 */
|
||||
.record-info-compact {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.compact-item {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.compact-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.compact-value {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 状态徽章和日期在小屏幕上调整 */
|
||||
.record-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.record-date {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
490
src/views/HelpCenter.vue
Normal file
924
src/views/Login.vue
Normal file
@@ -0,0 +1,924 @@
|
||||
<template>
|
||||
<div class="login-page-container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h1 class="main-title">用户登录</h1>
|
||||
<p class="subtitle">您的专属彩票数据助理</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<el-card class="login-form-container" shadow="never">
|
||||
<!-- 被踢出提示 -->
|
||||
<el-alert
|
||||
v-if="showKickedOutAlert"
|
||||
title="账号已在其他设备登录"
|
||||
type="warning"
|
||||
description="为保障账号安全,您的账号已在其他设备登录,当前会话已失效。请重新登录。"
|
||||
show-icon
|
||||
:closable="true"
|
||||
@close="showKickedOutAlert = false"
|
||||
style="margin-bottom: 20px"
|
||||
/>
|
||||
|
||||
<div class="login-tabs">
|
||||
<div
|
||||
class="login-tab"
|
||||
:class="{ active: loginType === 'account' }"
|
||||
@click="switchLoginType('account')"
|
||||
>
|
||||
账号登录
|
||||
</div>
|
||||
<div
|
||||
class="login-tab"
|
||||
:class="{ active: loginType === 'phone' }"
|
||||
@click="switchLoginType('phone')"
|
||||
>
|
||||
手机号登录
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="login-form">
|
||||
<!-- 账号登录表单 -->
|
||||
<div v-if="loginType === 'account'">
|
||||
<!-- 账号 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入账号"
|
||||
:error="errors.username"
|
||||
prefix-icon="User"
|
||||
size="large"
|
||||
clearable
|
||||
/>
|
||||
<div v-if="errors.username" class="error-text">{{ errors.username }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.password"
|
||||
placeholder="请输入密码"
|
||||
:error="errors.password"
|
||||
prefix-icon="Lock"
|
||||
size="large"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:show-password="true"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<div v-if="errors.password" class="error-text">{{ errors.password }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 记住密码 -->
|
||||
<div class="form-options">
|
||||
<el-checkbox v-model="formData.remember" label="记住密码" />
|
||||
<router-link to="/reset-password" class="forgot-password-link">忘记密码?</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手机号登录表单 -->
|
||||
<div v-else>
|
||||
<!-- 手机号和发送验证码按钮 -->
|
||||
<div class="form-group">
|
||||
<div class="phone-code-row">
|
||||
<el-input
|
||||
v-model="formData.phone"
|
||||
type="tel"
|
||||
placeholder="请输入手机号"
|
||||
prefix-icon="Iphone"
|
||||
size="large"
|
||||
maxlength="11"
|
||||
@input="validatePhoneInput"
|
||||
@blur="validatePhoneOnBlur"
|
||||
clearable
|
||||
class="phone-input"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="codeBtnDisabled"
|
||||
@click="sendVerificationCode"
|
||||
class="send-code-btn-inline"
|
||||
size="large"
|
||||
>
|
||||
{{ codeButtonText }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="errors.phone" class="error-text">{{ errors.phone }}</div>
|
||||
<div v-else-if="formData.phone && formData.phone.length > 0 && formData.phone.length < 11" class="tip-text">请输入11位手机号码</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入验证码"
|
||||
prefix-icon="Key"
|
||||
size="large"
|
||||
maxlength="6"
|
||||
/>
|
||||
<div v-if="errors.code" class="error-text">{{ errors.code }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="loading"
|
||||
class="login-btn"
|
||||
size="large"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</el-button>
|
||||
|
||||
<!-- 注册链接 -->
|
||||
<div class="register-link">
|
||||
<span>还没有账号?</span>
|
||||
<router-link to="/register" class="link">立即注册</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</el-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { userStore } from '../store/user'
|
||||
import { lotteryApi } from '../api/index.js'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElCard, ElInput, ElButton, ElCheckbox, ElAlert } from 'element-plus'
|
||||
import { User, Lock, Iphone, Key } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
components: {
|
||||
ElCard,
|
||||
ElInput,
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElAlert,
|
||||
User,
|
||||
Lock,
|
||||
Iphone,
|
||||
Key
|
||||
},
|
||||
setup() {
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
return { toast, router }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loginType: 'account', // 默认使用账号登录方式
|
||||
showPassword: false,
|
||||
loading: false,
|
||||
codeCountdown: 0,
|
||||
timer: null,
|
||||
showPhoneError: false,
|
||||
phoneValid: false,
|
||||
showKickedOutAlert: false, // 是否显示被踢出提示
|
||||
|
||||
formData: {
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
code: '',
|
||||
remember: false
|
||||
},
|
||||
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
codeButtonText() {
|
||||
return this.codeCountdown > 0 ? `${this.codeCountdown}秒后重试` : '获取验证码';
|
||||
},
|
||||
codeBtnDisabled() {
|
||||
return this.codeCountdown > 0 || !this.isValidPhone(this.formData.phone);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 检查是否因为被踢出而跳转到登录页
|
||||
if (userStore.isKickedOut) {
|
||||
this.showKickedOutAlert = true
|
||||
// 显示提示后重置状态
|
||||
setTimeout(() => {
|
||||
userStore.resetKickedOutStatus()
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 切换登录方式
|
||||
switchLoginType(type) {
|
||||
this.loginType = type;
|
||||
this.errors = {};
|
||||
|
||||
// 切换时重置相关表单数据
|
||||
if (type === 'account') {
|
||||
this.formData.phone = '';
|
||||
this.formData.code = '';
|
||||
} else {
|
||||
this.formData.username = '';
|
||||
this.formData.password = '';
|
||||
}
|
||||
},
|
||||
|
||||
// 手机号格式验证
|
||||
isValidPhone(phone) {
|
||||
return /^1[3-9]\d{9}$/.test(phone);
|
||||
},
|
||||
|
||||
// 手机号输入时验证
|
||||
validatePhoneInput() {
|
||||
// 清除之前的错误
|
||||
this.errors.phone = '';
|
||||
this.showPhoneError = false;
|
||||
this.phoneValid = false;
|
||||
|
||||
const phone = this.formData.phone;
|
||||
|
||||
// 如果输入不是数字,替换非数字字符
|
||||
if (!/^\d*$/.test(phone)) {
|
||||
this.formData.phone = phone.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
// 如果长度达到11位,验证格式
|
||||
if (phone.length === 11) {
|
||||
if (this.isValidPhone(phone)) {
|
||||
this.phoneValid = true;
|
||||
} else {
|
||||
this.errors.phone = '手机号格式不正确';
|
||||
this.showPhoneError = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 手机号失焦时验证
|
||||
validatePhoneOnBlur() {
|
||||
const phone = this.formData.phone;
|
||||
|
||||
if (phone && phone.length > 0) {
|
||||
if (phone.length !== 11) {
|
||||
this.errors.phone = '手机号应为11位数字';
|
||||
this.showPhoneError = true;
|
||||
this.phoneValid = false;
|
||||
} else if (!this.isValidPhone(phone)) {
|
||||
this.errors.phone = '请输入正确的手机号码';
|
||||
this.showPhoneError = true;
|
||||
this.phoneValid = false;
|
||||
} else {
|
||||
this.phoneValid = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 发送验证码
|
||||
async sendVerificationCode() {
|
||||
if (!this.formData.phone) {
|
||||
this.errors.phone = '请输入手机号';
|
||||
this.showPhoneError = true;
|
||||
this.phoneValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidPhone(this.formData.phone)) {
|
||||
this.errors.phone = '请输入正确的手机号码';
|
||||
this.showPhoneError = true;
|
||||
this.phoneValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.phoneValid = true;
|
||||
|
||||
try {
|
||||
// 开始倒计时 (先启动倒计时,避免API延迟导致用户体验不佳)
|
||||
this.codeCountdown = 60;
|
||||
this.startCodeCountdown();
|
||||
|
||||
const response = await lotteryApi.sendSmsCode(this.formData.phone);
|
||||
|
||||
if (response.success) {
|
||||
this.toast.success('验证码已发送,请注意查收');
|
||||
} else {
|
||||
// 如果发送失败,停止倒计时
|
||||
this.codeCountdown = 0;
|
||||
clearInterval(this.timer);
|
||||
this.toast.error(response.message || '发送验证码失败,请稍后重试');
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果发送出错,停止倒计时
|
||||
this.codeCountdown = 0;
|
||||
clearInterval(this.timer);
|
||||
console.error('发送验证码失败:', error);
|
||||
this.toast.error('发送验证码失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
|
||||
// 开始倒计时
|
||||
startCodeCountdown() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
if (this.codeCountdown > 0) {
|
||||
this.codeCountdown--;
|
||||
} else {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
// 表单验证
|
||||
validateForm() {
|
||||
this.errors = {};
|
||||
|
||||
if (this.loginType === 'account') {
|
||||
// 账号登录验证
|
||||
if (!this.formData.username) {
|
||||
this.errors.username = '请输入账号';
|
||||
}
|
||||
|
||||
if (!this.formData.password) {
|
||||
this.errors.password = '请输入密码';
|
||||
} else if (this.formData.password.length < 6) {
|
||||
this.errors.password = '密码至少6位';
|
||||
}
|
||||
} else {
|
||||
// 手机号登录验证
|
||||
if (!this.formData.phone) {
|
||||
this.errors.phone = '请输入手机号';
|
||||
this.showPhoneError = true;
|
||||
} else if (!this.isValidPhone(this.formData.phone)) {
|
||||
this.errors.phone = '请输入正确的手机号码';
|
||||
this.showPhoneError = true;
|
||||
}
|
||||
|
||||
if (!this.formData.code) {
|
||||
this.errors.code = '请输入验证码';
|
||||
} else if (this.formData.code.length < 4 || this.formData.code.length > 6) {
|
||||
this.errors.code = '验证码格式不正确';
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
// 处理登录
|
||||
async handleLogin() {
|
||||
if (!this.validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (this.loginType === 'account') {
|
||||
// 账号密码登录
|
||||
response = await lotteryApi.userLogin(this.formData.username, this.formData.password);
|
||||
} else {
|
||||
// 手机号验证码登录
|
||||
response = await lotteryApi.userPhoneLogin(this.formData.phone, this.formData.code);
|
||||
}
|
||||
|
||||
if (response.success === true) {
|
||||
// 登录成功,调用getLoginUser获取完整用户信息
|
||||
try {
|
||||
const userInfo = await userStore.fetchLoginUser();
|
||||
if (userInfo) {
|
||||
// 触发Coze SDK重新初始化事件
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('reinitializeCozeSDK'));
|
||||
console.log('已触发Coze SDK重新初始化事件');
|
||||
}, 200);
|
||||
|
||||
// 直接跳转到个人中心
|
||||
setTimeout(() => {
|
||||
this.router.push('/profile');
|
||||
}, 300);
|
||||
} else {
|
||||
this.toast.error('获取用户信息失败,请重新登录');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
this.toast.error('获取用户信息失败,请重新登录');
|
||||
}
|
||||
} else {
|
||||
// 登录失败
|
||||
this.toast.error(response.message || '登录失败,请检查账号密码');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
if (error.response && error.response.data) {
|
||||
this.toast.error(error.response.data.message || '登录失败,请检查账号密码');
|
||||
} else {
|
||||
this.toast.error('网络错误,请重试');
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 组件销毁时清除定时器
|
||||
beforeUnmount() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 登录页面容器 */
|
||||
.login-page-container {
|
||||
min-height: calc(100vh - 70px);
|
||||
background: #f0f2f5;
|
||||
padding: 20px 20px 8px 20px;
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
|
||||
color: white;
|
||||
padding: 35px 20px 25px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(238, 90, 82, 0.3);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 32px;
|
||||
margin: 0 auto 4px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-shadow: 0 2px 8px rgba(0,0,0,0.5), 0 0 20px rgba(0,0,0,0.3);
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
color: white;
|
||||
opacity: 0.95;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.4);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 桌面端样式 */
|
||||
@media (min-width: 1024px) {
|
||||
.page-header {
|
||||
padding: 30px 20px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form-container {
|
||||
padding: 0;
|
||||
background: white;
|
||||
margin: 0 0 20px 0;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 登录方式切换标签 */
|
||||
.login-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.login-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 18px 0;
|
||||
font-size: 15px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-tab.active {
|
||||
color: #e53e3e;
|
||||
background: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
.login-tab:hover:not(.active) {
|
||||
color: #666;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
background: white;
|
||||
border-radius: 0;
|
||||
padding: 28px 24px 20px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 表单组 */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
background: #f8f9fa;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 56px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.input-wrapper:focus-within {
|
||||
border-color: #e9ecef;
|
||||
background: white;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.input-wrapper.error {
|
||||
border-color: #dc3545;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.input-wrapper.success {
|
||||
border-color: #4caf50;
|
||||
background: #f8fff8;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
padding: 12px 25px;
|
||||
color: #6c757d;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.icon-img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
padding: 16px 8px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: #212529;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 控制浏览器自动填充的样式 */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px white inset !important;
|
||||
-webkit-text-fill-color: #212529 !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* 手机号和验证码按钮同行布局 */
|
||||
.phone-code-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.phone-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 内联发送验证码按钮样式 */
|
||||
.send-code-btn-inline {
|
||||
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120px;
|
||||
flex-shrink: 0;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.send-code-btn-inline:hover:not(.is-disabled) {
|
||||
background: linear-gradient(135deg, #d43030, #ff5a5a);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 15px rgba(229, 62, 62, 0.3);
|
||||
}
|
||||
|
||||
.send-code-btn-inline:active:not(.is-disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.send-code-btn-inline.is-disabled {
|
||||
background: #cccccc !important;
|
||||
border-color: #cccccc !important;
|
||||
color: #888 !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* 提示文本 */
|
||||
.error-text {
|
||||
color: #ff4444;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
color: #888888;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 隐藏浏览器自带的密码控件 */
|
||||
input::-ms-reveal,
|
||||
input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input::-webkit-credentials-auto-fill-button {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toggle-icon-img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 表单选项 */
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.checkbox-wrapper input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.checkbox-wrapper input:checked + .checkmark {
|
||||
background: #e53e3e;
|
||||
border-color: #e53e3e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 忘记密码链接 */
|
||||
.forgot-password-link {
|
||||
color: #e53e3e;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.forgot-password-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 登录按钮 */
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
margin: 24px 0 24px 0;
|
||||
padding: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
height: 52px;
|
||||
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(229, 62, 62, 0.25);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.login-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 30px rgba(229, 62, 62, 0.35);
|
||||
background: linear-gradient(135deg, #d43030, #ff5a5a);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.login-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(229, 62, 62, 0.3);
|
||||
}
|
||||
|
||||
/* Element UI 组件自定义样式 */
|
||||
:deep(.el-input__wrapper) {
|
||||
padding: 6px 16px;
|
||||
box-shadow: none !important;
|
||||
background-color: #f8f9fa;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-input__wrapper.is-focus) {
|
||||
background-color: #fff;
|
||||
border-color: #e53e3e;
|
||||
box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1);
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-input__prefix) {
|
||||
margin-right: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 44px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: #e53e3e;
|
||||
border-color: #e53e3e;
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-button.is-disabled) {
|
||||
background: #cccccc;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
|
||||
/* 注册链接 */
|
||||
.register-link {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.register-link .link {
|
||||
color: #e53e3e;
|
||||
text-decoration: none;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.register-link .link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.login-page-container {
|
||||
padding: 10px 10px 5px 10px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 22px 20px 18px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-page-container {
|
||||
padding: 5px 5px 3px 5px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 30px 20px 25px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 28px;
|
||||
margin-bottom: 3px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 28px 20px 20px;
|
||||
}
|
||||
|
||||
.login-tab {
|
||||
padding: 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
height: 48px;
|
||||
font-size: 15px;
|
||||
margin: 20px 0 20px 0;
|
||||
}
|
||||
|
||||
.send-code-btn-inline {
|
||||
font-size: 12px;
|
||||
min-width: 90px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.phone-code-row {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 桌面端样式 - 这部分已在上面定义,这里移除重复定义 */
|
||||
</style>
|
||||
770
src/views/LotteryInfo.vue
Normal file
@@ -0,0 +1,770 @@
|
||||
<template>
|
||||
<div class="lottery-info-page">
|
||||
<!-- 公告区域 -->
|
||||
<div class="notice-section">
|
||||
<div class="notice-icon">📊</div>
|
||||
<div class="notice-text">实时开奖结果查询,历史数据分析</div>
|
||||
</div>
|
||||
|
||||
<!-- 彩票种类区域 -->
|
||||
<div class="lottery-section">
|
||||
<h2 class="section-title">中国福利彩票</h2>
|
||||
<div class="lottery-grid">
|
||||
<!-- 双色球 -->
|
||||
<div class="lottery-card available welfare-red-gradient" @click="goToShuangseqiu">
|
||||
<div class="lottery-icon">
|
||||
<img src="/assets/type/ssq.svg" alt="双色球" />
|
||||
</div>
|
||||
<div class="lottery-info">
|
||||
<h3 class="lottery-name">双色球</h3>
|
||||
<p class="lottery-desc">每周二、四、日21:15开奖</p>
|
||||
</div>
|
||||
<div class="lottery-status available">可查询</div>
|
||||
</div>
|
||||
|
||||
<!-- 快乐8 -->
|
||||
<div class="lottery-card disabled welfare-yellow-gradient" @click="showDevelopingTip">
|
||||
<div class="lottery-icon">
|
||||
<img src="/assets/type/kl8.svg" alt="快乐8" />
|
||||
</div>
|
||||
<div class="lottery-info">
|
||||
<h3 class="lottery-name">快乐8</h3>
|
||||
<p class="lottery-desc">每天21:15开奖</p>
|
||||
</div>
|
||||
<div class="lottery-status developing">即将开放</div>
|
||||
</div>
|
||||
|
||||
<!-- 七乐彩 -->
|
||||
<div class="lottery-card disabled welfare-blue-gradient" @click="showDevelopingTip">
|
||||
<div class="lottery-icon">
|
||||
<img src="/assets/type/7lecai.svg" alt="七乐彩" />
|
||||
</div>
|
||||
<div class="lottery-info">
|
||||
<h3 class="lottery-name">七乐彩</h3>
|
||||
<p class="lottery-desc">每周一、三、五21:15开奖</p>
|
||||
</div>
|
||||
<div class="lottery-status developing">即将开放</div>
|
||||
</div>
|
||||
|
||||
<!-- 福彩3D -->
|
||||
<div class="lottery-card disabled welfare-purple-gradient" @click="showDevelopingTip">
|
||||
<div class="lottery-icon">
|
||||
<img src="/assets/type/3D.svg" alt="福彩3D" />
|
||||
</div>
|
||||
<div class="lottery-info">
|
||||
<h3 class="lottery-name">福彩3D</h3>
|
||||
<p class="lottery-desc">每天21:15开奖</p>
|
||||
</div>
|
||||
<div class="lottery-status developing">即将开放</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 体彩区域 -->
|
||||
<div class="lottery-section">
|
||||
<h2 class="section-title">中国体育彩票</h2>
|
||||
<div class="lottery-grid">
|
||||
<!-- 大乐透 -->
|
||||
<div class="lottery-card available sports-orange-gradient" @click="goToDaletou">
|
||||
<div class="lottery-icon">
|
||||
<img src="/assets/type/daletou.svg" alt="大乐透" />
|
||||
</div>
|
||||
<div class="lottery-info">
|
||||
<h3 class="lottery-name">大乐透</h3>
|
||||
<p class="lottery-desc">每周一、三、六21:25开奖</p>
|
||||
</div>
|
||||
<div class="lottery-status available">可查询</div>
|
||||
</div>
|
||||
|
||||
<!-- 排列三 -->
|
||||
<div class="lottery-card disabled sports-yellow-gradient" @click="showDevelopingTip">
|
||||
<div class="lottery-icon">
|
||||
<img src="/assets/type/pailie3.svg" alt="排列三" />
|
||||
</div>
|
||||
<div class="lottery-info">
|
||||
<h3 class="lottery-name">排列三</h3>
|
||||
<p class="lottery-desc">每天21:25开奖</p>
|
||||
</div>
|
||||
<div class="lottery-status developing">即将开放</div>
|
||||
</div>
|
||||
|
||||
<!-- 七星彩 -->
|
||||
<div class="lottery-card disabled sports-blue-gradient" @click="showDevelopingTip">
|
||||
<div class="lottery-icon">
|
||||
<img src="/assets/type/7xingcai.svg" alt="七星彩" />
|
||||
</div>
|
||||
<div class="lottery-info">
|
||||
<h3 class="lottery-name">七星彩</h3>
|
||||
<p class="lottery-desc">每周二、五、日21:25开奖</p>
|
||||
</div>
|
||||
<div class="lottery-status developing">即将开放</div>
|
||||
</div>
|
||||
|
||||
<!-- 排列五 -->
|
||||
<div class="lottery-card disabled sports-purple-gradient" @click="showDevelopingTip">
|
||||
<div class="lottery-icon">
|
||||
<img src="/assets/type/pailie5.svg" alt="排列五" />
|
||||
</div>
|
||||
<div class="lottery-info">
|
||||
<h3 class="lottery-name">排列五</h3>
|
||||
<p class="lottery-desc">每天21:25开奖</p>
|
||||
</div>
|
||||
<div class="lottery-status developing">即将开放</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 开发中提示框 -->
|
||||
<div v-if="showTip" class="tip-overlay" @click="hideTip">
|
||||
<div class="tip-content" @click.stop>
|
||||
<div class="tip-icon">
|
||||
<svg class="developing-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L13.09 8.26L22 9L13.09 9.74L12 16L10.91 9.74L2 9L10.91 8.26L12 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>功能开发中</h3>
|
||||
<p>该彩票类型的开奖信息查询功能正在开发中,敬请期待!</p>
|
||||
<div class="tip-actions">
|
||||
<button class="tip-button" @click="hideTip">我知道了</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LotteryInfo',
|
||||
data() {
|
||||
return {
|
||||
showTip: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToShuangseqiu() {
|
||||
// 这里可以跳转到双色球开奖信息页面
|
||||
this.$router.push('/lottery-info/ssq')
|
||||
},
|
||||
goToDaletou() {
|
||||
// 跳转到大乐透开奖信息页面
|
||||
this.$router.push('/lottery-info/dlt')
|
||||
},
|
||||
showDevelopingTip() {
|
||||
this.showTip = true
|
||||
},
|
||||
hideTip() {
|
||||
this.showTip = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lottery-info-page {
|
||||
min-height: calc(100vh - 70px);
|
||||
background: #f5f5f5;
|
||||
overflow-x: hidden;
|
||||
padding-top: 16px; /* 顶部添加一些间距,因为没有banner */
|
||||
}
|
||||
|
||||
/* 公告区域 */
|
||||
.notice-section {
|
||||
background: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
margin: 16px;
|
||||
padding: 8px 8px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.notice-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.notice-text {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 彩票区域 */
|
||||
.lottery-section {
|
||||
margin: 16px;
|
||||
margin-bottom: 24px;
|
||||
width: calc(100% - 32px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0 0 16px 0;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.lottery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lottery-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
border: 1px solid #f0f0f0;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-height: 110px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.lottery-card.available:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.lottery-card.disabled {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.lottery-card.disabled:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.lottery-icon {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lottery-icon img {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lottery-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lottery-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.lottery-desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin: 4px 0 0 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.lottery-status {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lottery-status.available {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
border: 1px solid #b7eb8f;
|
||||
}
|
||||
|
||||
.lottery-status.developing {
|
||||
background: #fff7e6;
|
||||
color: #fa8c16;
|
||||
border: 1px solid #ffd591;
|
||||
}
|
||||
|
||||
/* 体育彩票渐变样式 */
|
||||
.sports-orange-gradient {
|
||||
background: linear-gradient(90deg, rgba(254, 150, 51, 1) 0%, rgba(246, 102, 77, 1) 100%) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.sports-orange-gradient .lottery-name,
|
||||
.sports-orange-gradient .lottery-desc {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.sports-yellow-gradient {
|
||||
background: linear-gradient(90deg, rgba(251, 216, 48, 1) 0%, rgba(255, 174, 1, 1) 100%) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.sports-yellow-gradient .lottery-name,
|
||||
.sports-yellow-gradient .lottery-desc {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.sports-blue-gradient {
|
||||
background: linear-gradient(90deg, rgba(77, 225, 219, 1) 0%, rgba(39, 138, 241, 1) 100%) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.sports-blue-gradient .lottery-name,
|
||||
.sports-blue-gradient .lottery-desc {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.sports-purple-gradient {
|
||||
background: linear-gradient(90deg, rgba(191, 132, 252, 1) 0%, rgba(114, 89, 248, 1) 100%) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.sports-purple-gradient .lottery-name,
|
||||
.sports-purple-gradient .lottery-desc {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* 福利彩票渐变样式 */
|
||||
.welfare-red-gradient {
|
||||
background: linear-gradient(90deg, rgba(254, 150, 51, 1) 0%, rgba(246, 102, 77, 1) 100%) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.welfare-red-gradient .lottery-name,
|
||||
.welfare-red-gradient .lottery-desc {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.welfare-yellow-gradient {
|
||||
background: linear-gradient(90deg, rgba(251, 216, 48, 1) 0%, rgba(255, 174, 1, 1) 100%) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.welfare-yellow-gradient .lottery-name,
|
||||
.welfare-yellow-gradient .lottery-desc {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.welfare-blue-gradient {
|
||||
background: linear-gradient(90deg, rgba(77, 225, 219, 1) 0%, rgba(39, 138, 241, 1) 100%) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.welfare-blue-gradient .lottery-name,
|
||||
.welfare-blue-gradient .lottery-desc {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.welfare-purple-gradient {
|
||||
background: linear-gradient(90deg, rgba(191, 132, 252, 1) 0%, rgba(114, 89, 248, 1) 100%) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.welfare-purple-gradient .lottery-name,
|
||||
.welfare-purple-gradient .lottery-desc {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* 渐变卡片的状态标签样式 */
|
||||
.sports-orange-gradient .lottery-status,
|
||||
.sports-yellow-gradient .lottery-status,
|
||||
.sports-blue-gradient .lottery-status,
|
||||
.sports-purple-gradient .lottery-status,
|
||||
.welfare-red-gradient .lottery-status,
|
||||
.welfare-yellow-gradient .lottery-status,
|
||||
.welfare-blue-gradient .lottery-status,
|
||||
.welfare-purple-gradient .lottery-status {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
color: white !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.tip-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: 9999;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.tip-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
max-width: 380px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
border: 2px solid rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 20px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1890ff, #40a9ff);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 8px 24px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
.developing-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tip-content h3 {
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tip-content p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.tip-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tip-button {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 100px;
|
||||
background: linear-gradient(135deg, #1890ff, #40a9ff);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
.tip-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(24, 144, 255, 0.4);
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.lottery-info-page {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.lottery-section {
|
||||
margin: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.lottery-grid {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lottery-card {
|
||||
padding: 14px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.lottery-icon {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.lottery-icon img {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
.lottery-name {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.lottery-desc {
|
||||
font-size: 11px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.lottery-info-page {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.tip-content {
|
||||
padding: 25px 20px;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.developing-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.tip-content h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.tip-content p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tip-actions {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tip-button {
|
||||
width: 100%;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
|
||||
.notice-section {
|
||||
margin: 12px;
|
||||
padding: 8px 8px 8px 8px;
|
||||
}
|
||||
|
||||
.notice-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.lottery-section {
|
||||
margin: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.lottery-grid {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lottery-card {
|
||||
padding: 12px;
|
||||
min-height: 88px;
|
||||
}
|
||||
|
||||
.lottery-icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.lottery-icon img {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.lottery-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.lottery-desc {
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.lottery-status {
|
||||
font-size: 10px;
|
||||
padding: 3px 6px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕优化 */
|
||||
@media (max-width: 400px) {
|
||||
.lottery-card {
|
||||
padding: 10px;
|
||||
min-height: 90px;
|
||||
}
|
||||
|
||||
.lottery-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.lottery-icon img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.lottery-name {
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.lottery-desc {
|
||||
font-size: 11px;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.lottery-status {
|
||||
font-size: 9px;
|
||||
padding: 2px 4px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.lottery-grid {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lottery-section {
|
||||
margin: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 桌面端样式 */
|
||||
@media (min-width: 1024px) {
|
||||
.lottery-info-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
|
||||
.lottery-section {
|
||||
margin: 24px 40px 32px 40px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 22px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.lottery-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.lottery-card {
|
||||
padding: 20px;
|
||||
min-height: 160px;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lottery-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.lottery-icon img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.lottery-info {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lottery-name {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.lottery-desc {
|
||||
font-size: 13px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.lottery-status {
|
||||
font-size: 13px;
|
||||
padding: 5px 10px;
|
||||
position: static;
|
||||
margin-top: 8px;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1275
src/views/LotteryPremium.vue
Normal file
1280
src/views/LotterySelection.vue
Normal file
543
src/views/MemberAgreement.vue
Normal file
@@ -0,0 +1,543 @@
|
||||
<template>
|
||||
<div class="member-agreement-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header-modern">
|
||||
<div class="header-content">
|
||||
<button class="back-btn" @click="goBack">
|
||||
<svg viewBox="0 0 24 24" class="back-icon">
|
||||
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="header-info">
|
||||
<div class="header-text">
|
||||
<h1 class="header-title">会员服务协议</h1>
|
||||
<p class="header-subtitle">《精彩猪手》会员服务条款</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="agreement-content">
|
||||
<!-- 标题 -->
|
||||
<div class="agreement-title-section">
|
||||
<h2 class="main-title">《精彩猪手》会员服务协议</h2>
|
||||
<p class="intro-text">
|
||||
欢迎使用《精彩猪手》数据服务!<br>
|
||||
《精彩猪手》是一款彩票数据姿态逻辑分析推测工具,是各位会员朋友的超级数据助理。
|
||||
</p>
|
||||
<div class="notice-box">
|
||||
在开始使用我们的服务之前,请会员仔细阅读并充分理解本《会员服务协议》的全部内容。
|
||||
本协议是会员与我们(西安溢彩数智科技有限公司)之间关于使用本服务的法律协议。
|
||||
一旦会员使用本服务,即表示会员已同意接受本协议的约束。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第一章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">一、定义</h3>
|
||||
<div class="section-content">
|
||||
<div class="definition-item">
|
||||
<span class="def-num">1</span>
|
||||
<div class="def-content">
|
||||
<strong>本服务</strong>:指我们通过网站、应用程序或其他相关平台向会员提供的《精彩猪手》数据服务,包括但不限于彩票开奖信息查询、姿态逻辑数据分析报告、个性化推导推测辅助,以及会员行为统计、经验技巧交流等。
|
||||
</div>
|
||||
</div>
|
||||
<div class="definition-item">
|
||||
<span class="def-num">2</span>
|
||||
<div class="def-content">
|
||||
<strong>会员</strong>:指承认本协议,接受本服务的自然人。具体包括一般会员和付费会员。
|
||||
</div>
|
||||
</div>
|
||||
<div class="definition-item">
|
||||
<span class="def-num">3</span>
|
||||
<div class="def-content">
|
||||
<strong>个人信息</strong>:指以电子或者其他方式记录的与已识别或者可识别的自然人有关的各种信息,不包括匿名化处理后的信息。
|
||||
</div>
|
||||
</div>
|
||||
<div class="definition-item">
|
||||
<span class="def-num">4</span>
|
||||
<div class="def-content">
|
||||
<strong>知识产权</strong>:包括但不限于著作权、专利权、商标权、商业秘密等。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">二、服务内容及门槛</h3>
|
||||
<div class="section-content">
|
||||
<p>1. 我们将尽力为会员提供稳定、高效的服务。服务内容包括但不限于:</p>
|
||||
<ul class="service-list">
|
||||
<li>1.1 既定彩票开奖信息查询浏览;</li>
|
||||
<li>1.2 既定彩票开奖数据姿态逻辑分析及报告;</li>
|
||||
<li>1.3 按期次更新的号球动态数据,以及通过"本期"已出的开奖号球,进行"下期"开奖号球的个性化推测逻辑辅助;</li>
|
||||
<li>1.4 根据首球与随球的关联线索,启发复式/胆拖的投注参考;</li>
|
||||
<li>1.5 推测行为统计、分析、备份;</li>
|
||||
<li>1.6 进行规律总结、技巧探索、经验交流。</li>
|
||||
</ul>
|
||||
<p>2. 我们会根据实际情况对服务内容进行调整、更新。如有重大变更,我们将通过企业公众号、企业微信、短信等方式通知会员。</p>
|
||||
<p>3. 会员理解并同意,使用本服务可能需要会员具备相应的设备、软件、网络环境和一定的专业知识,相关费用由会员自行承担。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">三、会员账号</h3>
|
||||
<div class="section-content">
|
||||
<p>1. 会员可以通过免费注册的方式获取本服务账号。在注册过程中,会员需提供真实、准确、完整的信息,并确保信息的及时更新。</p>
|
||||
<p>2. 会员应妥善保管账号及密码,不将账号出借、转让、赠予或共享给他人使用。因会员自身原因导致账号泄露或被他人使用的后果,由会员自行承担。</p>
|
||||
<p>3. 若会员发现账号存在异常或安全问题,请立即通知我们,我们将尽力协助处理,但对于非因我们原因导致的损失,我们不承担责任。</p>
|
||||
<p>4. 会员在使用账号过程中应遵守法律法规及本协议约定,不得利用账号从事违法、违规,或损害他人权益的行为,否则我们有权暂停或终止会员的账号使用。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第四章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">四、会员权利与义务</h3>
|
||||
<div class="section-content">
|
||||
<h4 class="sub-title">1. 权利</h4>
|
||||
<ul class="rights-list">
|
||||
<li>1.1 有权在遵守本协议的前提下,按照我们提供的方式使用服务。</li>
|
||||
<li>1.2 对我们提供的服务质量有权提出合理的意见和建议。</li>
|
||||
</ul>
|
||||
<h4 class="sub-title">2. 义务</h4>
|
||||
<ul class="duties-list">
|
||||
<li>2.1 遵守国家法律法规及互联网相关规定,不得利用本服务从事违法犯罪活动。</li>
|
||||
<li>2.2 不得干扰、破坏本服务的正常运行,不得对服务进行反向工程、反编译、反汇编等行为。</li>
|
||||
<li>2.3 不得发布、传播任何侵犯他人知识产权、隐私或其他合法权益的信息。</li>
|
||||
<li>2.4 不得恶意注册账号、发送垃圾信息或进行其他滥用服务的行为。</li>
|
||||
<li>2.5 如因会员的行为导致我们或第三方遭受损失,责任方会员应承担相应的赔偿责任。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第五章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">五、隐私政策</h3>
|
||||
<div class="section-content">
|
||||
<p>1. 我们十分重视对会员个人信息保护,将谨慎收集、安全存储、妥善使用会员的个人信息。</p>
|
||||
<p>2. 会员同意我们为提供服务、改进服务,包括遵守法律法规的需要,对会员的个人信息进行合理的调用,过程中我们将采取合理措施确保信息安全。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第六章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">六、知识产权</h3>
|
||||
<div class="section-content">
|
||||
<p>我们对本服务及相关内容(包括但不限于软件、数据、文字、图片、音频、视频等)享有知识产权。未经我们书面许可,会员不得擅自复制、改编,或创造基于本服务的衍生品。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第七章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">七、责任限制与免责</h3>
|
||||
<div class="section-content">
|
||||
<div class="warning-box">
|
||||
<p><strong>重要提示:</strong>会员应当理解并知晓,彩票本身就是概率游戏,彩票开奖也是纯粹的随机事件。因此,本服务仅具备参考和辅助功效,会员必须对自己的判断和选择结果承担最终责任。</p>
|
||||
</div>
|
||||
<p>1. 本服务所提供的各项数据分析报告,均为根据彩票历史数据、统计学、数学和数据科学原理,通过计算机技术进行建模分析而得出。我们仅对数据的及时性、客观性承担责任。</p>
|
||||
<p>2. 我们将尽力确保服务的正常运行,但由于互联网的复杂性和不确定性,可能会出现服务中断、延迟、错误等情况。对于因不可抗力、技术故障、网络攻击等不可预见、不可避免的原因导致的服务问题,我们不承担责任。</p>
|
||||
<p>3. 我们对会员通过本服务获取的第三方信息的准确性、完整性、可靠性不承担保证责任,会员应自行判断并承担使用风险。</p>
|
||||
<p>4. 在任何情况下,我们对会员因使用本服务而产生的直接、间接、偶然、特殊或后果性损失,均不承担超过会员实际支付的服务费用的赔偿责任。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第八章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">八、收费及其规则</h3>
|
||||
<div class="section-content">
|
||||
<div class="price-box">
|
||||
<h4>会员类型与收费标准</h4>
|
||||
<div class="price-grid">
|
||||
<div class="price-item">
|
||||
<span class="price-label">包月付费</span>
|
||||
<span class="price-value">10元/月</span>
|
||||
</div>
|
||||
<div class="price-item">
|
||||
<span class="price-label">包年付费</span>
|
||||
<span class="price-value">100元/年</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>1. 本服务采用会员制运营模式。所有会员分为普通会员与VIP会员两种类型。</p>
|
||||
<p>2. 本服务对普通会员仅提供一般的基础数据服务。</p>
|
||||
<p>3. 本服务对VIP会员,在提供一般基础数据服务的基础上,还提供个性化的智推、精推特色辅助服务。</p>
|
||||
<p>4. 包月付费每月10元,不足一个月时,按一个月计算。</p>
|
||||
<p>5. 包年付费每年100元。若因会员原因中途提出终止协议,需要发生退款时,所退款项均按包月付费标准进行折算。</p>
|
||||
<p>6. 出现业务退款情形时,所退款项只限原路退回付款账号。</p>
|
||||
<p>7. 所有会员在服务有效期内使用本服务额定内容的时段和频次均不受限。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第九章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">九、协议变更与终止</h3>
|
||||
<div class="section-content">
|
||||
<p>1. 我们有权根据法律法规变化、业务发展需要等对本协议进行变更。变更后的协议将在相关平台以显著方式公布,自公布之日起生效。</p>
|
||||
<p>2. 出现以下情况下,我们有权终止本协议及停止会员继续使用服务:</p>
|
||||
<ul>
|
||||
<li>2.1 会员严重违反本协议约定。</li>
|
||||
<li>2.2 法律法规要求我们终止服务。</li>
|
||||
<li>2.3 因不可抗力等不可预见、不可避免的原因导致服务无法继续提供。</li>
|
||||
</ul>
|
||||
<p>3. 协议终止后,我们有权根据法律法规要求,对会员的相关信息进行封存处理。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第十章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">十、争议解决</h3>
|
||||
<div class="section-content">
|
||||
<p>1. 本协议的签订、履行、解释及争议解决均适用《中华人民共和国民法典》。</p>
|
||||
<p>2. 如双方在本协议履行过程中发生争议,应首先通过友好协商解决;协商不成的,任何一方均有权向有管辖权的人民法院提起诉讼。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第十一章 -->
|
||||
<div class="agreement-section">
|
||||
<h3 class="section-title">十一、其他条款</h3>
|
||||
<div class="section-content">
|
||||
<p>1. 本协议构成会员与我们之间关于本服务的完整协议。未经我们书面同意,会员不得转让本协议项下的任何权利、利益和义务。</p>
|
||||
<p>2. 本协议各条款的标题仅为方便阅读而设,不影响条款的具体含义及解释。</p>
|
||||
<p>3. 若本协议任何条款被认定为无效或不可执行,不影响其他条款的效力及执行。</p>
|
||||
<p>4. 我们未行使或执行本协议任何权利或条款,不构成对该权利或条款的放弃。</p>
|
||||
<p>5. 本协议自会员成功注册《精彩猪手》服务相关账号之日即刻生效。</p>
|
||||
<p>6. 任何有关本协议项下服务的问题,会员可通过本公司企业微信进行咨询。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="footer-info">
|
||||
<p class="company-name">西安溢彩数智科技有限公司</p>
|
||||
<p class="date">2025年12月31日</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MemberAgreement',
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.member-agreement-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header-modern {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
padding: 20px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 12px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 4px 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 主要内容 */
|
||||
.agreement-content {
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
/* 标题区域 */
|
||||
.agreement-title-section {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.notice-box {
|
||||
background: linear-gradient(135deg, rgba(255, 193, 7, 0.1) 0%, rgba(255, 152, 0, 0.1) 100%);
|
||||
padding: 16px 20px;
|
||||
border-radius: 12px;
|
||||
border-left: 4px solid #ffc107;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 章节样式 */
|
||||
.agreement-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #4facfe;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.section-content p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 16px 0 12px 0;
|
||||
}
|
||||
|
||||
/* 定义项 */
|
||||
.definition-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 14px;
|
||||
padding: 14px;
|
||||
background: #fafbfc;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.def-num {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.def-content {
|
||||
flex: 1;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
.service-list,
|
||||
.rights-list,
|
||||
.duties-list {
|
||||
padding-left: 20px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.service-list li,
|
||||
.rights-list li,
|
||||
.duties-list li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 警告框 */
|
||||
.warning-box {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%);
|
||||
padding: 16px 20px;
|
||||
border-radius: 12px;
|
||||
border-left: 4px solid #ef4444;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.warning-box p {
|
||||
margin: 0;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
/* 价格框 */
|
||||
.price-box {
|
||||
background: linear-gradient(135deg, rgba(79, 172, 254, 0.1) 0%, rgba(0, 242, 254, 0.1) 100%);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.price-box h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.price-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.price-item {
|
||||
background: white;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.price-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #4facfe;
|
||||
}
|
||||
|
||||
/* 底部信息 */
|
||||
.footer-info {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header-modern {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.agreement-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.definition-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.def-num {
|
||||
margin-bottom: 8px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.price-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
487
src/views/NotFound.vue
Normal file
@@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<div class="not-found">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="background-decoration">
|
||||
<div class="floating-shape shape-1"></div>
|
||||
<div class="floating-shape shape-2"></div>
|
||||
<div class="floating-shape shape-3"></div>
|
||||
<div class="floating-shape shape-4"></div>
|
||||
</div>
|
||||
|
||||
<div class="not-found-content">
|
||||
<!-- 404图标区域 -->
|
||||
<div class="error-illustration">
|
||||
<div class="error-code">
|
||||
<span class="digit">4</span>
|
||||
<div class="zero-container">
|
||||
<div class="zero-outer">
|
||||
<div class="zero-inner">
|
||||
<div class="sad-face">
|
||||
<div class="eye left-eye"></div>
|
||||
<div class="eye right-eye"></div>
|
||||
<div class="mouth"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="digit">4</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文字信息 -->
|
||||
<div class="error-info">
|
||||
<h1 class="error-title">页面未找到</h1>
|
||||
<p class="error-description">抱歉,您访问的页面不存在或已被移除</p>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions">
|
||||
<button class="action-btn primary-btn" @click="goHome">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 12L5 10M5 10L12 3L19 10M5 10V20C5 20.5523 5.44772 21 6 21H9M19 10L21 12M19 10V20C19 20.5523 18.5523 21 18 21H15M9 21C9.55228 21 10 20.5523 10 20V16C10 15.4477 10.4477 15 11 15H13C13.5523 15 14 15.4477 14 16V20C14 20.5523 14.4477 21 15 21M9 21H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
返回首页
|
||||
</button>
|
||||
<button class="action-btn secondary-btn" @click="goBack">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 12H5M12 19L5 12L12 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
返回上页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NotFound',
|
||||
methods: {
|
||||
goHome() {
|
||||
this.$router.push('/')
|
||||
},
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 主容器 */
|
||||
.not-found {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 背景装饰动画 */
|
||||
.background-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.floating-shape {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
top: 20%;
|
||||
left: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
top: 60%;
|
||||
right: 10%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.shape-3 {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
top: 30%;
|
||||
right: 25%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.shape-4 {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
bottom: 20%;
|
||||
left: 20%;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 内容容器 */
|
||||
.not-found-content {
|
||||
text-align: center;
|
||||
color: white;
|
||||
max-width: 600px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
animation: fadeInUp 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 404错误图标设计 */
|
||||
.error-illustration {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 120px;
|
||||
font-weight: 800;
|
||||
font-family: 'Arial', sans-serif;
|
||||
text-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.digit {
|
||||
color: #fff;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
.digit:first-child {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.digit:last-child {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 0字符设计(圆形带表情) */
|
||||
.zero-container {
|
||||
margin: 0 20px;
|
||||
animation: bounce 2s infinite;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.zero-outer {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 8px solid #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.zero-inner {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 表情设计 */
|
||||
.sad-face {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.eye {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
animation: blink 3s infinite;
|
||||
}
|
||||
|
||||
.left-eye {
|
||||
top: 18px;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.right-eye {
|
||||
top: 18px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.mouth {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 20px;
|
||||
height: 10px;
|
||||
border: 2px solid #fff;
|
||||
border-top: none;
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 90%, 100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
95% {
|
||||
transform: scaleY(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 文字信息 */
|
||||
.error-info {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
color: #fff;
|
||||
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 18px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.6;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 按钮设计 */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 28px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
min-width: 160px;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.error-code {
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.zero-outer {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.zero-inner {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.sad-face {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.eye {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.left-eye {
|
||||
top: 12px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.right-eye {
|
||||
top: 12px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.mouth {
|
||||
bottom: 10px;
|
||||
width: 15px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 200px;
|
||||
padding: 12px 24px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.floating-shape {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.not-found {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.zero-outer {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-width: 4px;
|
||||
}
|
||||
|
||||
.zero-inner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.sad-face {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.eye {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.left-eye {
|
||||
top: 8px;
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
.right-eye {
|
||||
top: 8px;
|
||||
right: 7px;
|
||||
}
|
||||
|
||||
.mouth {
|
||||
bottom: 6px;
|
||||
width: 12px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error-info {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1388
src/views/PredictRecords.vue
Normal file
2159
src/views/Profile.vue
Normal file
982
src/views/Register.vue
Normal file
@@ -0,0 +1,982 @@
|
||||
<template>
|
||||
<div class="register-page-container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h1 class="main-title">用户注册</h1>
|
||||
<p class="subtitle">您的专属彩票数据助理</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<el-card class="register-form-container" shadow="never">
|
||||
<form @submit.prevent="handleRegister" class="register-form">
|
||||
<!-- 账号 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入账号"
|
||||
:error="errors.username"
|
||||
prefix-icon="User"
|
||||
size="large"
|
||||
@blur="validateUsername"
|
||||
clearable
|
||||
/>
|
||||
<div v-if="errors.username" class="error-text">{{ errors.username }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 昵称 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.nickname"
|
||||
placeholder="请输入昵称"
|
||||
:error="errors.nickname"
|
||||
prefix-icon="Avatar"
|
||||
size="large"
|
||||
@blur="validateNickname"
|
||||
clearable
|
||||
/>
|
||||
<div v-if="errors.nickname" class="error-text">{{ errors.nickname }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 手机号和发送验证码按钮 -->
|
||||
<div class="form-group">
|
||||
<div class="phone-code-row">
|
||||
<el-input
|
||||
v-model="formData.phone"
|
||||
type="tel"
|
||||
placeholder="请输入手机号"
|
||||
:error="errors.phone"
|
||||
prefix-icon="Iphone"
|
||||
size="large"
|
||||
maxlength="11"
|
||||
@input="validatePhoneInput"
|
||||
@blur="validatePhoneOnBlur"
|
||||
clearable
|
||||
class="phone-input"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="codeBtnDisabled"
|
||||
@click="sendVerificationCode"
|
||||
class="send-code-btn-inline"
|
||||
size="large"
|
||||
>
|
||||
{{ codeButtonText }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="errors.phone" class="error-text">{{ errors.phone }}</div>
|
||||
<div v-else-if="formData.phone && formData.phone.length > 0 && formData.phone.length < 11" class="tip-text">请输入11位手机号码</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入验证码"
|
||||
prefix-icon="Key"
|
||||
size="large"
|
||||
maxlength="6"
|
||||
@blur="validateCode"
|
||||
/>
|
||||
<div v-if="errors.code" class="error-text">{{ errors.code }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.password"
|
||||
placeholder="请输入密码"
|
||||
:error="errors.password"
|
||||
prefix-icon="Lock"
|
||||
size="large"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:show-password="true"
|
||||
autocomplete="new-password"
|
||||
@blur="validatePassword"
|
||||
/>
|
||||
<div v-if="errors.password" class="error-text">{{ errors.password }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.confirmPassword"
|
||||
placeholder="请确认密码"
|
||||
:error="errors.confirmPassword"
|
||||
prefix-icon="Lock"
|
||||
size="large"
|
||||
:type="showConfirmPassword ? 'text' : 'password'"
|
||||
:show-password="true"
|
||||
autocomplete="new-password"
|
||||
@blur="validateConfirmPassword"
|
||||
/>
|
||||
<div v-if="errors.confirmPassword" class="error-text">{{ errors.confirmPassword }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务条款 -->
|
||||
<div class="form-group">
|
||||
<el-checkbox v-model="formData.agreeTerms" class="checkbox-custom">
|
||||
我已阅读并同意
|
||||
<a href="#" class="terms-link" @click.prevent="showTerms">《用户服务协议》</a>
|
||||
</el-checkbox>
|
||||
<div v-if="errors.agreeTerms" class="error-text">{{ errors.agreeTerms }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="loading"
|
||||
class="register-btn"
|
||||
size="large"
|
||||
>
|
||||
{{ loading ? '注册中...' : '立即注册' }}
|
||||
</el-button>
|
||||
|
||||
<!-- 登录链接 -->
|
||||
<div class="login-link">
|
||||
<span>已有账号?</span>
|
||||
<router-link to="/login" class="link">立即登录</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { userStore } from '../store/user'
|
||||
import { lotteryApi } from '../api/index.js'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElCard, ElInput, ElButton, ElCheckbox } from 'element-plus'
|
||||
import { User, Lock, Iphone, Key, Avatar } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'Register',
|
||||
components: {
|
||||
ElCard,
|
||||
ElInput,
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
User,
|
||||
Lock,
|
||||
Iphone,
|
||||
Key,
|
||||
Avatar
|
||||
},
|
||||
setup() {
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
return { toast, router }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPassword: false,
|
||||
showConfirmPassword: false,
|
||||
loading: false,
|
||||
|
||||
codeCountdown: 0,
|
||||
timer: null,
|
||||
saveTimer: null, // 用于防抖保存的定时器
|
||||
showPhoneError: false,
|
||||
phoneValid: false,
|
||||
|
||||
formData: {
|
||||
username: '',
|
||||
nickname: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
phone: '',
|
||||
code: '',
|
||||
agreeTerms: false
|
||||
},
|
||||
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
codeButtonText() {
|
||||
return this.codeCountdown > 0 ? `${this.codeCountdown}秒后重试` : '获取验证码';
|
||||
},
|
||||
codeBtnDisabled() {
|
||||
return this.codeCountdown > 0 || !this.isValidPhone(this.formData.phone);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 手机号格式验证
|
||||
isValidPhone(phone) {
|
||||
return /^1[3-9]\d{9}$/.test(phone);
|
||||
},
|
||||
|
||||
// 手机号输入时验证
|
||||
validatePhoneInput() {
|
||||
// 清除之前的错误
|
||||
this.errors.phone = '';
|
||||
this.showPhoneError = false;
|
||||
this.phoneValid = false;
|
||||
|
||||
const phone = this.formData.phone;
|
||||
|
||||
// 如果输入不是数字,替换非数字字符
|
||||
if (!/^\d*$/.test(phone)) {
|
||||
this.formData.phone = phone.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
// 如果长度达到11位,验证格式
|
||||
if (phone.length === 11) {
|
||||
if (this.isValidPhone(phone)) {
|
||||
this.phoneValid = true;
|
||||
} else {
|
||||
this.errors.phone = '手机号格式不正确';
|
||||
this.showPhoneError = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 手机号失焦时验证
|
||||
validatePhoneOnBlur() {
|
||||
const phone = this.formData.phone;
|
||||
|
||||
if (phone && phone.length > 0) {
|
||||
if (phone.length !== 11) {
|
||||
this.errors.phone = '手机号应为11位数字';
|
||||
this.showPhoneError = true;
|
||||
this.phoneValid = false;
|
||||
} else if (!this.isValidPhone(phone)) {
|
||||
this.errors.phone = '请输入正确的手机号码';
|
||||
this.showPhoneError = true;
|
||||
this.phoneValid = false;
|
||||
} else {
|
||||
this.phoneValid = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 发送验证码
|
||||
async sendVerificationCode() {
|
||||
if (!this.formData.phone) {
|
||||
this.errors.phone = '请输入手机号';
|
||||
this.showPhoneError = true;
|
||||
this.phoneValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidPhone(this.formData.phone)) {
|
||||
this.errors.phone = '请输入正确的手机号码';
|
||||
this.showPhoneError = true;
|
||||
this.phoneValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.phoneValid = true;
|
||||
|
||||
try {
|
||||
// 开始倒计时 (先启动倒计时,避免API延迟导致用户体验不佳)
|
||||
this.codeCountdown = 60;
|
||||
this.startCodeCountdown();
|
||||
|
||||
const response = await lotteryApi.sendSmsCode(this.formData.phone);
|
||||
|
||||
if (!response.success) {
|
||||
// 如果发送失败,停止倒计时
|
||||
this.codeCountdown = 0;
|
||||
clearInterval(this.timer);
|
||||
this.toast.error(response.message || '发送验证码失败,请稍后重试');
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果发送出错,停止倒计时
|
||||
this.codeCountdown = 0;
|
||||
clearInterval(this.timer);
|
||||
console.error('发送验证码失败:', error);
|
||||
this.toast.error('发送验证码失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
|
||||
// 开始倒计时
|
||||
startCodeCountdown() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
if (this.codeCountdown > 0) {
|
||||
this.codeCountdown--;
|
||||
} else {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
// 验证账号
|
||||
validateUsername() {
|
||||
if (!this.formData.username) {
|
||||
this.errors.username = '请输入账号';
|
||||
} else if (this.formData.username.length < 3 || this.formData.username.length > 20) {
|
||||
this.errors.username = '账号长度为3-20个字符';
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(this.formData.username)) {
|
||||
this.errors.username = '账号只能包含字母、数字和下划线';
|
||||
} else {
|
||||
delete this.errors.username;
|
||||
}
|
||||
},
|
||||
|
||||
// 验证昵称
|
||||
validateNickname() {
|
||||
if (!this.formData.nickname) {
|
||||
this.errors.nickname = '请输入昵称';
|
||||
} else if (this.formData.nickname.length < 2 || this.formData.nickname.length > 20) {
|
||||
this.errors.nickname = '昵称长度为2-20个字符';
|
||||
} else {
|
||||
delete this.errors.nickname;
|
||||
}
|
||||
},
|
||||
|
||||
// 验证验证码
|
||||
validateCode() {
|
||||
if (!this.formData.code) {
|
||||
this.errors.code = '请输入验证码';
|
||||
} else if (this.formData.code.length < 4 || this.formData.code.length > 6) {
|
||||
this.errors.code = '验证码格式不正确';
|
||||
} else {
|
||||
delete this.errors.code;
|
||||
}
|
||||
},
|
||||
|
||||
// 验证密码
|
||||
validatePassword() {
|
||||
if (!this.formData.password) {
|
||||
this.errors.password = '请输入密码';
|
||||
} else if (this.formData.password.length < 8 || this.formData.password.length > 20) {
|
||||
this.errors.password = '密码长度不小于8位';
|
||||
} else {
|
||||
delete this.errors.password;
|
||||
}
|
||||
// 如果确认密码有值,则重新校验
|
||||
if (this.formData.confirmPassword) {
|
||||
this.validateConfirmPassword();
|
||||
}
|
||||
},
|
||||
|
||||
// 验证确认密码
|
||||
validateConfirmPassword() {
|
||||
if (!this.formData.confirmPassword) {
|
||||
this.errors.confirmPassword = '请确认密码';
|
||||
} else if (this.formData.password !== this.formData.confirmPassword) {
|
||||
this.errors.confirmPassword = '两次输入的密码不一致';
|
||||
} else {
|
||||
delete this.errors.confirmPassword;
|
||||
}
|
||||
},
|
||||
|
||||
// 表单验证
|
||||
validateForm() {
|
||||
this.errors = {};
|
||||
|
||||
this.validateUsername();
|
||||
this.validateNickname();
|
||||
this.validatePhoneOnBlur();
|
||||
this.validateCode();
|
||||
this.validatePassword();
|
||||
this.validateConfirmPassword();
|
||||
|
||||
// 服务条款验证
|
||||
if (!this.formData.agreeTerms) {
|
||||
this.errors.agreeTerms = '请同意用户服务协议';
|
||||
}
|
||||
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
// 处理注册
|
||||
async handleRegister() {
|
||||
if (!this.validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
// 调用手机号注册接口
|
||||
const response = await lotteryApi.userPhoneRegister(
|
||||
this.formData.username,
|
||||
this.formData.password,
|
||||
this.formData.confirmPassword,
|
||||
this.formData.phone,
|
||||
this.formData.code,
|
||||
this.formData.nickname
|
||||
);
|
||||
|
||||
if (response.success === true) {
|
||||
// 注册成功
|
||||
this.clearSavedFormData(); // 清除保存的表单数据
|
||||
this.toast.success('注册成功!请登录您的账号');
|
||||
this.router.push('/login');
|
||||
} else {
|
||||
// 注册失败
|
||||
this.toast.error(response.message || '注册失败,请稍后重试');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error);
|
||||
if (error.response && error.response.data) {
|
||||
this.toast.error(error.response.data.message || '注册失败,请稍后重试');
|
||||
} else {
|
||||
this.toast.error('网络错误,请重试');
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 保存表单数据到临时存储(带防抖)
|
||||
saveFormData(immediate = false) {
|
||||
// 如果不是立即保存,使用防抖
|
||||
if (!immediate) {
|
||||
if (this.saveTimer) {
|
||||
clearTimeout(this.saveTimer);
|
||||
}
|
||||
this.saveTimer = setTimeout(() => {
|
||||
this.doSaveFormData();
|
||||
}, 1000); // 1秒防抖
|
||||
} else {
|
||||
this.doSaveFormData();
|
||||
}
|
||||
},
|
||||
|
||||
// 实际执行保存操作
|
||||
doSaveFormData() {
|
||||
const formData = {
|
||||
username: this.formData.username,
|
||||
nickname: this.formData.nickname,
|
||||
password: this.formData.password,
|
||||
confirmPassword: this.formData.confirmPassword,
|
||||
phone: this.formData.phone,
|
||||
code: this.formData.code,
|
||||
agreeTerms: this.formData.agreeTerms
|
||||
};
|
||||
sessionStorage.setItem('register_form_data', JSON.stringify(formData));
|
||||
},
|
||||
|
||||
// 从临时存储恢复表单数据
|
||||
restoreFormData() {
|
||||
const savedData = sessionStorage.getItem('register_form_data');
|
||||
if (savedData) {
|
||||
try {
|
||||
const formData = JSON.parse(savedData);
|
||||
this.formData = { ...this.formData, ...formData };
|
||||
// 恢复手机号验证状态
|
||||
if (formData.phone && this.isValidPhone(formData.phone)) {
|
||||
this.phoneValid = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('恢复表单数据失败:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 清除临时存储的表单数据
|
||||
clearSavedFormData() {
|
||||
sessionStorage.removeItem('register_form_data');
|
||||
},
|
||||
|
||||
// 显示服务条款
|
||||
showTerms() {
|
||||
// 跳转前立即保存当前表单数据
|
||||
this.saveFormData(true);
|
||||
this.$router.push('/user-agreement');
|
||||
}
|
||||
},
|
||||
// 监听表单数据变化,自动保存
|
||||
watch: {
|
||||
'formData': {
|
||||
handler(newFormData) {
|
||||
// 只有当表单有实际内容时才保存,避免保存空数据
|
||||
if (newFormData.username || newFormData.nickname || newFormData.phone ||
|
||||
newFormData.password || newFormData.code) {
|
||||
this.saveFormData();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
// 延迟保存,避免频繁操作
|
||||
immediate: false
|
||||
}
|
||||
},
|
||||
// 组件挂载时恢复表单数据
|
||||
mounted() {
|
||||
this.restoreFormData();
|
||||
},
|
||||
// 组件销毁时清除定时器
|
||||
beforeUnmount() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
// 清除保存定时器
|
||||
if (this.saveTimer) {
|
||||
clearTimeout(this.saveTimer);
|
||||
this.saveTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 注册页面容器 */
|
||||
.register-page-container {
|
||||
min-height: calc(100vh - 70px);
|
||||
background: #f0f2f5;
|
||||
padding: 20px 20px 8px 20px;
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
|
||||
color: white;
|
||||
padding: 35px 20px 25px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(238, 90, 82, 0.3);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 32px;
|
||||
margin: 0 auto 4px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-shadow: 0 2px 8px rgba(0,0,0,0.5), 0 0 20px rgba(0,0,0,0.3);
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
color: white;
|
||||
opacity: 0.95;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.4);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 桌面端样式 */
|
||||
@media (min-width: 1024px) {
|
||||
.page-header {
|
||||
padding: 30px 20px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.register-form-container {
|
||||
padding: 0;
|
||||
background: white;
|
||||
margin: 0 0 20px 0;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.register-form {
|
||||
background: white;
|
||||
border-radius: 0;
|
||||
padding: 28px 24px 20px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 表单组 */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
background: #f8f9fa;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 56px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.input-wrapper:focus-within {
|
||||
border-color: #e9ecef;
|
||||
background: white;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-wrapper.error {
|
||||
border-color: #dc3545;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.input-wrapper.success {
|
||||
border-color: #4caf50;
|
||||
background: #f8fff8;
|
||||
}
|
||||
|
||||
.validation-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
|
||||
.input-icon {
|
||||
padding: 12px 25px;
|
||||
color: #6c757d;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.icon-img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
padding: 16px 8px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: #212529;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 控制浏览器自动填充的样式 */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px white inset !important;
|
||||
-webkit-text-fill-color: #212529 !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* 手机号和验证码按钮同行布局 */
|
||||
.phone-code-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.phone-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 内联发送验证码按钮样式 */
|
||||
.send-code-btn-inline {
|
||||
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120px;
|
||||
flex-shrink: 0;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.send-code-btn-inline:hover:not(.is-disabled) {
|
||||
background: linear-gradient(135deg, #d43030, #ff5a5a);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 15px rgba(229, 62, 62, 0.3);
|
||||
}
|
||||
|
||||
.send-code-btn-inline:active:not(.is-disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.send-code-btn-inline.is-disabled {
|
||||
background: #cccccc !important;
|
||||
border-color: #cccccc !important;
|
||||
color: #888 !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* 隐藏浏览器自带的密码控件 */
|
||||
input::-ms-reveal,
|
||||
input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input::-webkit-credentials-auto-fill-button {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toggle-icon-img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #ff4444;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
color: #888888;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 复选框 */
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.checkbox-wrapper input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: transparent;
|
||||
transition: all 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox-wrapper input:checked + .checkmark {
|
||||
background: #e53e3e;
|
||||
border-color: #e53e3e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.terms-link {
|
||||
color: #e53e3e;
|
||||
text-decoration: none;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.terms-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 注册按钮 */
|
||||
.register-btn {
|
||||
width: 100%;
|
||||
margin: 8px 0 24px 0;
|
||||
padding: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
height: 52px;
|
||||
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(229, 62, 62, 0.25);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.register-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 30px rgba(229, 62, 62, 0.35);
|
||||
background: linear-gradient(135deg, #d43030, #ff5a5a);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.register-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(229, 62, 62, 0.3);
|
||||
}
|
||||
|
||||
/* Element UI 组件自定义样式 */
|
||||
:deep(.el-input__wrapper) {
|
||||
padding: 6px 16px;
|
||||
box-shadow: none !important;
|
||||
background-color: #f8f9fa;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper.is-focus) {
|
||||
background-color: #fff;
|
||||
border-color: #e53e3e;
|
||||
box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1);
|
||||
}
|
||||
|
||||
:deep(.el-input__prefix) {
|
||||
margin-right: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 44px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: #e53e3e;
|
||||
border-color: #e53e3e;
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-button.is-disabled) {
|
||||
background: #cccccc;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
|
||||
/* 处理用户协议链接 */
|
||||
.checkbox-custom :deep(.el-checkbox__label) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 登录链接 */
|
||||
.login-link {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.login-link .link {
|
||||
color: #e53e3e;
|
||||
text-decoration: none;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.login-link .link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.register-page-container {
|
||||
padding: 10px 10px 5px 10px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 30px 15px 25px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.register-page-container {
|
||||
padding: 5px 5px 3px 5px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 30px 20px 25px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 28px;
|
||||
margin-bottom: 3px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.register-form {
|
||||
padding: 24px 20px 18px;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
height: 48px;
|
||||
font-size: 15px;
|
||||
margin: 8px 0 20px 0;
|
||||
}
|
||||
|
||||
.send-code-btn-inline {
|
||||
font-size: 12px;
|
||||
height: 42px;
|
||||
min-width: 90px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.phone-code-row {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 桌面端样式 - 这部分已在上面定义,这里移除重复定义 */
|
||||
</style>
|
||||
743
src/views/ResetPassword.vue
Normal file
@@ -0,0 +1,743 @@
|
||||
<template>
|
||||
<div class="reset-password-page-container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h1 class="main-title">找回密码</h1>
|
||||
<p class="subtitle">重置您的登录密码</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 找回密码表单 -->
|
||||
<div class="reset-password-form-container">
|
||||
<form @submit.prevent="handleResetPassword" class="reset-password-form">
|
||||
<!-- 手机号 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.phone"
|
||||
type="tel"
|
||||
placeholder="请输入手机号"
|
||||
prefix-icon="Iphone"
|
||||
size="large"
|
||||
maxlength="11"
|
||||
@input="validatePhoneInput"
|
||||
@blur="validatePhoneOnBlur"
|
||||
clearable
|
||||
/>
|
||||
<div v-if="errors.phone" class="error-text">{{ errors.phone }}</div>
|
||||
<div v-else-if="formData.phone && formData.phone.length > 0 && formData.phone.length < 11" class="tip-text">请输入11位手机号码</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入验证码"
|
||||
prefix-icon="Key"
|
||||
size="large"
|
||||
maxlength="6"
|
||||
>
|
||||
<template #append>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="codeBtnDisabled"
|
||||
@click="sendVerificationCode"
|
||||
>
|
||||
{{ codeButtonText }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<div v-if="errors.code" class="error-text">{{ errors.code }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 新密码 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.newPassword"
|
||||
placeholder="请输入新密码"
|
||||
prefix-icon="Lock"
|
||||
size="large"
|
||||
:type="showNewPassword ? 'text' : 'password'"
|
||||
:show-password="true"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<div v-if="errors.newPassword" class="error-text">{{ errors.newPassword }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认新密码 -->
|
||||
<div class="form-group">
|
||||
<el-input
|
||||
v-model="formData.confirmPassword"
|
||||
placeholder="请再次输入新密码"
|
||||
prefix-icon="Lock"
|
||||
size="large"
|
||||
:type="showConfirmPassword ? 'text' : 'password'"
|
||||
:show-password="true"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<div v-if="errors.confirmPassword" class="error-text">{{ errors.confirmPassword }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 重置密码按钮 -->
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="loading"
|
||||
class="reset-password-btn"
|
||||
size="large"
|
||||
>
|
||||
{{ loading ? '重置中...' : '重置密码' }}
|
||||
</el-button>
|
||||
|
||||
<!-- 返回登录链接 -->
|
||||
<div class="login-link">
|
||||
<span><router-link to="/login" class="link">返回登录</router-link></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 成功/错误弹窗 -->
|
||||
<div v-if="showModal" class="modal-overlay" @click="closeModal">
|
||||
<div class="modal-content" :class="{ 'success-modal': isSuccess, 'error-modal': !isSuccess }" @click.stop>
|
||||
<div v-if="!isSuccess" class="error-icon">❌</div>
|
||||
<h3>{{ modalTitle }}</h3>
|
||||
<p>{{ modalMessage }}</p>
|
||||
<button class="modal-btn" :class="{ 'error-btn': !isSuccess }" @click="closeModal">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { lotteryApi } from '../api/index.js'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElInput, ElButton } from 'element-plus'
|
||||
import { Lock, Iphone, Key } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'ResetPassword',
|
||||
components: {
|
||||
ElInput,
|
||||
ElButton,
|
||||
Lock,
|
||||
Iphone,
|
||||
Key
|
||||
},
|
||||
setup() {
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
return { toast, router }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showNewPassword: false,
|
||||
showConfirmPassword: false,
|
||||
codeCountdown: 0,
|
||||
timer: null,
|
||||
phoneValid: false,
|
||||
showModal: false,
|
||||
isSuccess: false,
|
||||
modalTitle: '',
|
||||
modalMessage: '',
|
||||
|
||||
formData: {
|
||||
phone: '',
|
||||
code: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
codeButtonText() {
|
||||
return this.codeCountdown > 0 ? `${this.codeCountdown}秒后重试` : '获取验证码';
|
||||
},
|
||||
codeBtnDisabled() {
|
||||
return this.codeCountdown > 0 || !this.isValidPhone(this.formData.phone);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 手机号格式验证
|
||||
isValidPhone(phone) {
|
||||
return /^1[3-9]\d{9}$/.test(phone);
|
||||
},
|
||||
|
||||
// 手机号输入时验证
|
||||
validatePhoneInput() {
|
||||
this.errors.phone = '';
|
||||
this.phoneValid = false;
|
||||
|
||||
const phone = this.formData.phone;
|
||||
|
||||
if (!/^\d*$/.test(phone)) {
|
||||
this.formData.phone = phone.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
if (phone.length === 11) {
|
||||
if (this.isValidPhone(phone)) {
|
||||
this.phoneValid = true;
|
||||
} else {
|
||||
this.errors.phone = '手机号格式不正确';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 手机号失焦时验证
|
||||
validatePhoneOnBlur() {
|
||||
const phone = this.formData.phone;
|
||||
|
||||
if (phone && phone.length > 0) {
|
||||
if (phone.length !== 11) {
|
||||
this.errors.phone = '手机号应为11位数字';
|
||||
this.phoneValid = false;
|
||||
} else if (!this.isValidPhone(phone)) {
|
||||
this.errors.phone = '请输入正确的手机号码';
|
||||
this.phoneValid = false;
|
||||
} else {
|
||||
this.phoneValid = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 发送验证码
|
||||
async sendVerificationCode() {
|
||||
if (!this.formData.phone) {
|
||||
this.errors.phone = '请输入手机号';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidPhone(this.formData.phone)) {
|
||||
this.errors.phone = '请输入正确的手机号码';
|
||||
return;
|
||||
}
|
||||
|
||||
this.phoneValid = true;
|
||||
|
||||
try {
|
||||
this.codeCountdown = 60;
|
||||
this.startCodeCountdown();
|
||||
|
||||
const response = await lotteryApi.sendSmsCode(this.formData.phone);
|
||||
|
||||
if (response.success) {
|
||||
this.toast.success('验证码已发送,请注意查收');
|
||||
} else {
|
||||
this.codeCountdown = 0;
|
||||
clearInterval(this.timer);
|
||||
this.toast.error(response.message || '发送验证码失败,请稍后重试');
|
||||
}
|
||||
} catch (error) {
|
||||
this.codeCountdown = 0;
|
||||
clearInterval(this.timer);
|
||||
console.error('发送验证码失败:', error);
|
||||
this.toast.error('发送验证码失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
|
||||
// 开始倒计时
|
||||
startCodeCountdown() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
if (this.codeCountdown > 0) {
|
||||
this.codeCountdown--;
|
||||
} else {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
// 表单验证
|
||||
validateForm() {
|
||||
this.errors = {};
|
||||
|
||||
if (!this.formData.phone) {
|
||||
this.errors.phone = '请输入手机号';
|
||||
} else if (!this.isValidPhone(this.formData.phone)) {
|
||||
this.errors.phone = '请输入正确的手机号码';
|
||||
}
|
||||
|
||||
if (!this.formData.code) {
|
||||
this.errors.code = '请输入验证码';
|
||||
} else if (this.formData.code.length !== 6 && this.formData.code.length !== 4) {
|
||||
this.errors.code = '验证码格式不正确';
|
||||
}
|
||||
|
||||
if (!this.formData.newPassword) {
|
||||
this.errors.newPassword = '请输入新密码';
|
||||
} else if (this.formData.newPassword.length < 6 || this.formData.newPassword.length > 20) {
|
||||
this.errors.newPassword = '密码长度在 6 到 20 个字符';
|
||||
}
|
||||
|
||||
if (!this.formData.confirmPassword) {
|
||||
this.errors.confirmPassword = '请再次输入新密码';
|
||||
} else if (this.formData.newPassword !== this.formData.confirmPassword) {
|
||||
this.errors.confirmPassword = '两次输入的密码不一致';
|
||||
}
|
||||
|
||||
return Object.keys(this.errors).length === 0;
|
||||
},
|
||||
|
||||
// 处理重置密码
|
||||
async handleResetPassword() {
|
||||
if (!this.validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const response = await lotteryApi.resetPassword(
|
||||
this.formData.phone,
|
||||
this.formData.code,
|
||||
this.formData.newPassword,
|
||||
this.formData.confirmPassword
|
||||
);
|
||||
|
||||
if (response.success === true) {
|
||||
this.showModal = true;
|
||||
this.isSuccess = true;
|
||||
this.modalTitle = '密码重置成功';
|
||||
this.modalMessage = '您的密码已成功重置,请返回登录页面。';
|
||||
// 重置表单
|
||||
this.formData.phone = '';
|
||||
this.formData.code = '';
|
||||
this.formData.newPassword = '';
|
||||
this.formData.confirmPassword = '';
|
||||
this.errors = {};
|
||||
} else {
|
||||
this.showModal = true;
|
||||
this.isSuccess = false;
|
||||
this.modalTitle = '密码重置失败';
|
||||
this.modalMessage = response.message || '重置密码失败,请重试。';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error);
|
||||
this.showModal = true;
|
||||
this.isSuccess = false;
|
||||
this.modalTitle = '操作失败';
|
||||
this.modalMessage = error.response?.data?.message || '网络错误或服务器异常,请稍后重试。';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 关闭弹窗
|
||||
closeModal() {
|
||||
this.showModal = false;
|
||||
if (this.isSuccess) {
|
||||
this.router.push('/login');
|
||||
}
|
||||
this.modalTitle = '';
|
||||
this.modalMessage = '';
|
||||
}
|
||||
},
|
||||
// 组件销毁时清除定时器
|
||||
beforeUnmount() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面容器 */
|
||||
.reset-password-page-container {
|
||||
min-height: calc(100vh - 70px);
|
||||
background: #f0f2f5;
|
||||
padding: 20px 20px 8px 20px;
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
|
||||
color: white;
|
||||
padding: 35px 20px 25px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(238, 90, 82, 0.3);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 32px;
|
||||
margin: 0 auto 4px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-shadow: 0 2px 8px rgba(0,0,0,0.5), 0 0 20px rgba(0,0,0,0.3);
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
color: white;
|
||||
opacity: 0.95;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.4);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.reset-password-form-container {
|
||||
padding: 0;
|
||||
background: white;
|
||||
margin: 0 0 20px 0;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reset-password-form {
|
||||
background: white;
|
||||
border-radius: 0;
|
||||
padding: 32px 24px 24px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 表单组 */
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Element UI 组件自定义样式 */
|
||||
:deep(.el-input__wrapper) {
|
||||
padding: 6px 16px;
|
||||
box-shadow: none !important;
|
||||
background-color: #f8f9fa;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 带有 append 按钮的输入框样式优化 */
|
||||
:deep(.el-input-group .el-input__wrapper) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append) {
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper.is-focus) {
|
||||
background-color: #fff;
|
||||
border-color: #e53e3e;
|
||||
box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1);
|
||||
}
|
||||
|
||||
/* 带有 append 按钮的输入框聚焦时的样式 */
|
||||
:deep(.el-input-group .el-input__wrapper.is-focus) {
|
||||
border-color: #e53e3e;
|
||||
box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1);
|
||||
}
|
||||
|
||||
:deep(.el-input-group.is-focus .el-input-group__append .el-button) {
|
||||
border-color: #e53e3e;
|
||||
box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1);
|
||||
}
|
||||
|
||||
:deep(.el-input__prefix) {
|
||||
margin-right: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 44px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append .el-button) {
|
||||
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
|
||||
border: 2px solid #e53e3e;
|
||||
border-left: none;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
min-width: 100px;
|
||||
box-shadow: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append .el-button:hover) {
|
||||
background: linear-gradient(135deg, #d43030, #ff5a5a);
|
||||
border-color: #d43030;
|
||||
color: white;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append .el-button.is-disabled) {
|
||||
background: #cccccc;
|
||||
border-color: #cccccc;
|
||||
color: #888;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 提示文本 */
|
||||
.error-text {
|
||||
color: #ff4444;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
color: #888888;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 重置密码按钮 */
|
||||
.reset-password-btn {
|
||||
width: 100%;
|
||||
margin: 32px 0 28px 0;
|
||||
padding: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
height: 52px;
|
||||
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(229, 62, 62, 0.25);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.reset-password-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 30px rgba(229, 62, 62, 0.35);
|
||||
background: linear-gradient(135deg, #d43030, #ff5a5a);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.reset-password-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(229, 62, 62, 0.3);
|
||||
}
|
||||
|
||||
:deep(.el-button.is-disabled) {
|
||||
background: #cccccc;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
|
||||
/* 返回登录链接 */
|
||||
.login-link {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.login-link .link {
|
||||
color: #e53e3e;
|
||||
text-decoration: none;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.login-link .link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 模态框 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px 30px;
|
||||
text-align: center;
|
||||
max-width: 350px;
|
||||
width: 100%;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.success-modal h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #4caf50;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.success-modal p {
|
||||
margin-bottom: 25px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
background: linear-gradient(135deg, #4caf50, #66bb6a);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.modal-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.modal-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
animation: shake 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(5px); }
|
||||
}
|
||||
|
||||
.error-modal h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #f44336;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-modal p {
|
||||
margin-bottom: 25px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error-btn {
|
||||
background: linear-gradient(135deg, #f44336, #e57373) !important;
|
||||
}
|
||||
|
||||
.error-btn:hover {
|
||||
box-shadow: 0 8px 25px rgba(244, 67, 54, 0.3) !important;
|
||||
}
|
||||
|
||||
/* 桌面端样式 */
|
||||
@media (min-width: 1024px) {
|
||||
.page-header {
|
||||
padding: 30px 20px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.reset-password-page-container {
|
||||
padding: 10px 10px 5px 10px;
|
||||
}
|
||||
|
||||
.reset-password-form {
|
||||
padding: 25px 20px;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append .el-button) {
|
||||
font-size: 12px;
|
||||
min-width: 90px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.reset-password-page-container {
|
||||
padding: 5px 5px 3px 5px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 30px 20px 25px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 28px;
|
||||
margin-bottom: 3px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.reset-password-form {
|
||||
padding: 28px 20px 20px;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.reset-password-btn {
|
||||
height: 48px;
|
||||
font-size: 15px;
|
||||
margin: 28px 0 24px 0;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append .el-button) {
|
||||
font-size: 12px;
|
||||
min-width: 90px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
196
src/views/UserAgreement.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="user-agreement-page">
|
||||
<el-page-header @back="goBack" content="用户协议">
|
||||
<template #title>
|
||||
<span>返回</span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
<el-card class="agreement-content-card">
|
||||
<div class="agreement-content">
|
||||
<h4>欢迎使用《精彩猪手》数据服务!</h4>
|
||||
<p>  在开始使用我们的服务之前,请您(用户)仔细阅读并充分理解本《用户服务协议》(以下简称 "本协议")的全部内容。本协议是您(用户)与西安精彩数据服务社之间关于使用本服务的法律协议,一旦您(用户)使用本服务,即表示您(用户)已同意接受本协议的约束。如果您(用户)不同意本协议的任何条款,请不要使用本服务。</p>
|
||||
|
||||
<h5>一、定义</h5>
|
||||
<p>1、本服务:指我们通过网站、应用程序或其他相关平台向您提供的《精彩猪手》数据服务,包括但不限于信息发布、数据存储、在线交流等。</p>
|
||||
<p>2、用户:指承认本协议,接受本服务的自然人、法人或其他组织。具体包含付费账号用户和体验账号用户。</p>
|
||||
<p>3、个人信息:以电子或者其他方式记录的与已识别或者可识别的自然人有关的各种信息,不包括匿名化处理后的信息。</p>
|
||||
<p>4、知识产权:包括但不限于著作权、专利权、商标权、商业秘密等。</p>
|
||||
|
||||
<h5>二、服务内容及门槛</h5>
|
||||
<p>1、我们将尽力为您(用户)提供稳定、高效的服务。服务内容包括但不限于:</p>
|
||||
<p class="indent">1.1既定彩票数据查询浏览;</p>
|
||||
<p class="indent">1.2既定彩票数据分析报告;</p>
|
||||
<p class="indent">1.3个性化开奖号码辅助推导、推测;</p>
|
||||
<p class="indent">1.4推测记录统计、备份;</p>
|
||||
<p class="indent">1.5精彩猪手AI互动交流服务。</p>
|
||||
<p>2、我们会根据实际情况对服务内容进行调整、更新或终止。如有重大变更,我们将通过企业公众号通知您(用户),需要您(用户)及时接收。如您(用户)在服务内容发生变更后继续使用本服务,视为您(用户)接受变更后的协议内容。</p>
|
||||
<p>3、您(用户)理解并同意,使用本服务可能需要您(用户)具备相应的设备、软件、网络环境和一定的专业知识,相关费用由您(用户)自行承担。</p>
|
||||
|
||||
<h5>三、用户账号</h5>
|
||||
<p>1、您(用户)可以通过注册或使用第三方账号登录的方式获取本服务账号。在注册过程中,您(用户)需提供真实、准确、完整的信息,并确保信息的及时更新。</p>
|
||||
<p>2、您(用户)应妥善保管账号及密码,不得将账号出借、转让、赠与或共享给他人使用。因您(用户)自身原因导致账号泄露或被他人使用的后果,由您(用户)自行承担。</p>
|
||||
<p>3、若(用户)您发现账号存在异常或安全问题,请立即通知我们,我们将尽力协助处理,但对于非因我们原因导致的损失,我们不承担责任。</p>
|
||||
<p>4、您(用户)在使用账号过程中应遵守法律法规及本协议约定,不得利用账号从事违法违规或损害他人权益的行为,否则我们有权暂停或终止您(用户)的账号使用,由此造成的损失您(用户)自行承担。</p>
|
||||
|
||||
<h5>四、用户权利与义务</h5>
|
||||
<p>1、权利</p>
|
||||
<p class="indent">1.1有权在遵守本协议的前提下,按照我们提供的方式使用服务。</p>
|
||||
<p class="indent">1.2对我们提供的服务质量有权提出合理的意见和建议。</p>
|
||||
<p>2、义务</p>
|
||||
<p class="indent">2.1遵守国家法律法规及互联网相关规定,不得利用本服务从事违法犯罪活动。</p>
|
||||
<p class="indent">2.2不得干扰、破坏本服务的正常运行,不得对服务进行反向工程、反编译、反汇编等行为。</p>
|
||||
<p class="indent">2.3不得发布、传播任何侵犯他人知识产权、隐私或其他合法权益的信息。</p>
|
||||
<p class="indent">2.4不得恶意注册账号、发送垃圾信息或进行其他滥用服务的行为。</p>
|
||||
<p class="indent">2.5如因您(用户)的行为导致我们或第三方遭受损失,您(用户)应承担相应的赔偿责任。</p>
|
||||
|
||||
<h5>五、隐私政策</h5>
|
||||
<p>1、我们十分重视对您(用户)个人信息的保护,将谨慎收集、安全存储、妥善使用您(用户)的个人信息。</p>
|
||||
<p>2、您(用户)同意我们为提供服务、改进服务,包括遵守法律法规的需要,对您(用户)的个人信息进行合理的调用,过程中我们将采取合理措施确保信息安全。</p>
|
||||
|
||||
<h5>六、知识产权</h5>
|
||||
<p>我们对本服务及相关内容(包括但不限于软件、文字、图片、音频、视频等)享有知识产权。未经我们书面许可,您(用户)不得擅自复制、改编,或创造基于本服务的衍生品。</p>
|
||||
|
||||
<h5>七、责任限制与免责</h5>
|
||||
<p>1、本服务所提供的数据分析报告,均为根据彩票历史数据、统计学和数学原理,通过计算技术进行研究和建模而得出。我们可以对数据的及时性、客观性承担责任,但对任何经过主观干预之后而产生的数据结果,无法承当相应的后果。</p>
|
||||
<p>2、您(用户)应当理解并知晓,彩票娱乐本身就是概率游戏,彩票开奖也是最为典型的随机事件。本服务仅具备参考功能,会助您(用户)有限地缩小关注重点。您(用户)必须对自己的选择和决策承担最终责任。</p>
|
||||
<p>3、我们将尽力确保服务的正常运行,但由于互联网的复杂性和不确定性,可能会出现服务中断、延迟、错误等情况。对于因不可抗力、技术故障、网络攻击等不可预见、不可避免的原因导致的服务问题,我们不承担责任。</p>
|
||||
<p>4、我们对您(用户)通过本服务获取的第三方信息的准确性、完整性、可靠性不承担保证责任,您应自行判断并承担使用风险。</p>
|
||||
<p>5、在任何情况下,我们对您(用户)因使用本服务而产生的直接、间接、偶然、特殊或后果性损失(包括但不限于数据丢失、业务中断、利润损失等),无论基于合同、侵权或其他理论,均不承担超过您(用户)实际支付的服务费用的赔偿责任。</p>
|
||||
|
||||
<h5>八、收费及其规则</h5>
|
||||
<p>1、本服务采用会员制运营模式。所有用户分为会员与非会员两种类型。</p>
|
||||
<p>2、本服务对非会员用户提供1次为期连续10天的免费体验服务。免费时限结束后未转入会员的用户,将无法继续获得本服务。而且,以后该非会员用户将再无免费体验服务的机会。</p>
|
||||
<p>3、本服务对会员用户实行付费服务。付费标准分为包月付费和包年付费两种。</p>
|
||||
<p>4、包月付费每月10元,不足一个月时,按一个月计算。包年付费每年100元,期间任何一方提出终止协议和退费要求时,所退款项均按包月付费标准进行折算,四舍五入。</p>
|
||||
<p>5、出现业务退款情形时,所退款项只限原路退回付款账号。</p>
|
||||
<p>6、所有用户在服务有效期内使用本服务的时段和频次均不受限。</p>
|
||||
<p>7、用户可以根据自己的实际需要,随时选择成为会员或非会员。</p>
|
||||
|
||||
<h5>九、协议变更与终止</h5>
|
||||
<p>1、我们有权根据法律法规变化、业务发展需要等对本协议进行变更。变更后的协议将在相关平台公布,公布后即视为已通知您(用户)。若您(用户)在协议变更后继续使用服务,视为您(用户)接受变更后的协议;若您(用户)不同意变更,有权停止使用本服务。</p>
|
||||
<p>2、出现以下情况下,我们有权终止本协议及停止您(用户)继续使用服务:</p>
|
||||
<p class="indent">2.1您(用户)严重违反本协议约定。</p>
|
||||
<p class="indent">2.2法律法规要求我们终止服务。</p>
|
||||
<p class="indent">2.3因不可抗力等不可预见、不可避免的原因导致服务无法继续提供。</p>
|
||||
<p>2.4协议终止后,我们有权根据法律法规要求,对您(用户)的相关信息进行处理。</p>
|
||||
|
||||
<h5>十、争议解决</h5>
|
||||
<p>1、本协议的签订、履行、解释及争议解决均适用 《中华人民共和国民法典》。</p>
|
||||
<p>2、如双方在本协议履行过程中发生争议,应首先通过友好协商解决;协商不成的,任何一方均有权向有管辖权的人民法院提起诉讼。</p>
|
||||
|
||||
<h5>十一、其他条款</h5>
|
||||
<p>1、本协议构成您(用户)与我们之间关于本服务的完整协议。未经我们书面同意,您(用户)不得转让本协议项下的任何权利、利益和义务。</p>
|
||||
<p>2、本协议各条款的标题仅为方便阅读而设,不影响条款的具体含义及解释。</p>
|
||||
<p>3、若本协议任何条款被认定为无效或不可执行,不影响其他条款的效力及执行。</p>
|
||||
<p>4、我们未行使或执行本协议任何权利或条款,不构成对该权利或条款的放弃。</p>
|
||||
<p>5、本协议自您(用户)成功注册精彩猪手服务相关账号之日即刻生效。</p>
|
||||
<p>6、任何有关本协议项下服务的问题,您(用户)可通过本企业微信号进行咨询。</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElPageHeader, ElCard } from 'element-plus'
|
||||
|
||||
export default {
|
||||
name: 'UserAgreement',
|
||||
components: {
|
||||
ElPageHeader,
|
||||
ElCard
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-agreement-page {
|
||||
padding: 10px;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
/* 自定义返回按钮样式 */
|
||||
:deep(.el-page-header__header) {
|
||||
background: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-page-header__back) {
|
||||
color: #409EFF !important;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-page-header__back:hover) {
|
||||
color: #66b1ff !important;
|
||||
}
|
||||
|
||||
:deep(.el-page-header__content) {
|
||||
color: #333 !important;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.agreement-content-card {
|
||||
margin-top: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.agreement-content-card h4 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.agreement-content-card h5 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.agreement-content {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.agreement-content h4 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agreement-content h5 {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-left: 3px solid #409EFF;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.agreement-content p {
|
||||
line-height: 1.8;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.agreement-content p.indent {
|
||||
text-indent: 2em;
|
||||
margin-left: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.agreement-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
727
src/views/UserGuide.vue
Normal file
@@ -0,0 +1,727 @@
|
||||
<template>
|
||||
<div class="user-guide-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header-modern">
|
||||
<div class="header-content">
|
||||
<button class="back-btn" @click="goBack">
|
||||
<svg viewBox="0 0 24 24" class="back-icon">
|
||||
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="header-info">
|
||||
<div class="header-text">
|
||||
<h1 class="header-title">使用指南</h1>
|
||||
<p class="header-subtitle">《精彩猪手》完整使用说明</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="guide-content">
|
||||
<!-- 标题 -->
|
||||
<div class="guide-title-section">
|
||||
<h2 class="main-title">《精彩猪手》使用指南</h2>
|
||||
<p class="intro-text">
|
||||
《精彩猪手》是一款彩票数据姿态逻辑专业分析工具,是广大彩民用数据指向、凭理性娱彩的良师益友。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 目录导航 -->
|
||||
<div class="toc-section">
|
||||
<h3 class="toc-title">目录</h3>
|
||||
<div class="toc-list">
|
||||
<a href="#section1" class="toc-item">一、功能介绍</a>
|
||||
<a href="#section2" class="toc-item">二、专有名词解释</a>
|
||||
<a href="#section3" class="toc-item">三、注册、登录与使用</a>
|
||||
<a href="#section4" class="toc-item">四、双色球智推版操作要点</a>
|
||||
<a href="#section5" class="toc-item">五、双色球精推版操作要点</a>
|
||||
<a href="#section6" class="toc-item">六、大乐透智推版操作要点</a>
|
||||
<a href="#section7" class="toc-item">七、大乐透精推版操作要点</a>
|
||||
<a href="#section8" class="toc-item">八、活跃性分析数据调用</a>
|
||||
<a href="#section9" class="toc-item">九、接续性分析数据调用</a>
|
||||
<a href="#section10" class="toc-item">十、组合性分析数据调用</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第一章 -->
|
||||
<div id="section1" class="guide-section">
|
||||
<h3 class="section-title">一、《精彩猪手》功能介绍</h3>
|
||||
<div class="section-content">
|
||||
<p>《精彩猪手》由西安溢彩数智科技有限公司研创开发,主要围绕国内乐透型、数字型、基诺型主流彩票,提供相关基于姿态逻辑模型化的数据分析,以及"下期"开奖号球推测辅助服务。具体内容如下:</p>
|
||||
<ol class="feature-list">
|
||||
<li><strong>开奖信息查询</strong>:包括按开奖期号查询、按开奖日期查询、批量查询和表相查询等;</li>
|
||||
<li><strong>开奖数据分析</strong>:所有的开奖数据通过姿态逻辑模型化分析,多维度展现号球的活跃性、组合性和接续性特征,可以为彩民感知号球态势、判断变化趋势、复式/胆拖投注,提供参考依据。</li>
|
||||
<li><strong>开奖推测辅助</strong>:就是设定彩票号球接续、组合的逻辑规划,依据"本期"已出的号球,来对"下期"待出的号球进行多重数据运算,再提出系数指标从优至次的号球推荐队列,供彩民选择中参考。</li>
|
||||
<li>根据首球与随球的关联线索,启发复式/胆拖的投注参考;</li>
|
||||
<li>推测行为统计、分析、备份;</li>
|
||||
<li>进行规律总结、技巧探索、经验交流。</li>
|
||||
</ol>
|
||||
<p class="highlight-text">《精彩猪手》立志成为广大彩民值得信赖的超级数据助理。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二章 -->
|
||||
<div id="section2" class="guide-section">
|
||||
<h3 class="section-title">二、《精彩猪手》专有名词解释</h3>
|
||||
<div class="section-content">
|
||||
<p>《精彩猪手》开创了透视彩票开奖数据的新视角,以及推测彩票号组的新逻辑,因此注入了诸多的新概念,需要进行重点的理解和把握:</p>
|
||||
<div class="term-list">
|
||||
<div class="term-item">
|
||||
<span class="term-number">1</span>
|
||||
<div class="term-content">
|
||||
<strong>首球</strong>:是《精彩猪手》分段推测法的一个重要概念。对于数字型彩票,其开奖时第1个出场的号球便是首球。但对于乐透型和基诺型彩票而言,相同属性的号球并无关乎其先后的出场顺序,所以,"首球"可以是它们其中的任何一个球。首球从一定意义上具有胆码的部分功效,可以作为重点观察对象。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">2</span>
|
||||
<div class="term-content">
|
||||
<strong>随球</strong>:是《精彩猪手》分段推测法的另一个重要概念,与"首球"概念共存。对于乐透型、基诺型彩票而言,通常指紧跟在首球之后的所有同属性号球为随球。随球从一定意义上具有拖码的部分功效,同时,随球也可以是复式投注的重点观察对象。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">3</span>
|
||||
<div class="term-content">
|
||||
<strong>活跃系数</strong>:这是对一个号球活跃性状态进行综合计算的表述性指标,其中主要包括了该号球的出现频率因素,也包括了与邻近号球在开奖序列中发生关联交互的因素。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">4</span>
|
||||
<div class="term-content">
|
||||
<strong>接续系数</strong>:这是对一个号球接续性状态进行综合计算的表述性指标,其中主要包括了该号球与上期和下期号球的接续因素,也包括了与邻近号球在开奖序列中发生关联交互的因素。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">5</span>
|
||||
<div class="term-content">
|
||||
<strong>组合系数</strong>:这是对一个号球组合性状态进行综合计算的表述性指标,其中主要包括了该号球与其它号球的组合因素,也包括了与邻近号球在开奖序列中发生关联交互的因素。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">6</span>
|
||||
<div class="term-content">
|
||||
<strong>智推版</strong>:这是《精彩猪手》提供的一个关于下期开奖号球推测智选模块化程序。该程序基于"上期"与"本期"的姿态逻辑数据集,采用分段分步推测法,辅助彩民来搜寻最有希望成为"下期"开奖号球的"可能性目标"。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">7</span>
|
||||
<div class="term-content">
|
||||
<strong>精推版</strong>:这是《精彩猪手》提供的一个关于下期开奖号球推测精选模块化程序。该程序敞开目标号球主要数据,由彩民根据各项指标来精比细对,审慎选择其中最有希望成为"下期"开奖号球的"可能目标"。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">8</span>
|
||||
<div class="term-content">
|
||||
<strong>出现频次</strong>:是指一个号球进入开奖号球榜的次数。每进入1次,这个号球便增加1个出现频次。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">9</span>
|
||||
<div class="term-content">
|
||||
<strong>平均隐现期</strong>:所谓隐现期,是指号球没有进入开奖号球榜的期次。开奖期次与号球出现频次的简单比值就是平均隐现期。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">10</span>
|
||||
<div class="term-content">
|
||||
<strong>当前隐现期</strong>:是指一个号球从上一次登入开奖榜,到本期开奖连续未登入开奖榜的期次。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">11</span>
|
||||
<div class="term-content">
|
||||
<strong>最长隐现期次</strong>:是指一个号球在标定的时间范围内,连续未登入开奖榜期次最多的那一条记录。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">12</span>
|
||||
<div class="term-content">
|
||||
<strong>最大连出期次</strong>:是指号球在标定的期间范围内,连续不间断的都有登入开奖榜的最高期次记录。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">13</span>
|
||||
<div class="term-content">
|
||||
<strong>上期、本期与下期</strong>:这是对开奖期次的不同时态进行界定的简单标准:所有本期之前的开奖期次均称为上期;已开出的最新的开奖期次称为本期;紧接本期即将开出的奖期称为下期。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">14</span>
|
||||
<div class="term-content">
|
||||
<strong>百期排位</strong>:是指最近100期号球的活跃性排行位置。活跃性数据越优,排行越靠前。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">15</span>
|
||||
<div class="term-content">
|
||||
<strong>历史排位</strong>:是指在整个历史开奖周期中号球的活跃性排行位置。活跃性数据越优,排行越靠前。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">16</span>
|
||||
<div class="term-content">
|
||||
<strong>张力值</strong>:是号球活跃性、组合性、接续性的综合计算的指标性结果。张力值越高,表示综合指标越好。
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-item">
|
||||
<span class="term-number">17</span>
|
||||
<div class="term-content">
|
||||
<strong>系数值</strong>:是号球组合性、接续性的交叉计算结果。系数值越高,表示组合、接续交叉性越好。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三章 -->
|
||||
<div id="section3" class="guide-section">
|
||||
<h3 class="section-title">三、注册、登录与使用</h3>
|
||||
<div class="section-content">
|
||||
<p>用户可通过手机(电脑)浏览器,直接访问 <strong>www.yicaishuzhi.com</strong> 网址进行注册和登录,也可通过微信直接登录《精彩猪手》。注册成功并登录后即刻成为《精彩猪手》会员。</p>
|
||||
<div class="info-box">
|
||||
<p><strong>会员收费标准:</strong></p>
|
||||
<ul>
|
||||
<li>月度会员:10元/月</li>
|
||||
<li>年度会员:100元/年</li>
|
||||
</ul>
|
||||
<p class="tip-text">新会员入住即可获得为期30天的VIP会员体验。</p>
|
||||
</div>
|
||||
<p><strong>会员权益包括:</strong></p>
|
||||
<ol>
|
||||
<li>开奖信息查询;</li>
|
||||
<li>号球活跃性、组合性、接续性数据分析;</li>
|
||||
<li>下期开奖推测辅助(VIP会员);</li>
|
||||
<li>推测数据备份(VIP会员);</li>
|
||||
<li>推测行为分析(VIP会员);</li>
|
||||
<li>AI互动交流。</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第四章 -->
|
||||
<div id="section4" class="guide-section">
|
||||
<h3 class="section-title">四、双色球智推版操作要点</h3>
|
||||
<div class="section-content">
|
||||
<p>双色球开奖号球智推版,是一个用于推测"下期"开奖号球的简易辅助程序。完成一轮推测流程仅可产生一个彩票号组。具体操作要领如下:</p>
|
||||
<div class="step-list">
|
||||
<div class="step-item"><span class="step-num">第一步</span>完成注册登录,进入首页,《精彩猪手》默认停留在智推版,点击"双色球"进入。</div>
|
||||
<div class="step-item"><span class="step-num">第二步</span>会员"请选择推测进程中需要调用的关键数据区位"。《精彩猪手》姿态逻辑数据集的关键数据,分为"高、中、低"三块区位,会员每次推测必须且只能选择其中一个区位。</div>
|
||||
<div class="step-item"><span class="step-num">第三步</span>确认"本期开奖期号"栏内,系统默认写入最新开奖期号,以及与本期期号相对应的红球号码和篮球号码均无误。</div>
|
||||
<div class="step-item"><span class="step-num">第四步</span>确认"推测下期期号"栏内,系统默认写入紧接本期的下期期号无误。</div>
|
||||
<div class="step-item"><span class="step-num">第五步</span>点击"继续",《精彩猪手》即推荐出11个数据向好的红球供参考。请会员根据自己的理解,从所有33个红球当中选择1个,作为"下期"的首球。</div>
|
||||
<div class="step-item"><span class="step-num">第六步</span>请会员在预留的位置,任意写入2个自己所期望中的红球号码,加入下一步的随球推测行列。</div>
|
||||
<div class="step-item"><span class="step-num">第七步</span>点击"继续",《精彩猪手》即推荐出11个数据向好的红球供参考。请会员根据自己的理解,从不包含已选首球的其它32个红球中,选择5个作为随球。</div>
|
||||
<div class="step-item"><span class="step-num">第八步</span>点击"继续",开始推测篮球进程。请会员根据自己的理解,在预留处选择2个自己所期望中的篮球号码,加入下一步的篮球推测行列。</div>
|
||||
<div class="step-item"><span class="step-num">第九步</span>点击"分析篮球",《精彩猪手》即推荐出5个数据向好的篮球号码供参考。请会员根据自己的理解,选择16个篮球中的任意1个作为下期的篮球号码。</div>
|
||||
<div class="step-item"><span class="step-num">第十步</span>点击"继续",《精彩猪手》展示出会员所选定的一支完整的双色球彩票号组,确认无误之后,会员勾选提示框"以上号码由我本人所推测选定",然后点击"确认"。</div>
|
||||
<div class="step-item"><span class="step-num">第十一步</span>《精彩猪手》弹出风险提示框:"彩票开奖系随机,数据、逻辑仅参考!",会员点击"确认"。</div>
|
||||
<div class="step-item"><span class="step-num">第十二步</span>《精彩猪手》公告完成本次推测进程,会员点击"确定",所推测结果保存至会员推测记录。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第五章 -->
|
||||
<div id="section5" class="guide-section">
|
||||
<h3 class="section-title">五、双色球精推版操作要点</h3>
|
||||
<div class="section-content">
|
||||
<p>双色球开奖号球精推版,是一个用于推测"下期"开奖号球的精细辅助程序。全程配以号球动态数据,便于会员观察、比对和选择。完成一轮推测流程仅可产生一个彩票号组。具体操作要领如下:</p>
|
||||
<div class="step-list">
|
||||
<div class="step-item"><span class="step-num">第一步</span>进入首页,点击"精推版",选择"双色球",界面转入"推测准备"环节。</div>
|
||||
<div class="step-item"><span class="step-num">第二步</span>会员"请选择推测进程中所调用的关键数据区位"。《精彩猪手》将自有的姿态逻辑数据集的关键数据,分为"高、中、低"三块区位,会员每次推测必须且只能选择其中一个区位。</div>
|
||||
<div class="step-item"><span class="step-num">第三步</span>确认"本期开奖期号"栏内,系统默认写入最新开奖期号,以及与本期期号相对应的红球号码和篮球号码均无误。</div>
|
||||
<div class="step-item"><span class="step-num">第四步</span>确认"推测下期期号"栏内,系统默认写入紧接本期的下期期号无误。</div>
|
||||
<div class="step-item"><span class="step-num">第五步</span>点击"继续"。《精彩猪手》进入推测首球环节,依序展示出所有红球的分析数据,包括张力值、系数值、百期排位、历史排位,供会员参考选择。</div>
|
||||
<div class="step-item"><span class="step-num">第六步</span>会员从列表中的33个红球中选择1个作为首球,然后点击"继续"。</div>
|
||||
<div class="step-item"><span class="step-num">第七步</span>进入推测随球环节。会员参考所列举的"随球分析数据",选择5个与首球不相重复的红球号码作为随球,点击"继续",进行下一步。</div>
|
||||
<div class="step-item"><span class="step-num">第八步</span>进入推测篮球环节。会员参考所列举的"篮球分析数据",选择1个篮球号码,点击"继续",进行下一步。</div>
|
||||
<div class="step-item"><span class="step-num">第九步</span>《精彩猪手》展示出会员所选定的一支完整的双色球彩票号组,确认无误之后,会员勾选提示框"以上号码由我本人所推测选定",然后点击"确认"。</div>
|
||||
<div class="step-item"><span class="step-num">第十步</span>《精彩猪手》弹出风险提示框:"彩票开奖系随机,数据、逻辑仅参考!",会员点击"确认"。</div>
|
||||
<div class="step-item"><span class="step-num">第十一步</span>《精彩猪手》公告完成本次推测进程,会员点击"确定",所推测结果保存至会员推测记录。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第六章 -->
|
||||
<div id="section6" class="guide-section">
|
||||
<h3 class="section-title">六、超级大乐透智推版操作要点</h3>
|
||||
<div class="section-content">
|
||||
<p>超级大乐透(以下简称大乐透)开奖号球智推版,是一个用于推测"下期"开奖号球的简易辅助程序。完成一轮推测流程仅可产生一个彩票号组。具体操作要领如下:</p>
|
||||
<div class="step-list">
|
||||
<div class="step-item"><span class="step-num">第一步</span>进入首页,《精彩猪手》默认停留在智推版,点击"大乐透"进入。</div>
|
||||
<div class="step-item"><span class="step-num">第二步</span>会员"请选择推测进程中所调用的关键数据区位"。《精彩猪手》将自有的姿态逻辑数据集中的关键数据,分为"高、中、低"三块区位,会员每次推测必须且只能选择其中一个区位。</div>
|
||||
<div class="step-item"><span class="step-num">第三步</span>确认"本期开奖期号"栏内,系统默认写入最新开奖期号,以及与本期期号相对应的前区号球和后区号球均无误。</div>
|
||||
<div class="step-item"><span class="step-num">第四步</span>确认"推测下期期号"栏内,系统默认写入紧接本期的下期期号无误。</div>
|
||||
<div class="step-item"><span class="step-num">第五步</span>点击"继续",《精彩猪手》推荐出12个数据向好的前区号球供参考。请会员根据自己的理解,从所有前区号球当中选择1个,作为"下期"彩票号组的前区首球。</div>
|
||||
<div class="step-item"><span class="step-num">第六步</span>请会员在预留的位置,任意写入3个自己期望中的前区号球,以加入下一步的前区随球推测行列。</div>
|
||||
<div class="step-item"><span class="step-num">第七步</span>点击"继续",《精彩猪手》推荐出12个数据向好的前区号球供参考。请会员根据自己的理解,从不包含已选前区首球的其它号球中,选择4个作为前区随球。</div>
|
||||
<div class="step-item"><span class="step-num">第八步</span>开始推测后区首球。请会员根据自己的理解,在预留处选择2个自己期望中的后区号球,加入下一步的后区首球推测行列。</div>
|
||||
<div class="step-item"><span class="step-num">第九步</span>点击"继续",《精彩猪手》推荐了5个数据向好的后区号球,供参考。请会员根据自己的判断,从所有的后区号球中选择1个,做为后区首球。</div>
|
||||
<div class="step-item"><span class="step-num">第十步</span>《精彩猪手》根据会员所选择的后区首球,又向会员推荐了4个数据向好的后区号球供参考。</div>
|
||||
<div class="step-item"><span class="step-num">第十一步</span>请会员从不包含已选后区首球的所有后区号球中,选择1个自己认定的号球,做为后区随球。</div>
|
||||
<div class="step-item"><span class="step-num">第十二步</span>点击"继续",《精彩猪手》展示出会员所选定的一支完整的大乐透彩票号组,确认无误之后,会员勾选提示框"以上号码为本人所研判选定",然后点击"确认"。</div>
|
||||
<div class="step-item"><span class="step-num">第十三步</span>《精彩猪手》弹出风险提示框:"彩票开奖系随机,数据、逻辑仅参考!",会员点击"确认"。</div>
|
||||
<div class="step-item"><span class="step-num">第十四步</span>《精彩猪手》公告完成本次推测进程,会员点击"确定",所推测结果保存至会员推测记录。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第七章 -->
|
||||
<div id="section7" class="guide-section">
|
||||
<h3 class="section-title">七、大乐透精推版操作要点</h3>
|
||||
<div class="section-content">
|
||||
<p>大乐透开奖号球精推版,是一个用于推测"下期"开奖号球的精细辅助程序。全程配以号球动态数据,便于会员观察、比对和选择。完成一轮推测流程仅可产生一个彩票号组。具体操作要领如下:</p>
|
||||
<div class="step-list">
|
||||
<div class="step-item"><span class="step-num">第一步</span>进入首页,点击"精推版",选择"大乐透",界面转入"推测准备"环节。</div>
|
||||
<div class="step-item"><span class="step-num">第二步</span>会员"请选择推测进程中需要调用的关键数据区位"。《精彩猪手》将自有的姿态逻辑数据集中的关键数据,分为"高、中、低"三块区位,会员每次推测必须且只能选择其中一个区位。</div>
|
||||
<div class="step-item"><span class="step-num">第三步</span>确认"本期开奖期号"栏内,系统默认写入最新开奖期号,以及与本期期号相对应的前区号球和后区号球均无误。</div>
|
||||
<div class="step-item"><span class="step-num">第四步</span>确认"推测下期期号"栏内,系统默认写入紧接本期的下期期号无误。</div>
|
||||
<div class="step-item"><span class="step-num">第五步</span>点击"继续"。《精彩猪手》进入推测前区首球环节,依序展示出所有前区号球的分析数据,包括张力值、系数值、百期排位、历史排位,供会员参考选择。</div>
|
||||
<div class="step-item"><span class="step-num">第六步</span>会员从列表中选择1个号球作为前区首球,然后点击"继续"。</div>
|
||||
<div class="step-item"><span class="step-num">第七步</span>进入推测前区随球环节。会员参考所列举的"前区随球分析数据",选择4个与前区首球不相重复的号球作为前区随球,点击"继续",进行下一步。</div>
|
||||
<div class="step-item"><span class="step-num">第八步</span>进入推测后区首球环节。会员参考所列举的"后区首球分析数据",选择1个后区号球作为后区首球,点击"继续",进行下一步。</div>
|
||||
<div class="step-item"><span class="step-num">第九步</span>《精彩猪手》根据会员所选择的后区首球,展示出相应的"后区随球分析数据",供参考。由会员选择1个与后区首球不相重复的号球做为后区随球,点击"继续"。</div>
|
||||
<div class="step-item"><span class="step-num">第十步</span>《精彩猪手》展示出会员所选定的一支完整的大乐透彩票号组,确认无误之后,会员勾选提示框"以上号码为本人所推测选定",然后点击"确认"。</div>
|
||||
<div class="step-item"><span class="step-num">第十一步</span>《精彩猪手》弹出风险提示框:"彩票开奖系随机,数据、逻辑仅参考!",会员点击"确认"。</div>
|
||||
<div class="step-item"><span class="step-num">第十二步</span>《精彩猪手》公告完成本次推测进程,会员点击"确定",所推测结果保存至会员推测记录。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第八章 -->
|
||||
<div id="section8" class="guide-section">
|
||||
<h3 class="section-title">八、"活跃性分析"数据调用</h3>
|
||||
<div class="section-content">
|
||||
<div class="step-list">
|
||||
<div class="step-item"><span class="step-num">1</span>点击"分析",进入数据分析页面。</div>
|
||||
<div class="step-item"><span class="step-num">2</span>点击选择需要分析的彩票名称。《精彩猪手》默认状态为双色球。</div>
|
||||
<div class="step-item"><span class="step-num">3</span>点击"活跃性分析",进入活跃性分析页面。</div>
|
||||
<div class="step-item"><span class="step-num">4</span>在活跃性分析页面上选择所需要调用的号球色别,比如"红球"或"篮球"。《精彩猪手》默认状态为红球。</div>
|
||||
<div class="step-item"><span class="step-num">5</span>选择所需要分析的数据项目,对应的数据即全部同步展现出来。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第九章 -->
|
||||
<div id="section9" class="guide-section">
|
||||
<h3 class="section-title">九、"接续性分析"数据调用</h3>
|
||||
<div class="section-content">
|
||||
<div class="step-list">
|
||||
<div class="step-item"><span class="step-num">1</span>点击"分析",进入数据分析页面。</div>
|
||||
<div class="step-item"><span class="step-num">2</span>点击选择需要分析的彩票名称。《精彩猪手》默认状态为双色球。</div>
|
||||
<div class="step-item"><span class="step-num">3</span>点击"接续性分析",进入接续性分析页面。</div>
|
||||
<div class="step-item"><span class="step-num">4</span>选择所需要分析的号球接续形态。比如,红球与篮球的接续。</div>
|
||||
<div class="step-item"><span class="step-num">5</span>在下方所显示的图形中,分别选择对应的红球和篮球号码,然后点击"开始分析"。一次仅可以通过《精彩猪手》分析两个号球之间的接续数据。</div>
|
||||
<div class="step-item"><span class="step-num">6</span>下方展示出当前号球的接续性分析报告。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第十章 -->
|
||||
<div id="section10" class="guide-section">
|
||||
<h3 class="section-title">十、"组合性分析"数据调用</h3>
|
||||
<div class="section-content">
|
||||
<div class="step-list">
|
||||
<div class="step-item"><span class="step-num">1</span>点击"分析",进入数据分析页面。</div>
|
||||
<div class="step-item"><span class="step-num">2</span>点击选择需要分析的彩票名称。《精彩猪手》默认状态为双色球。</div>
|
||||
<div class="step-item"><span class="step-num">3</span>点击"组合性分析",进入组合性分析页面。</div>
|
||||
<div class="step-item"><span class="step-num">4</span>选择所需要分析的号球组合形态。比如,红球与篮球的组合。</div>
|
||||
<div class="step-item"><span class="step-num">5</span>在下方所显示的图形中,分别选择对应的红球和篮球号码,然后点击"开始分析"。《精彩猪手》一次仅可以分析两个号球之间的组合数据。</div>
|
||||
<div class="step-item"><span class="step-num">6</span>下方展示出当前号球的组合性分析报告。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="footer-info">
|
||||
<p class="company-name">西安溢彩数智科技有限公司</p>
|
||||
<p class="date">2025年12月31日</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'UserGuide',
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-guide-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header-modern {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
padding: 20px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 12px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 4px 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 主要内容 */
|
||||
.guide-content {
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
/* 标题区域 */
|
||||
.guide-title-section {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 目录导航 */
|
||||
.toc-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.toc-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #4facfe;
|
||||
}
|
||||
|
||||
.toc-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toc-item {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(135deg, rgba(79, 172, 254, 0.1) 0%, rgba(0, 242, 254, 0.1) 100%);
|
||||
border-radius: 20px;
|
||||
color: #4facfe;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toc-item:hover {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 章节样式 */
|
||||
.guide-section {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #4facfe;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -2px;
|
||||
width: 60px;
|
||||
height: 2px;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
font-size: 15px;
|
||||
color: #555;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.section-content p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-content strong {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.highlight-text {
|
||||
background: linear-gradient(135deg, rgba(79, 172, 254, 0.1) 0%, rgba(0, 242, 254, 0.1) 100%);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #4facfe;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 功能列表 */
|
||||
.feature-list {
|
||||
padding-left: 20px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.feature-list li {
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 术语列表 */
|
||||
.term-list {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.term-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafbfc;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.term-item:hover {
|
||||
background: linear-gradient(135deg, rgba(79, 172, 254, 0.05) 0%, rgba(0, 242, 254, 0.05) 100%);
|
||||
}
|
||||
|
||||
.term-number {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.term-content {
|
||||
flex: 1;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 信息框 */
|
||||
.info-box {
|
||||
background: linear-gradient(135deg, rgba(79, 172, 254, 0.1) 0%, rgba(0, 242, 254, 0.1) 100%);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin: 16px 0;
|
||||
border-left: 4px solid #4facfe;
|
||||
}
|
||||
|
||||
.info-box ul {
|
||||
padding-left: 20px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.info-box li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
color: #4facfe;
|
||||
font-weight: 500;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 步骤列表 */
|
||||
.step-list {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
padding: 14px 16px;
|
||||
background: #fafbfc;
|
||||
border-radius: 10px;
|
||||
line-height: 1.6;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.step-item:hover {
|
||||
background: linear-gradient(135deg, rgba(79, 172, 254, 0.05) 0%, rgba(0, 242, 254, 0.05) 100%);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.step-num {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-right: 12px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 底部信息 */
|
||||
.footer-info {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header-modern {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.guide-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.toc-list {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toc-item {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.term-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.term-number {
|
||||
margin-bottom: 8px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
margin-bottom: 8px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
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>
|
||||
431
src/views/dlt/DltTableAnalysis.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<div class="table-analysis-container">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button @click="goBack" type="default" size="medium" icon="ArrowLeft">
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>大乐透表相查询</h1>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<el-card v-if="loading" class="loading-container" shadow="never">
|
||||
<el-skeleton :rows="10" animated />
|
||||
</el-card>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<el-card v-else-if="error" class="error-container" shadow="never">
|
||||
<el-alert
|
||||
:title="error"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
/>
|
||||
<el-button @click="fetchData" type="primary">重新加载</el-button>
|
||||
</el-card>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card v-else-if="drawsData.length > 0" class="table-container" shadow="never">
|
||||
<div class="table-wrapper">
|
||||
<el-table
|
||||
:data="drawsData"
|
||||
border
|
||||
style="width: 100%"
|
||||
:highlight-current-row="true"
|
||||
@current-change="handleCurrentChange"
|
||||
height="calc(100vh - 150px)"
|
||||
>
|
||||
<el-table-column
|
||||
prop="drawId"
|
||||
label="期号"
|
||||
width="80"
|
||||
fixed="left"
|
||||
align="center"
|
||||
/>
|
||||
|
||||
<!-- 前区球列 -->
|
||||
<el-table-column label="前区球" align="center">
|
||||
<el-table-column
|
||||
v-for="num in 35"
|
||||
:key="`front-${num}`"
|
||||
:label="formatNumberLabel(num)"
|
||||
width="40"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div v-if="isFrontBallHit(scope.row, num)" class="ball front-ball" :class="{'double-digits': num >= 10}">
|
||||
{{ num }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 后区球列 -->
|
||||
<el-table-column label="后区球" align="center">
|
||||
<el-table-column
|
||||
v-for="num in 12"
|
||||
:key="`back-${num}`"
|
||||
:label="formatNumberLabel(num)"
|
||||
width="40"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div v-if="isBackBallHit(scope.row, num)" class="ball back-ball" :class="{'double-digits': num >= 10}">
|
||||
{{ num }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 无数据状态 -->
|
||||
<el-card v-else class="no-data-container" shadow="never">
|
||||
<el-empty description="暂无开奖数据" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { dltLotteryApi } from '../../api/dlt/index.js'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElSkeleton,
|
||||
ElAlert,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElEmpty
|
||||
} from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'DltTableAnalysis',
|
||||
components: {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElSkeleton,
|
||||
ElAlert,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElEmpty
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drawsData: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
selectedRowIndex: null // 选中的行索引
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
// 获取当前彩票类型,优先使用路由参数,否则使用默认值 'dlt'
|
||||
const lotteryType = this.$route.query.lotteryType || 'dlt'
|
||||
this.$router.push({
|
||||
path: '/data-analysis',
|
||||
query: { lotteryType: lotteryType }
|
||||
})
|
||||
},
|
||||
|
||||
// 选中行
|
||||
handleCurrentChange(row) {
|
||||
this.selectedRowIndex = row ? this.drawsData.indexOf(row) : null
|
||||
},
|
||||
|
||||
// 获取数据
|
||||
async fetchData() {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
console.log('开始获取最新100期大乐透开奖数据')
|
||||
const response = await dltLotteryApi.getRecent100Draws()
|
||||
|
||||
if (response && response.success && response.data) {
|
||||
this.drawsData = response.data
|
||||
console.log('获取大乐透开奖数据成功,共', this.drawsData.length, '期')
|
||||
console.log('第一条数据示例:', this.drawsData[0])
|
||||
if (this.drawsData.length > 0) {
|
||||
console.log('前区球数据:', this.getFrontBalls(this.drawsData[0]))
|
||||
console.log('后区球数据:', this.getBackBalls(this.drawsData[0]))
|
||||
}
|
||||
} else {
|
||||
this.error = response?.message || '获取数据失败'
|
||||
console.error('获取数据失败:', response)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取大乐透开奖数据出错:', error)
|
||||
this.error = error?.response?.data?.message || '网络错误,请稍后重试'
|
||||
|
||||
// 添加测试数据以便调试
|
||||
this.drawsData = [
|
||||
{
|
||||
id: 2761,
|
||||
drawId: '25093',
|
||||
drawDate: 1755273600000,
|
||||
frontBall1: 1,
|
||||
frontBall2: 7,
|
||||
frontBall3: 9,
|
||||
frontBall4: 16,
|
||||
frontBall5: 30,
|
||||
backBall1: 2,
|
||||
backBall2: 5
|
||||
},
|
||||
{
|
||||
id: 2760,
|
||||
drawId: '25092',
|
||||
drawDate: 1755014400000,
|
||||
frontBall1: 4,
|
||||
frontBall2: 10,
|
||||
frontBall3: 17,
|
||||
frontBall4: 25,
|
||||
frontBall5: 32,
|
||||
backBall1: 5,
|
||||
backBall2: 7
|
||||
}
|
||||
]
|
||||
console.log('使用测试数据:', this.drawsData)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 获取前区球数组
|
||||
getFrontBalls(draw) {
|
||||
const balls = []
|
||||
if (draw.frontBall1) balls.push(parseInt(draw.frontBall1))
|
||||
if (draw.frontBall2) balls.push(parseInt(draw.frontBall2))
|
||||
if (draw.frontBall3) balls.push(parseInt(draw.frontBall3))
|
||||
if (draw.frontBall4) balls.push(parseInt(draw.frontBall4))
|
||||
if (draw.frontBall5) balls.push(parseInt(draw.frontBall5))
|
||||
return balls
|
||||
},
|
||||
|
||||
// 获取后区球数组
|
||||
getBackBalls(draw) {
|
||||
const balls = []
|
||||
if (draw.backBall1) balls.push(parseInt(draw.backBall1))
|
||||
if (draw.backBall2) balls.push(parseInt(draw.backBall2))
|
||||
return balls
|
||||
},
|
||||
|
||||
// 判断前区球是否命中
|
||||
isFrontBallHit(draw, num) {
|
||||
const frontBalls = this.getFrontBalls(draw)
|
||||
return frontBalls.includes(num)
|
||||
},
|
||||
|
||||
// 判断后区球是否命中
|
||||
isBackBallHit(draw, num) {
|
||||
const backBalls = this.getBackBalls(draw)
|
||||
return backBalls.includes(num)
|
||||
},
|
||||
|
||||
// 格式化数字标签,确保两位数显示在一行
|
||||
formatNumberLabel(num) {
|
||||
// 直接返回数字,CSS会处理双位数的显示
|
||||
return num;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table-analysis-container {
|
||||
min-height: calc(100vh - 70px);
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 工具栏样式 */
|
||||
.toolbar {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.error-container {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* 自定义表格样式 */
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: #f5f7fa;
|
||||
--el-table-header-text-color: #333;
|
||||
--el-table-row-hover-bg-color: #ecf5ff;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) th {
|
||||
padding: 2px 0;
|
||||
height: 32px;
|
||||
font-size: 12px; /* 减小表头字体大小 */
|
||||
}
|
||||
|
||||
:deep(.el-table__header .cell) {
|
||||
padding: 0 !important; /* 移除单元格内边距 */
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 对两位数的表头标签应用特殊样式 */
|
||||
:deep(.el-table__header th[class*="is-leaf"]:nth-child(n+11)) .cell {
|
||||
font-size: 11px; /* 缩小两位数字体 */
|
||||
letter-spacing: -0.5px; /* 减少字母间距 */
|
||||
}
|
||||
|
||||
:deep(.el-table__body) td {
|
||||
padding: 4px 0;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
:deep(.el-table--border .el-table__cell) {
|
||||
border-right: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
:deep(.el-table__fixed-right) {
|
||||
height: 100%;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 无数据状态 */
|
||||
.no-data-container {
|
||||
margin-bottom: 20px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 球体样式 */
|
||||
.ball {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 900;
|
||||
font-size: 10px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 两位数的球使用更小字体 */
|
||||
.double-digits {
|
||||
font-size: 9px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
/* 前区球样式(红色) */
|
||||
.front-ball {
|
||||
background: linear-gradient(135deg, #ff5252 0%, #d32f2f 50%, #b71c1c 100%);
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #b71c1c;
|
||||
}
|
||||
|
||||
.front-ball::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 后区球样式(蓝色) */
|
||||
.back-ball {
|
||||
background: linear-gradient(135deg, #42a5f5 0%, #1976d2 50%, #0d47a1 100%);
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #0d47a1;
|
||||
}
|
||||
|
||||
.back-ball::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.table-analysis-container {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 在移动端调整球体大小 */
|
||||
.ball {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.ball::before {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
831
src/views/dlt/HitAnalysis.vue
Normal file
3840
src/views/dlt/Home.vue
Normal file
813
src/views/dlt/LineAnalysis.vue
Normal file
@@ -0,0 +1,813 @@
|
||||
<template>
|
||||
<div class="line-analysis">
|
||||
<div class="header">
|
||||
<!-- 返回按钮 -->
|
||||
<div class="back-button-container">
|
||||
<el-button @click="goBack" icon="ArrowLeft" size="medium">
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<h2>接续性分析</h2>
|
||||
<p>号球接续分析,把握上依下托</p>
|
||||
</div>
|
||||
|
||||
<!-- 分析类型选择 -->
|
||||
<div class="analysis-buttons">
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'front-front' }"
|
||||
@click="selectAnalysisType('front-front')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">前区与前区</div>
|
||||
<div class="btn-desc">前区球接续性分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'back-back' }"
|
||||
@click="selectAnalysisType('back-back')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">后区与后区</div>
|
||||
<div class="btn-desc">后区球接续性分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'front-back' }"
|
||||
@click="selectAnalysisType('front-back')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">前区与后区</div>
|
||||
<div class="btn-desc">前后区接续性分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'back-front' }"
|
||||
@click="selectAnalysisType('back-front')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">后区与前区</div>
|
||||
<div class="btn-desc">后前区接续性分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 号码选择和分析区域 -->
|
||||
<el-card v-if="analysisType" class="analysis-container" shadow="never">
|
||||
<div class="number-selection">
|
||||
<h3>{{ getSelectionTitle() }}</h3>
|
||||
|
||||
<!-- 主球选择 -->
|
||||
<div class="ball-selection-group">
|
||||
<el-divider>{{ getMasterBallLabel() }}</el-divider>
|
||||
<div class="ball-grid">
|
||||
<el-button
|
||||
v-for="num in getMasterBallRange()"
|
||||
:key="'master-' + num"
|
||||
:class="{
|
||||
active: masterBall === num,
|
||||
'red-ball': isMasterRed(),
|
||||
'blue-ball': !isMasterRed()
|
||||
}"
|
||||
:type="masterBall === num ? 'primary' : 'default'"
|
||||
:plain="masterBall !== num"
|
||||
circle
|
||||
@click="selectMasterBall(num)"
|
||||
>
|
||||
{{ num }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 随球选择 -->
|
||||
<div class="ball-selection-group">
|
||||
<el-divider>{{ getSlaveBallLabel() }}</el-divider>
|
||||
<div class="ball-grid">
|
||||
<el-button
|
||||
v-for="num in getSlaveBallRange()"
|
||||
:key="'slave-' + num"
|
||||
:class="{
|
||||
active: slaveBall === num,
|
||||
'red-ball': isSlaveRed(),
|
||||
'blue-ball': !isSlaveRed()
|
||||
}"
|
||||
:type="slaveBall === num ? 'primary' : 'default'"
|
||||
:plain="slaveBall !== num"
|
||||
circle
|
||||
@click="selectSlaveBall(num)"
|
||||
>
|
||||
{{ num }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<el-alert
|
||||
v-if="selectionError"
|
||||
:title="selectionError"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin: 15px 0;"
|
||||
/>
|
||||
|
||||
<!-- 分析按钮 -->
|
||||
<div class="analyze-section">
|
||||
<el-button
|
||||
type="success"
|
||||
size="large"
|
||||
round
|
||||
:disabled="!canAnalyze || loading"
|
||||
:loading="loading"
|
||||
@click="performAnalysis"
|
||||
>
|
||||
{{ loading ? '分析中...' : '开始分析' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分析结果 -->
|
||||
<div v-if="result !== null || error" class="result-container">
|
||||
<el-alert
|
||||
v-if="error"
|
||||
:title="error"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
>
|
||||
<template #default>
|
||||
<el-button @click="clearError" type="primary" size="small">重试</el-button>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-result
|
||||
v-else-if="result !== null"
|
||||
icon="success"
|
||||
title="接续性分析结果"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="result-details">
|
||||
<div class="table-wrapper">
|
||||
<div class="table-title-row">
|
||||
<div class="table-title-cell">
|
||||
<span class="highlight-ball" :class="{ 'blue-text': !isMasterRed() }">{{masterBall}}</span>号{{isMasterRed() ? '前区' : '后区'}}球与<span class="highlight-ball" :class="{ 'blue-text': !isSlaveRed() }">{{slaveBall}}</span>号{{isSlaveRed() ? '前区' : '后区'}}球接续性分析报告
|
||||
</div>
|
||||
</div>
|
||||
<table class="analysis-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label-cell">引用数据截至</td>
|
||||
<td class="value-cell">{{resultData[0]?.latestDrawId || '-'}}期</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">两号接续系数</td>
|
||||
<td class="value-cell">{{resultData[0]?.lineCoefficient || '-'}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">同号组最高系数球号及系数</td>
|
||||
<td class="value-cell">
|
||||
<span class="highlight-ball" :class="{ 'blue-text': !isSlaveRed() }">{{resultData[0]?.highestBall || '--'}}</span>
|
||||
<span class="coefficient-value">{{resultData[0]?.highestCoefficient || '-'}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">同号组最低系数球号及系数</td>
|
||||
<td class="value-cell">
|
||||
<span class="highlight-ball" :class="{ 'blue-text': !isSlaveRed() }">{{resultData[0]?.lowestBall || '--'}}</span>
|
||||
<span class="coefficient-value">{{resultData[0]?.lowestCoefficient || '-'}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">同号组平均系数</td>
|
||||
<td class="value-cell">{{ (resultData[0]?.averageCoefficient && Number(resultData[0].averageCoefficient).toFixed(2)) || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">建议</td>
|
||||
<td class="value-cell recommendation">
|
||||
系数越高表示彼此接续越频繁。可进行多球接续系数比对,一般观察,高于平均系数的接续关系更值得关注。
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" @click="resetAnalysis">重新分析</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<el-card v-if="!analysisType" class="instruction-container" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span><el-icon><InfoFilled /></el-icon> 接续性分析说明</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-collapse accordion>
|
||||
<el-collapse-item title="前区与前区分析" name="front-front">
|
||||
<div class="collapse-content">
|
||||
<p>分析两个前区球号码之间的接续性关系,计算前区球配对的接续系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="后区与后区分析" name="back-back">
|
||||
<div class="collapse-content">
|
||||
<p>分析两个后区球号码之间的接续性关系,计算后区球配对的接续系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="前区与后区分析" name="front-back">
|
||||
<div class="collapse-content">
|
||||
<p>分析前区球与后区球号码的接续性关系,计算前后区配对的接续系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="后区与前区分析" name="back-front">
|
||||
<div class="collapse-content">
|
||||
<p>分析后区球与前区球号码的接续性关系,计算后前区配对的接续系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
<el-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-top: 20px;"
|
||||
>
|
||||
<template #title>
|
||||
<span>选择分析类型后,依次选择主球和随球号码,点击"开始分析"获取接续系数值</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { dltLotteryApi } from '@/api/dlt'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElAvatar,
|
||||
ElDivider,
|
||||
ElAlert,
|
||||
ElResult,
|
||||
ElIcon,
|
||||
ElCollapse,
|
||||
ElCollapseItem
|
||||
} from 'element-plus'
|
||||
import { ArrowLeft, InfoFilled } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'DltLineAnalysis',
|
||||
components: {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElAvatar,
|
||||
ElDivider,
|
||||
ElAlert,
|
||||
ElResult,
|
||||
ElIcon,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
InfoFilled
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
analysisType: '', // 'front-front', 'back-back', 'front-back', 'back-front'
|
||||
masterBall: null,
|
||||
slaveBall: null,
|
||||
loading: false,
|
||||
result: null,
|
||||
error: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canAnalyze() {
|
||||
return this.masterBall !== null && this.slaveBall !== null && this.analysisType && !this.selectionError
|
||||
},
|
||||
|
||||
selectionError() {
|
||||
if (this.masterBall && this.slaveBall) {
|
||||
// 前区与前区不能选择相同号码
|
||||
if (this.analysisType === 'front-front' && this.masterBall === this.slaveBall) {
|
||||
return '前区与前区分析不能选择相同的号码'
|
||||
}
|
||||
// 后区与后区不能选择相同号码
|
||||
if (this.analysisType === 'back-back' && this.masterBall === this.slaveBall) {
|
||||
return '后区与后区分析不能选择相同的号码'
|
||||
}
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
resultData() {
|
||||
if (!this.result) return [];
|
||||
|
||||
try {
|
||||
const data = typeof this.result === 'string' ? JSON.parse(this.result) : this.result;
|
||||
|
||||
return [{
|
||||
latestDrawId: data.latestDrawId || '',
|
||||
lineCoefficient: data.lineCoefficient || 0,
|
||||
highestCoefficient: data.highestCoefficient || 0,
|
||||
lowestCoefficient: data.lowestCoefficient || 0,
|
||||
averageCoefficient: data.averageCoefficient || 0,
|
||||
highestBall: data.highestBall || '-',
|
||||
lowestBall: data.lowestBall || '-'
|
||||
}];
|
||||
} catch (e) {
|
||||
console.error('解析结果数据失败', e);
|
||||
return [{
|
||||
latestDrawId: '',
|
||||
lineCoefficient: 0,
|
||||
highestCoefficient: 0,
|
||||
lowestCoefficient: 0,
|
||||
averageCoefficient: 0,
|
||||
highestBall: '-',
|
||||
lowestBall: '-'
|
||||
}];
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
// 获取当前彩票类型,优先使用路由参数,否则使用默认值 'dlt'
|
||||
const lotteryType = this.$route.query.lotteryType || 'dlt'
|
||||
this.$router.push({
|
||||
path: '/data-analysis',
|
||||
query: { lotteryType: lotteryType }
|
||||
})
|
||||
},
|
||||
|
||||
selectAnalysisType(type) {
|
||||
this.analysisType = type
|
||||
this.masterBall = null
|
||||
this.slaveBall = null
|
||||
this.result = null
|
||||
this.error = null
|
||||
},
|
||||
|
||||
getSelectionTitle() {
|
||||
const titles = {
|
||||
'front-front': '前区与前区接续性分析',
|
||||
'back-back': '后区与后区接续性分析',
|
||||
'front-back': '前区与后区接续性分析',
|
||||
'back-front': '后区与前区接续性分析'
|
||||
}
|
||||
return titles[this.analysisType] || ''
|
||||
},
|
||||
|
||||
getMasterBallLabel() {
|
||||
const labels = {
|
||||
'front-front': '主球(前区)',
|
||||
'back-back': '主球(后区)',
|
||||
'front-back': '主球(前区)',
|
||||
'back-front': '主球(后区)'
|
||||
}
|
||||
return labels[this.analysisType] || ''
|
||||
},
|
||||
|
||||
getSlaveBallLabel() {
|
||||
const labels = {
|
||||
'front-front': '随球(前区)',
|
||||
'back-back': '随球(后区)',
|
||||
'front-back': '随球(后区)',
|
||||
'back-front': '随球(前区)'
|
||||
}
|
||||
return labels[this.analysisType] || ''
|
||||
},
|
||||
|
||||
getMasterBallRange() {
|
||||
if (this.analysisType === 'back-back' || this.analysisType === 'back-front') {
|
||||
return Array.from({ length: 12 }, (_, i) => i + 1) // 1-12 后区
|
||||
}
|
||||
return Array.from({ length: 35 }, (_, i) => i + 1) // 1-35 前区
|
||||
},
|
||||
|
||||
getSlaveBallRange() {
|
||||
if (this.analysisType === 'back-back' || this.analysisType === 'front-back') {
|
||||
return Array.from({ length: 12 }, (_, i) => i + 1) // 1-12 后区
|
||||
}
|
||||
return Array.from({ length: 35 }, (_, i) => i + 1) // 1-35 前区
|
||||
},
|
||||
|
||||
isMasterRed() {
|
||||
return this.analysisType === 'front-front' || this.analysisType === 'front-back'
|
||||
},
|
||||
|
||||
isSlaveRed() {
|
||||
return this.analysisType === 'front-front' || this.analysisType === 'back-front'
|
||||
},
|
||||
|
||||
selectMasterBall(num) {
|
||||
this.masterBall = num
|
||||
this.result = null
|
||||
this.error = null
|
||||
},
|
||||
|
||||
selectSlaveBall(num) {
|
||||
this.slaveBall = num
|
||||
this.result = null
|
||||
this.error = null
|
||||
},
|
||||
|
||||
async performAnalysis() {
|
||||
if (!this.canAnalyze) return
|
||||
|
||||
this.loading = true
|
||||
this.error = null
|
||||
this.result = null
|
||||
|
||||
try {
|
||||
let response
|
||||
|
||||
switch (this.analysisType) {
|
||||
case 'front-front':
|
||||
response = await dltLotteryApi.frontFrontPersistenceAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
case 'back-back':
|
||||
response = await dltLotteryApi.backBackPersistenceAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
case 'front-back':
|
||||
response = await dltLotteryApi.frontBackPersistenceAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
case 'back-front':
|
||||
response = await dltLotteryApi.backFrontPersistenceAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
this.result = response.data
|
||||
} else {
|
||||
this.error = response.message || '分析失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('接续系数分析失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
clearError() {
|
||||
this.error = null
|
||||
},
|
||||
|
||||
resetAnalysis() {
|
||||
this.masterBall = null
|
||||
this.slaveBall = null
|
||||
this.result = null
|
||||
this.error = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-analysis {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.back-button-container {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 28px;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #7f8c8d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.analysis-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.analysis-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.analysis-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.15);
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.analysis-card.active {
|
||||
background-color: #eaf5ff;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
|
||||
.analysis-card.active :deep(.el-card__body) {
|
||||
background: transparent;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.red-ball-icon {
|
||||
background-color: #e74c3c !important;
|
||||
}
|
||||
|
||||
.blue-ball-icon {
|
||||
background-color: #3498db !important;
|
||||
}
|
||||
|
||||
.btn-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.analysis-card.active .btn-title {
|
||||
color: #2980b9;
|
||||
}
|
||||
|
||||
.btn-desc {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.analysis-card.active .btn-desc {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
|
||||
.number-selection {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.number-selection h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ball-selection-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.ball-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
:deep(.el-button.red-ball.is-plain:not(.is-disabled)) {
|
||||
color: #e74c3c;
|
||||
border-color: #e74c3c;
|
||||
background-color: #fff5f5;
|
||||
}
|
||||
|
||||
:deep(.el-button.red-ball:not(.is-plain):not(.is-disabled)) {
|
||||
background-color: #e74c3c;
|
||||
border-color: #c0392b;
|
||||
}
|
||||
|
||||
:deep(.el-button.blue-ball.is-plain:not(.is-disabled)) {
|
||||
color: #3498db;
|
||||
border-color: #3498db;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
:deep(.el-button.blue-ball:not(.is-plain):not(.is-disabled)) {
|
||||
background-color: #3498db;
|
||||
border-color: #2980b9;
|
||||
}
|
||||
|
||||
.analyze-section {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.result-container {
|
||||
border-top: 2px solid #ecf0f1;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-result) {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.result-details {
|
||||
width: 100%;
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto 20px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table-title-row {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.table-title-cell {
|
||||
padding: 8px;
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.analysis-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #000;
|
||||
border-top: none;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.analysis-table tr {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.analysis-table tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label-cell {
|
||||
width: 150px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #000;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.value-cell {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.highlight-ball {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.highlight-ball.blue-text {
|
||||
color: #3498db; /* 蓝色文字 */
|
||||
}
|
||||
|
||||
.coefficient-value {
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
padding: 5px;
|
||||
white-space: normal;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.instruction-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.collapse-content p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.line-analysis {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.back-button-container {
|
||||
position: static;
|
||||
text-align: left;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.analysis-buttons {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ball-grid {
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.table-title-cell {
|
||||
font-size: 14px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.label-cell, .value-cell {
|
||||
padding: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1369
src/views/dlt/Lottery.vue
Normal file
1324
src/views/dlt/PredictRecords.vue
Normal file
621
src/views/dlt/PrizeStatistics.vue
Normal file
913
src/views/dlt/SurfaceAnalysis.vue
Normal file
@@ -0,0 +1,913 @@
|
||||
<template>
|
||||
<div class="surface-analysis">
|
||||
<div class="header">
|
||||
<!-- 返回按钮 -->
|
||||
<div class="back-button-container">
|
||||
<el-button @click="goBack" icon="ArrowLeft" size="medium">
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<h2>组合性分析</h2>
|
||||
<p>号球组合分析,探寻左姻右缘</p>
|
||||
</div>
|
||||
|
||||
<!-- 分析类型选择 -->
|
||||
<div class="analysis-buttons">
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'front-front' }"
|
||||
@click="selectAnalysisType('front-front')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">前区与前区</div>
|
||||
<div class="btn-desc">前区组合分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'front-back' }"
|
||||
@click="selectAnalysisType('front-back')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">前区与后区</div>
|
||||
<div class="btn-desc">前后组合分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'back-back' }"
|
||||
@click="selectAnalysisType('back-back')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">后区与后区</div>
|
||||
<div class="btn-desc">后区组合分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'back-front' }"
|
||||
@click="selectAnalysisType('back-front')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">后区与前区</div>
|
||||
<div class="btn-desc">后前组合分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 号码选择和分析区域 -->
|
||||
<el-card v-if="analysisType" class="analysis-container" shadow="never">
|
||||
<el-alert
|
||||
v-if="selectionError"
|
||||
:title="selectionError"
|
||||
type="warning"
|
||||
show-icon
|
||||
@close="selectionError = null"
|
||||
style="margin-bottom: 20px;"
|
||||
/>
|
||||
<div class="number-selection">
|
||||
<h3>{{ getSelectionTitle() }}</h3>
|
||||
|
||||
<!-- 主球选择 -->
|
||||
<div class="ball-selection-group">
|
||||
<el-divider>{{ getMasterBallLabel() }}</el-divider>
|
||||
<div class="ball-grid">
|
||||
<el-button
|
||||
v-for="num in getMasterBallRange()"
|
||||
:key="'master-' + num"
|
||||
:class="{
|
||||
active: masterBall === num,
|
||||
'red-ball': isMasterFront(),
|
||||
'blue-ball': !isMasterFront()
|
||||
}"
|
||||
:type="masterBall === num ? 'primary' : 'default'"
|
||||
:plain="masterBall !== num"
|
||||
circle
|
||||
@click="selectMasterBall(num)"
|
||||
>
|
||||
{{ num }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 随球选择 -->
|
||||
<div class="ball-selection-group">
|
||||
<el-divider>{{ getSlaveBallLabel() }}</el-divider>
|
||||
<div class="ball-grid">
|
||||
<el-button
|
||||
v-for="num in getSlaveBallRange()"
|
||||
:key="'slave-' + num"
|
||||
:class="{
|
||||
active: slaveBall === num,
|
||||
'red-ball': isSlaveFront(),
|
||||
'blue-ball': !isSlaveFront()
|
||||
}"
|
||||
:type="slaveBall === num ? 'primary' : 'default'"
|
||||
:plain="slaveBall !== num"
|
||||
circle
|
||||
@click="selectSlaveBall(num)"
|
||||
>
|
||||
{{ num }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分析按钮 -->
|
||||
<div class="analyze-section">
|
||||
<el-button
|
||||
type="success"
|
||||
size="large"
|
||||
round
|
||||
:disabled="!canAnalyze || loading"
|
||||
:loading="loading"
|
||||
@click="performAnalysis"
|
||||
>
|
||||
{{ loading ? '分析中...' : '开始分析' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分析结果 -->
|
||||
<div v-if="result !== null || error" class="result-container">
|
||||
<el-alert
|
||||
v-if="error"
|
||||
:title="error"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
>
|
||||
<template #default>
|
||||
<el-button @click="clearError" type="primary" size="small">重试</el-button>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-result
|
||||
v-else-if="result !== null"
|
||||
icon="success"
|
||||
title="组合性分析结果"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="result-details">
|
||||
|
||||
<div class="table-wrapper">
|
||||
<div class="table-title-row">
|
||||
<div class="table-title-cell">
|
||||
<span class="highlight-ball" :class="{ 'blue-text': !isMasterFront() }">{{masterBall}}</span>号{{isMasterFront() ? '前区' : '后区'}}球与<span class="highlight-ball" :class="{ 'blue-text': !isSlaveFront() }">{{slaveBall}}</span>号{{isSlaveFront() ? '前区' : '后区'}}球组合性分析报告
|
||||
</div>
|
||||
</div>
|
||||
<table class="analysis-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label-cell">引用数据截至</td>
|
||||
<td class="value-cell">{{resultData[0]?.latestDrawId || '-'}}期</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">两号组合系数</td>
|
||||
<td class="value-cell">{{resultData[0]?.faceCoefficient || '-'}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">同号组最高系数球号及系数</td>
|
||||
<td class="value-cell">
|
||||
<span class="highlight-ball" :class="{ 'blue-text': !isSlaveFront() }">{{resultData[0]?.highestBall || '--'}}</span>
|
||||
<span class="coefficient-value">{{resultData[0]?.highestCoefficient || '-'}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">同号组最低系数球号及系数</td>
|
||||
<td class="value-cell">
|
||||
<span class="highlight-ball" :class="{ 'blue-text': !isSlaveFront() }">{{resultData[0]?.lowestBall || '--'}}</span>
|
||||
<span class="coefficient-value">{{resultData[0]?.lowestCoefficient || '-'}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">同号组平均系数</td>
|
||||
<td class="value-cell">{{ (resultData[0]?.averageCoefficient && Number(resultData[0].averageCoefficient).toFixed(2)) || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">建议</td>
|
||||
<td class="value-cell recommendation">
|
||||
系数越高表示该此组合概率较高,高于平均系数的组合更值得关注。同时,可尝试寻找关联组合与交叉组合的共性规律。
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" @click="resetAnalysis">重新分析</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<el-card v-if="!analysisType" class="instruction-container" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span><el-icon><InfoFilled /></el-icon> 组合性分析说明</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-collapse accordion>
|
||||
<el-collapse-item title="前区与前区分析" name="front-front">
|
||||
<div class="collapse-content">
|
||||
<p>分析两个前区球号码之间的组合关系,计算前区配对的组合系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="前区与后区分析" name="front-back">
|
||||
<div class="collapse-content">
|
||||
<p>分析前区与后区球号码的组合关系,计算前后配对的组合系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="后区与后区分析" name="back-back">
|
||||
<div class="collapse-content">
|
||||
<p>分析两个后区球号码之间的组合关系,计算后区配对的组合系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="后区与前区分析" name="back-front">
|
||||
<div class="collapse-content">
|
||||
<p>分析后区与前区球号码的组合关系,计算后前配对的组合系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
<el-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-top: 20px;"
|
||||
>
|
||||
<template #title>
|
||||
<span>选择分析类型后,依次选择主球和随球号码,点击"开始分析"获取组合系数值</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { dltLotteryApi } from '@/api/dlt'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElAvatar,
|
||||
ElDivider,
|
||||
ElAlert,
|
||||
ElResult,
|
||||
ElTag,
|
||||
ElStatistic,
|
||||
ElTooltip,
|
||||
ElIcon,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElTable,
|
||||
ElTableColumn
|
||||
} from 'element-plus'
|
||||
import { ArrowLeft, InfoFilled } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'DltSurfaceAnalysis',
|
||||
components: {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElAvatar,
|
||||
ElDivider,
|
||||
ElAlert,
|
||||
ElResult,
|
||||
ElTag,
|
||||
ElStatistic,
|
||||
ElTooltip,
|
||||
ElIcon,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
InfoFilled
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
analysisType: '', // 'front-front', 'front-back', 'back-back', 'back-front'
|
||||
masterBall: null,
|
||||
slaveBall: null,
|
||||
loading: false,
|
||||
result: null,
|
||||
error: null,
|
||||
selectionError: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canAnalyze() {
|
||||
return this.masterBall !== null && this.slaveBall !== null && this.analysisType
|
||||
},
|
||||
|
||||
resultData() {
|
||||
if (!this.result) return [];
|
||||
|
||||
try {
|
||||
const data = typeof this.result === 'string' ? JSON.parse(this.result) : this.result;
|
||||
|
||||
return [{
|
||||
latestDrawId: data.latestDrawId || '',
|
||||
faceCoefficient: data.faceCoefficient || 0,
|
||||
highestCoefficient: data.highestCoefficient || 0,
|
||||
lowestCoefficient: data.lowestCoefficient || 0,
|
||||
averageCoefficient: data.averageCoefficient || 0,
|
||||
highestBall: data.highestBall || '-',
|
||||
lowestBall: data.lowestBall || '-'
|
||||
}];
|
||||
} catch (e) {
|
||||
console.error('解析结果数据失败', e);
|
||||
return [{
|
||||
latestDrawId: '',
|
||||
faceCoefficient: 0,
|
||||
highestCoefficient: 0,
|
||||
lowestCoefficient: 0,
|
||||
averageCoefficient: 0,
|
||||
highestBall: '-',
|
||||
lowestBall: '-'
|
||||
}];
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
// 获取当前彩票类型,优先使用路由参数,否则使用默认值 'dlt'
|
||||
const lotteryType = this.$route.query.lotteryType || 'dlt'
|
||||
this.$router.push({
|
||||
path: '/data-analysis',
|
||||
query: { lotteryType: lotteryType }
|
||||
})
|
||||
},
|
||||
|
||||
selectAnalysisType(type) {
|
||||
this.analysisType = type
|
||||
this.masterBall = null
|
||||
this.slaveBall = null
|
||||
this.result = null
|
||||
this.error = null
|
||||
this.selectionError = null
|
||||
},
|
||||
|
||||
getSelectionTitle() {
|
||||
const titles = {
|
||||
'front-front': '前区与前区组合分析(不能重复)',
|
||||
'front-back': '前区与后区组合分析',
|
||||
'back-back': '后区与后区组合分析(不能重复)',
|
||||
'back-front': '后区与前区组合分析'
|
||||
}
|
||||
return titles[this.analysisType] || ''
|
||||
},
|
||||
|
||||
getMasterBallLabel() {
|
||||
const labels = {
|
||||
'front-front': '主球(前区)',
|
||||
'front-back': '主球(前区)',
|
||||
'back-back': '主球(后区)',
|
||||
'back-front': '主球(后区)'
|
||||
}
|
||||
return labels[this.analysisType] || ''
|
||||
},
|
||||
|
||||
getSlaveBallLabel() {
|
||||
const labels = {
|
||||
'front-front': '随球(前区)',
|
||||
'front-back': '随球(后区)',
|
||||
'back-back': '随球(后区)',
|
||||
'back-front': '随球(前区)'
|
||||
}
|
||||
return labels[this.analysisType] || ''
|
||||
},
|
||||
|
||||
getMasterBallRange() {
|
||||
if (this.analysisType === 'back-back' || this.analysisType === 'back-front') {
|
||||
return Array.from({ length: 12 }, (_, i) => i + 1) // 1-12 后区
|
||||
}
|
||||
return Array.from({ length: 35 }, (_, i) => i + 1) // 1-35 前区
|
||||
},
|
||||
|
||||
getSlaveBallRange() {
|
||||
if (this.analysisType === 'front-back' || this.analysisType === 'back-back') {
|
||||
return Array.from({ length: 12 }, (_, i) => i + 1) // 1-12 后区
|
||||
}
|
||||
return Array.from({ length: 35 }, (_, i) => i + 1) // 1-35 前区
|
||||
},
|
||||
|
||||
isMasterFront() {
|
||||
return this.analysisType === 'front-front' || this.analysisType === 'front-back'
|
||||
},
|
||||
|
||||
isSlaveFront() {
|
||||
return this.analysisType === 'front-front' || this.analysisType === 'back-front'
|
||||
},
|
||||
|
||||
selectMasterBall(num) {
|
||||
// 前区与前区、后区与后区不能选择相同号码
|
||||
if ((this.analysisType === 'front-front' || this.analysisType === 'back-back') && num === this.slaveBall) {
|
||||
this.selectionError = '主球和随球不能是同一个号码'
|
||||
return
|
||||
}
|
||||
this.masterBall = num
|
||||
this.result = null
|
||||
this.error = null
|
||||
this.selectionError = null
|
||||
},
|
||||
|
||||
selectSlaveBall(num) {
|
||||
// 前区与前区、后区与后区不能选择相同号码
|
||||
if ((this.analysisType === 'front-front' || this.analysisType === 'back-back') && num === this.masterBall) {
|
||||
this.selectionError = '主球和随球不能是同一个号码'
|
||||
return
|
||||
}
|
||||
this.slaveBall = num
|
||||
this.result = null
|
||||
this.error = null
|
||||
this.selectionError = null
|
||||
},
|
||||
|
||||
async performAnalysis() {
|
||||
if (!this.canAnalyze) return
|
||||
|
||||
this.loading = true
|
||||
this.error = null
|
||||
this.result = null
|
||||
|
||||
try {
|
||||
let response
|
||||
|
||||
switch (this.analysisType) {
|
||||
case 'front-front':
|
||||
response = await dltLotteryApi.frontFrontCombinationAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
case 'front-back':
|
||||
response = await dltLotteryApi.frontBackCombinationAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
case 'back-back':
|
||||
response = await dltLotteryApi.backBackCombinationAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
case 'back-front':
|
||||
response = await dltLotteryApi.backFrontCombinationAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
this.result = response.data
|
||||
} else {
|
||||
this.error = response.message || '分析失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('组合系数分析失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
clearError() {
|
||||
this.error = null
|
||||
},
|
||||
|
||||
resetAnalysis() {
|
||||
this.masterBall = null
|
||||
this.slaveBall = null
|
||||
this.result = null
|
||||
this.error = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.surface-analysis {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 返回按钮 */
|
||||
.back-button-container {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #7f8c8d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 分析类型卡片 */
|
||||
.analysis-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.analysis-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.analysis-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.15);
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.analysis-card.active {
|
||||
background-color: #eaf5ff;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
|
||||
.analysis-card.active :deep(.el-card__body) {
|
||||
background: transparent;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
/* 减少卡片内边距 */
|
||||
:deep(.el-card__body) {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.red-ball-icon {
|
||||
background-color: #e74c3c !important;
|
||||
}
|
||||
|
||||
.blue-ball-icon {
|
||||
background-color: #3498db !important;
|
||||
}
|
||||
|
||||
.btn-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.btn-desc {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.analysis-card.active .btn-title {
|
||||
color: #2980b9;
|
||||
}
|
||||
|
||||
.analysis-card.active .btn-desc {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.number-selection {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.number-selection h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ball-selection-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
/* 球号网格 */
|
||||
.ball-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
/* 前区球(红球)和后区球(蓝球)按钮样式 */
|
||||
:deep(.el-button.red-ball.is-plain:not(.is-disabled)) {
|
||||
color: #e74c3c;
|
||||
border-color: #e74c3c;
|
||||
background-color: #fff5f5;
|
||||
}
|
||||
|
||||
:deep(.el-button.red-ball:not(.is-plain):not(.is-disabled)) {
|
||||
background-color: #e74c3c;
|
||||
border-color: #c0392b;
|
||||
}
|
||||
|
||||
:deep(.el-button.blue-ball.is-plain:not(.is-disabled)) {
|
||||
color: #3498db;
|
||||
border-color: #3498db;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
:deep(.el-button.blue-ball:not(.is-plain):not(.is-disabled)) {
|
||||
background-color: #3498db;
|
||||
border-color: #2980b9;
|
||||
}
|
||||
|
||||
/* 分析按钮 */
|
||||
.analyze-section {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* 结果容器 */
|
||||
.result-container {
|
||||
border-top: 2px solid #ecf0f1;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-result) {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.result-details {
|
||||
width: 100%;
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ball-combo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.combo-label {
|
||||
font-size: 16px;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.combo-separator {
|
||||
font-size: 20px;
|
||||
color: #7f8c8d;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ball-tag {
|
||||
padding: 8px 12px;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
border-radius: 50%;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.coefficient-result {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
:deep(.el-statistic__head) {
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
font-size: 24px !important;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
/* 使用说明 */
|
||||
.instruction-container {
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
padding: 10px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto 20px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table-title-row {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.table-title-cell {
|
||||
padding: 8px;
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.analysis-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #000;
|
||||
border-top: none;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.analysis-table tr {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.analysis-table tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label-cell {
|
||||
width: 150px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #000;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.value-cell {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.value-cell .ball-number {
|
||||
display: inline-block;
|
||||
min-width: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.value-cell .highlight-ball {
|
||||
display: inline-block;
|
||||
min-width: 30px;
|
||||
text-align: left;
|
||||
color: #e74c3c; /* 红色文字 */
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.highlight-ball {
|
||||
color: #e74c3c; /* 红色文字 */
|
||||
font-weight: bold;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.highlight-ball.blue-text {
|
||||
color: #3498db; /* 蓝色文字 */
|
||||
}
|
||||
|
||||
.coefficient-value {
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.value-cell .coefficient-value {
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
padding: 5px;
|
||||
white-space: normal;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.report-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin: 20px 0 15px;
|
||||
text-align: center;
|
||||
padding-bottom: 10px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
display: none; /* 隐藏原来的标题,使用表格自身的标题 */
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.surface-analysis {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.back-button-container {
|
||||
position: static;
|
||||
text-align: left;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.analysis-buttons {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-desc {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ball-grid {
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.table-title-cell {
|
||||
font-size: 14px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.label-cell, .value-cell {
|
||||
padding: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
817
src/views/dlt/TrendAnalysis.vue
Normal file
@@ -0,0 +1,817 @@
|
||||
<template>
|
||||
<div class="trend-analysis">
|
||||
<div class="header">
|
||||
<!-- 返回按钮 -->
|
||||
<div class="back-button-container">
|
||||
<el-button @click="goBack" icon="ArrowLeft" size="medium">
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<h2>活跃性分析</h2>
|
||||
<p>查看前区球和后区球历史数据统计</p>
|
||||
|
||||
<!-- 球类选择切换 -->
|
||||
<div class="ball-type-tabs">
|
||||
<el-radio-group v-model="ballType" @change="switchBallType">
|
||||
<el-radio-button label="frontend">🔴 前区球</el-radio-button>
|
||||
<el-radio-button label="backend">🔵 后区球</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能按钮区域 -->
|
||||
<div class="analysis-buttons">
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: currentView === 'all' }"
|
||||
@click="loadData('all')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-icon size="32" color="#5856d6"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">历史数据</div>
|
||||
<div class="btn-desc">查看所有历史数据</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: currentView === 'top' }"
|
||||
@click="loadData('top')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-icon size="32" color="#ff9500"><Sort /></el-icon>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">历史排行</div>
|
||||
<div class="btn-desc">历史数据排行榜</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: currentView === '100' }"
|
||||
@click="loadData('100')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-icon size="32" color="#34c759"><Histogram /></el-icon>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">百期数据</div>
|
||||
<div class="btn-desc">最近100期数据</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: currentView === 'top100' }"
|
||||
@click="loadData('top100')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-icon size="32" color="#ff3b30"><Menu /></el-icon>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">百期排行</div>
|
||||
<div class="btn-desc">最近100期数据排行</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 数据显示区域 -->
|
||||
<el-card class="data-container" shadow="never">
|
||||
<div v-if="loading" class="loading">
|
||||
<el-skeleton :rows="10" animated />
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-else-if="error"
|
||||
:title="error"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
>
|
||||
<template #default>
|
||||
<el-button @click="retryLoad" type="primary" size="small">重试</el-button>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<div v-else-if="tableData.length > 0" class="data-table-container">
|
||||
<div class="table-header">
|
||||
<h3>
|
||||
{{ getTableTitleText() }}
|
||||
<span v-if="latestDrawId" class="draw-id-suffix">
|
||||
(引用数据截至<span class="draw-id-number">{{ latestDrawId }}</span>期)
|
||||
</span>
|
||||
</h3>
|
||||
<div class="table-info">
|
||||
<span>共 {{ tableData.length }} 条记录</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="paginatedData" stripe style="width: 100%">
|
||||
<el-table-column
|
||||
v-for="column in getTableColumns()"
|
||||
:key="column.key"
|
||||
:prop="column.key"
|
||||
:label="column.title"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span :class="getColumnClass(column.key, scope.row[column.key])">
|
||||
{{ formatValue(column.key, scope.row[column.key], scope.$index) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:total="tableData.length"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!loading" description="暂无数据" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { dltLotteryApi } from '@/api/dlt'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElRadioGroup,
|
||||
ElRadioButton,
|
||||
ElSkeleton,
|
||||
ElAlert,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElPagination,
|
||||
ElEmpty,
|
||||
ElIcon
|
||||
} from 'element-plus'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Document,
|
||||
Sort,
|
||||
Histogram,
|
||||
Menu
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'DltTrendAnalysis',
|
||||
components: {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElRadioGroup,
|
||||
ElRadioButton,
|
||||
ElSkeleton,
|
||||
ElAlert,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElPagination,
|
||||
ElEmpty,
|
||||
ElIcon,
|
||||
Document,
|
||||
Sort,
|
||||
Histogram,
|
||||
Menu
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
error: null,
|
||||
currentView: '',
|
||||
ballType: 'frontend', // 'frontend' 或 'backend'
|
||||
tableData: [],
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
latestDrawId: null // 最新期号
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 获取最新期号
|
||||
this.fetchLatestDrawId()
|
||||
},
|
||||
computed: {
|
||||
totalPages() {
|
||||
return Math.ceil(this.tableData.length / this.pageSize)
|
||||
},
|
||||
paginatedData() {
|
||||
const start = (this.currentPage - 1) * this.pageSize
|
||||
return this.tableData.slice(start, start + this.pageSize)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadFrontendHistoryAll() {
|
||||
this.currentView = 'all'
|
||||
this.currentPage = 1
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await dltLotteryApi.getFrontendHistoryAll()
|
||||
if (response.success) {
|
||||
this.tableData = response.data || []
|
||||
} else {
|
||||
this.error = response.message || '获取数据失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取前区历史全部记录失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadFrontendHistory100() {
|
||||
this.currentView = '100'
|
||||
this.currentPage = 1
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await dltLotteryApi.getFrontendHistory100()
|
||||
if (response.success) {
|
||||
this.tableData = response.data || []
|
||||
} else {
|
||||
this.error = response.message || '获取数据失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取前区最近100期记录失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadFrontendHistoryTop() {
|
||||
this.currentView = 'top'
|
||||
this.currentPage = 1
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await dltLotteryApi.getFrontendHistoryTop()
|
||||
if (response.success) {
|
||||
this.tableData = response.data || []
|
||||
} else {
|
||||
this.error = response.message || '获取数据失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取前区历史排行记录失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadFrontendHistoryTop100() {
|
||||
this.currentView = 'top100'
|
||||
this.currentPage = 1
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await dltLotteryApi.getFrontendHistoryTop100()
|
||||
if (response.success) {
|
||||
this.tableData = response.data || []
|
||||
} else {
|
||||
this.error = response.message || '获取数据失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取前区100期排行记录失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 后区数据获取方法
|
||||
async loadBackendHistoryAll() {
|
||||
this.currentView = 'all'
|
||||
this.currentPage = 1
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await dltLotteryApi.getBackendHistoryAll()
|
||||
if (response.success) {
|
||||
this.tableData = response.data || []
|
||||
} else {
|
||||
this.error = response.message || '获取数据失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取后区历史全部记录失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadBackendHistory100() {
|
||||
this.currentView = '100'
|
||||
this.currentPage = 1
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await dltLotteryApi.getBackendHistory100()
|
||||
if (response.success) {
|
||||
this.tableData = response.data || []
|
||||
} else {
|
||||
this.error = response.message || '获取数据失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取后区最近100期记录失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadBackendHistoryTop() {
|
||||
this.currentView = 'top'
|
||||
this.currentPage = 1
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await dltLotteryApi.getBackendHistoryTop()
|
||||
if (response.success) {
|
||||
this.tableData = response.data || []
|
||||
} else {
|
||||
this.error = response.message || '获取数据失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取后区历史排行记录失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadBackendHistoryTop100() {
|
||||
this.currentView = 'top100'
|
||||
this.currentPage = 1
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await dltLotteryApi.getBackendHistoryTop100()
|
||||
if (response.success) {
|
||||
this.tableData = response.data || []
|
||||
} else {
|
||||
this.error = response.message || '获取数据失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取后区100期排行记录失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 切换球类型
|
||||
switchBallType() {
|
||||
this.currentView = ''
|
||||
this.tableData = []
|
||||
this.currentPage = 1
|
||||
this.error = null
|
||||
},
|
||||
|
||||
// 统一的数据加载方法
|
||||
loadData(viewType) {
|
||||
if (this.ballType === 'frontend') {
|
||||
switch (viewType) {
|
||||
case 'all':
|
||||
this.loadFrontendHistoryAll()
|
||||
break
|
||||
case '100':
|
||||
this.loadFrontendHistory100()
|
||||
break
|
||||
case 'top':
|
||||
this.loadFrontendHistoryTop()
|
||||
break
|
||||
case 'top100':
|
||||
this.loadFrontendHistoryTop100()
|
||||
break
|
||||
}
|
||||
} else {
|
||||
switch (viewType) {
|
||||
case 'all':
|
||||
this.loadBackendHistoryAll()
|
||||
break
|
||||
case '100':
|
||||
this.loadBackendHistory100()
|
||||
break
|
||||
case 'top':
|
||||
this.loadBackendHistoryTop()
|
||||
break
|
||||
case 'top100':
|
||||
this.loadBackendHistoryTop100()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getTableTitleText() {
|
||||
const ballTypeText = this.ballType === 'frontend' ? '前区球' : '后区球'
|
||||
const titles = {
|
||||
'all': `${ballTypeText}历史数据`,
|
||||
'100': `${ballTypeText}最近100期数据`,
|
||||
'top': `${ballTypeText}历史数据排行`,
|
||||
'top100': `${ballTypeText}最近100期数据排行`
|
||||
}
|
||||
return titles[this.currentView] || '数据列表'
|
||||
},
|
||||
|
||||
// 获取最新期号
|
||||
async fetchLatestDrawId() {
|
||||
try {
|
||||
const response = await dltLotteryApi.getRecentDraws(1)
|
||||
if (response.data && response.data.length > 0) {
|
||||
this.latestDrawId = response.data[0].drawId
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取最新期号失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
getTableColumns() {
|
||||
// 根据不同的视图返回不同的列配置
|
||||
if (this.currentView === 'all') {
|
||||
// 历史全部记录 - 显示完整数据
|
||||
return [
|
||||
{ key: 'ballNumber', title: '球号' },
|
||||
{ key: 'frequencyCount', title: '出现次数' },
|
||||
{ key: 'frequencyPercentage', title: '出现频率(%)' },
|
||||
{ key: 'averageHiddenAppear', title: '平均隐现期' },
|
||||
{ key: 'maxHiddenInterval', title: '最长隐现期' },
|
||||
{ key: 'maxConsecutive', title: '最大连出期' },
|
||||
{ key: 'activeCoefficient', title: '活跃系数' }
|
||||
]
|
||||
} else if (this.currentView === '100') {
|
||||
// 最近100期 - 只显示有数据的列
|
||||
return [
|
||||
{ key: 'ballNumber', title: '球号' },
|
||||
{ key: 'frequencyCount', title: '出现次数' },
|
||||
{ key: 'averageHiddenAppear', title: '平均隐现期' },
|
||||
{ key: 'nowInterval', title: '当前隐现期' },
|
||||
{ key: 'maxConsecutive', title: '最大连出期' },
|
||||
{ key: 'activeCoefficient', title: '活跃系数' }
|
||||
]
|
||||
} else if (this.currentView === 'top' || this.currentView === 'top100') {
|
||||
return [
|
||||
{ key: 'ranking', title: '排名' },
|
||||
{ key: 'ballNumber', title: '球号' },
|
||||
{ key: 'activeCoefficient', title: '活跃系数' }
|
||||
]
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
getColumnClass(key, value) {
|
||||
if (key === 'ballNumber') {
|
||||
return this.ballType === 'frontend' ? 'ball-number red-ball' : 'ball-number blue-ball'
|
||||
} else if (key === 'activeCoefficient') {
|
||||
return 'point-coefficient'
|
||||
} else if (key === 'ranking') {
|
||||
return 'ranking'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
formatValue(key, value, index) {
|
||||
if (key === 'ranking') {
|
||||
// 稠密排名(同分同名次,名次不跳跃),基于整表数据
|
||||
const realIndex = (this.currentPage - 1) * this.pageSize + index
|
||||
if (!Array.isArray(this.tableData) || realIndex < 0 || realIndex >= this.tableData.length) {
|
||||
return realIndex + 1
|
||||
}
|
||||
let rank = 1
|
||||
let lastVal = null
|
||||
for (let i = 0; i <= realIndex; i++) {
|
||||
const item = this.tableData[i]
|
||||
const val = item ? item.activeCoefficient : null
|
||||
if (i === 0) {
|
||||
lastVal = val
|
||||
} else if (val !== lastVal) {
|
||||
rank++
|
||||
lastVal = val
|
||||
}
|
||||
}
|
||||
return rank
|
||||
} else if (key === 'frequencyPercentage' && typeof value === 'number') {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
return value
|
||||
},
|
||||
|
||||
getRealRanking(currentIndex) {
|
||||
// 如果不是排行榜视图,使用简单的索引排名
|
||||
if (this.currentView !== 'top' && this.currentView !== 'top100') {
|
||||
return (this.currentPage - 1) * this.pageSize + currentIndex + 1
|
||||
}
|
||||
|
||||
// 获取当前项的系数值
|
||||
const currentItem = this.paginatedData[currentIndex]
|
||||
if (!currentItem || !currentItem.activeCoefficient) {
|
||||
return (this.currentPage - 1) * this.pageSize + currentIndex + 1
|
||||
}
|
||||
|
||||
const currentCoefficient = currentItem.activeCoefficient
|
||||
let realRank = 1
|
||||
|
||||
// 遍历所有数据,计算真实排名
|
||||
for (let i = 0; i < this.tableData.length; i++) {
|
||||
const item = this.tableData[i]
|
||||
if (item && item.activeCoefficient > currentCoefficient) {
|
||||
realRank++
|
||||
}
|
||||
}
|
||||
|
||||
return realRank
|
||||
},
|
||||
|
||||
retryLoad() {
|
||||
if (this.currentView) {
|
||||
this.loadData(this.currentView)
|
||||
}
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
// 获取当前彩票类型,优先使用路由参数,否则使用默认值 'dlt'
|
||||
const lotteryType = this.$route.query.lotteryType || 'dlt'
|
||||
this.$router.push({
|
||||
path: '/data-analysis',
|
||||
query: { lotteryType: lotteryType }
|
||||
})
|
||||
},
|
||||
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--
|
||||
}
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.currentPage++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trend-analysis {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.back-button-container {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #7f8c8d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ball-type-tabs {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.analysis-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.analysis-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.analysis-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.15);
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.analysis-card.active {
|
||||
background-color: #eaf5ff;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
|
||||
.analysis-card.active :deep(.el-card__body) {
|
||||
background: transparent;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
/* 减少卡片内边距 */
|
||||
:deep(.el-card__body) {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
margin-bottom: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.analysis-card:hover .btn-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.analysis-card.active .btn-title {
|
||||
color: #2980b9;
|
||||
}
|
||||
|
||||
.btn-desc {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.analysis-card.active .btn-desc {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.data-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.table-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.draw-id-suffix {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.draw-id-number {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-info {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ball-number {
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.red-ball {
|
||||
background: #e74c3c;
|
||||
}
|
||||
|
||||
.blue-ball {
|
||||
background: #3498db;
|
||||
}
|
||||
|
||||
.point-coefficient {
|
||||
color: #e74c3c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ranking {
|
||||
background: linear-gradient(135deg, #8e9aaf 0%, #6c757d 100%);
|
||||
color: white;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
display: inline-block;
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(108, 117, 125, 0.25);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ranking:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(108, 117, 125, 0.35);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.trend-analysis {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.back-button-container {
|
||||
position: static;
|
||||
text-align: left;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.analysis-buttons {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.table-header h3 {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.draw-id-suffix {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ranking {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
min-width: 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
719
src/views/dlt/UsageStats.vue
Normal file
4108
src/views/jt/DltHome.vue
Normal file
3786
src/views/jt/SsqHome.vue
Normal file
791
src/views/ssq/HitAnalysis.vue
Normal file
3738
src/views/ssq/Home.vue
Normal file
767
src/views/ssq/LineAnalysis.vue
Normal file
@@ -0,0 +1,767 @@
|
||||
<template>
|
||||
<div class="line-analysis">
|
||||
<div class="header">
|
||||
<!-- 返回按钮 -->
|
||||
<div class="back-button-container">
|
||||
<el-button @click="goBack" icon="ArrowLeft" size="medium">
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<h2>接续性分析</h2>
|
||||
<p>球号接续分析,把握上依下托</p>
|
||||
</div>
|
||||
|
||||
<!-- 分析类型选择 -->
|
||||
<div class="analysis-buttons">
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'red-red' }"
|
||||
@click="selectAnalysisType('red-red')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">红球与红球</div>
|
||||
<div class="btn-desc">红球接续性分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'blue-blue' }"
|
||||
@click="selectAnalysisType('blue-blue')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">蓝球与蓝球</div>
|
||||
<div class="btn-desc">蓝球接续性分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'red-blue' }"
|
||||
@click="selectAnalysisType('red-blue')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">红球与蓝球</div>
|
||||
<div class="btn-desc">红蓝接续性分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="analysis-card"
|
||||
:class="{ active: analysisType === 'blue-red' }"
|
||||
@click="selectAnalysisType('blue-red')"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="btn-icon">
|
||||
<el-avatar class="blue-ball-icon"></el-avatar>
|
||||
<el-avatar class="red-ball-icon"></el-avatar>
|
||||
</div>
|
||||
<div class="btn-text">
|
||||
<div class="btn-title">蓝球与红球</div>
|
||||
<div class="btn-desc">蓝红接续性分析</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 号码选择和分析区域 -->
|
||||
<el-card v-if="analysisType" class="analysis-container" shadow="never">
|
||||
<div class="number-selection">
|
||||
<h3>{{ getSelectionTitle() }}</h3>
|
||||
|
||||
<!-- 主球选择 -->
|
||||
<div class="ball-selection-group">
|
||||
<el-divider>{{ getMasterBallLabel() }}</el-divider>
|
||||
<div class="ball-grid">
|
||||
<el-button
|
||||
v-for="num in getMasterBallRange()"
|
||||
:key="'master-' + num"
|
||||
:class="{
|
||||
active: masterBall === num,
|
||||
'red-ball': isMasterRed(),
|
||||
'blue-ball': !isMasterRed()
|
||||
}"
|
||||
:type="masterBall === num ? 'primary' : 'default'"
|
||||
:plain="masterBall !== num"
|
||||
circle
|
||||
@click="selectMasterBall(num)"
|
||||
>
|
||||
{{ num }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 随球选择 -->
|
||||
<div class="ball-selection-group">
|
||||
<el-divider>{{ getSlaveBallLabel() }}</el-divider>
|
||||
<div class="ball-grid">
|
||||
<el-button
|
||||
v-for="num in getSlaveBallRange()"
|
||||
:key="'slave-' + num"
|
||||
:class="{
|
||||
active: slaveBall === num,
|
||||
'red-ball': isSlaveRed(),
|
||||
'blue-ball': !isSlaveRed()
|
||||
}"
|
||||
:type="slaveBall === num ? 'primary' : 'default'"
|
||||
:plain="slaveBall !== num"
|
||||
circle
|
||||
@click="selectSlaveBall(num)"
|
||||
>
|
||||
{{ num }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分析按钮 -->
|
||||
<div class="analyze-section">
|
||||
<el-button
|
||||
type="success"
|
||||
size="large"
|
||||
round
|
||||
:disabled="!canAnalyze || loading"
|
||||
:loading="loading"
|
||||
@click="performAnalysis"
|
||||
>
|
||||
{{ loading ? '分析中...' : '开始分析' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分析结果 -->
|
||||
<div v-if="result !== null || error" class="result-container">
|
||||
<el-alert
|
||||
v-if="error"
|
||||
:title="error"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
>
|
||||
<template #default>
|
||||
<el-button @click="clearError" type="primary" size="small">重试</el-button>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-result
|
||||
v-else-if="result !== null"
|
||||
icon="success"
|
||||
title="接续性分析结果"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="result-details">
|
||||
<div class="table-wrapper">
|
||||
<div class="table-title-row">
|
||||
<div class="table-title-cell">
|
||||
<span class="highlight-ball" :class="{ 'blue-text': !isMasterRed() }">{{masterBall}}</span>号{{isMasterRed() ? '红' : '蓝'}}球与<span class="highlight-ball" :class="{ 'blue-text': !isSlaveRed() }">{{slaveBall}}</span>号{{isSlaveRed() ? '红' : '蓝'}}球接续性分析报告
|
||||
</div>
|
||||
</div>
|
||||
<table class="analysis-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label-cell">引用数据截至</td>
|
||||
<td class="value-cell">{{resultData[0]?.latestDrawId || '-'}}期</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">两号接续系数</td>
|
||||
<td class="value-cell">{{resultData[0]?.lineCoefficient || '-'}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">同号组最高系数球号及系数</td>
|
||||
<td class="value-cell">
|
||||
<span class="highlight-ball" :class="{ 'blue-text': !isSlaveRed() }">{{resultData[0]?.highestBall || '--'}}</span>
|
||||
<span class="coefficient-value">{{resultData[0]?.highestCoefficient || '-'}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">同号组最低系数球号及系数</td>
|
||||
<td class="value-cell">
|
||||
<span class="highlight-ball" :class="{ 'blue-text': !isSlaveRed() }">{{resultData[0]?.lowestBall || '--'}}</span>
|
||||
<span class="coefficient-value">{{resultData[0]?.lowestCoefficient || '-'}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">同号组平均系数</td>
|
||||
<td class="value-cell">{{ (resultData[0]?.averageCoefficient && Number(resultData[0].averageCoefficient).toFixed(2)) || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">建议</td>
|
||||
<td class="value-cell recommendation">
|
||||
系数越高表示彼此接续越频繁。可进行多球接续系数比对,一般观察,高于平均系数的接续关系更值得关注。
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" @click="resetAnalysis">重新分析</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<el-card v-if="!analysisType" class="instruction-container" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span><el-icon><InfoFilled /></el-icon> 接续性分析说明</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-collapse accordion>
|
||||
<el-collapse-item title="红球与红球分析" name="red-red">
|
||||
<div class="collapse-content">
|
||||
<p>分析两个红球号码之间的接续性关系,计算红球配对的接续系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="蓝球与蓝球分析" name="blue-blue">
|
||||
<div class="collapse-content">
|
||||
<p>分析两个蓝球号码之间的接续性关系,计算蓝球配对的接续系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="红球与蓝球分析" name="red-blue">
|
||||
<div class="collapse-content">
|
||||
<p>分析红球与蓝球号码的接续性关系,计算红蓝配对的接续系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="蓝球与红球分析" name="blue-red">
|
||||
<div class="collapse-content">
|
||||
<p>分析蓝球与红球号码的接续性关系,计算蓝红配对的接续系数</p>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
<el-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-top: 20px;"
|
||||
>
|
||||
<template #title>
|
||||
<span>选择分析类型后,依次选择主球和随球号码,点击"开始分析"获取接续系数值</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { lotteryApi } from '@/api'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElAvatar,
|
||||
ElDivider,
|
||||
ElAlert,
|
||||
ElResult,
|
||||
ElIcon,
|
||||
ElCollapse,
|
||||
ElCollapseItem
|
||||
} from 'element-plus'
|
||||
import { ArrowLeft, InfoFilled } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'LineAnalysis',
|
||||
components: {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElAvatar,
|
||||
ElDivider,
|
||||
ElAlert,
|
||||
ElResult,
|
||||
ElIcon,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
InfoFilled
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
analysisType: '', // 'red-red', 'blue-blue', 'red-blue', 'blue-red'
|
||||
masterBall: null,
|
||||
slaveBall: null,
|
||||
loading: false,
|
||||
result: null,
|
||||
error: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canAnalyze() {
|
||||
return this.masterBall !== null && this.slaveBall !== null && this.analysisType
|
||||
},
|
||||
|
||||
resultData() {
|
||||
if (!this.result) return [];
|
||||
|
||||
try {
|
||||
const data = typeof this.result === 'string' ? JSON.parse(this.result) : this.result;
|
||||
|
||||
return [{
|
||||
latestDrawId: data.latestDrawId || '',
|
||||
lineCoefficient: data.lineCoefficient || 0,
|
||||
highestCoefficient: data.highestCoefficient || 0,
|
||||
lowestCoefficient: data.lowestCoefficient || 0,
|
||||
averageCoefficient: data.averageCoefficient || 0,
|
||||
highestBall: data.highestBall || '-',
|
||||
lowestBall: data.lowestBall || '-'
|
||||
}];
|
||||
} catch (e) {
|
||||
console.error('解析结果数据失败', e);
|
||||
return [{
|
||||
latestDrawId: '',
|
||||
lineCoefficient: 0,
|
||||
highestCoefficient: 0,
|
||||
lowestCoefficient: 0,
|
||||
averageCoefficient: 0,
|
||||
highestBall: '-',
|
||||
lowestBall: '-'
|
||||
}];
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
// 获取当前彩票类型,优先使用路由参数,否则使用默认值 'ssq'
|
||||
const lotteryType = this.$route.query.lotteryType || 'ssq'
|
||||
this.$router.push({
|
||||
path: '/data-analysis',
|
||||
query: { lotteryType: lotteryType }
|
||||
})
|
||||
},
|
||||
|
||||
selectAnalysisType(type) {
|
||||
this.analysisType = type
|
||||
this.masterBall = null
|
||||
this.slaveBall = null
|
||||
this.result = null
|
||||
this.error = null
|
||||
},
|
||||
|
||||
getSelectionTitle() {
|
||||
const titles = {
|
||||
'red-red': '红球与红球接续性分析',
|
||||
'blue-blue': '蓝球与蓝球接续性分析',
|
||||
'red-blue': '红球与蓝球接续性分析',
|
||||
'blue-red': '蓝球与红球接续性分析'
|
||||
}
|
||||
return titles[this.analysisType] || ''
|
||||
},
|
||||
|
||||
getMasterBallLabel() {
|
||||
const labels = {
|
||||
'red-red': '主球(红球)',
|
||||
'blue-blue': '主球(蓝球)',
|
||||
'red-blue': '主球(红球)',
|
||||
'blue-red': '主球(蓝球)'
|
||||
}
|
||||
return labels[this.analysisType] || ''
|
||||
},
|
||||
|
||||
getSlaveBallLabel() {
|
||||
const labels = {
|
||||
'red-red': '随球(红球)',
|
||||
'blue-blue': '随球(蓝球)',
|
||||
'red-blue': '随球(蓝球)',
|
||||
'blue-red': '随球(红球)'
|
||||
}
|
||||
return labels[this.analysisType] || ''
|
||||
},
|
||||
|
||||
getMasterBallRange() {
|
||||
if (this.analysisType === 'blue-blue' || this.analysisType === 'blue-red') {
|
||||
return Array.from({ length: 16 }, (_, i) => i + 1) // 1-16
|
||||
}
|
||||
return Array.from({ length: 33 }, (_, i) => i + 1) // 1-33
|
||||
},
|
||||
|
||||
getSlaveBallRange() {
|
||||
if (this.analysisType === 'blue-blue' || this.analysisType === 'red-blue') {
|
||||
return Array.from({ length: 16 }, (_, i) => i + 1) // 1-16
|
||||
}
|
||||
return Array.from({ length: 33 }, (_, i) => i + 1) // 1-33
|
||||
},
|
||||
|
||||
isMasterRed() {
|
||||
return this.analysisType === 'red-red' || this.analysisType === 'red-blue'
|
||||
},
|
||||
|
||||
isSlaveRed() {
|
||||
return this.analysisType === 'red-red' || this.analysisType === 'blue-red'
|
||||
},
|
||||
|
||||
selectMasterBall(num) {
|
||||
this.masterBall = num
|
||||
this.result = null
|
||||
this.error = null
|
||||
},
|
||||
|
||||
selectSlaveBall(num) {
|
||||
this.slaveBall = num
|
||||
this.result = null
|
||||
this.error = null
|
||||
},
|
||||
|
||||
async performAnalysis() {
|
||||
if (!this.canAnalyze) return
|
||||
|
||||
this.loading = true
|
||||
this.error = null
|
||||
this.result = null
|
||||
|
||||
try {
|
||||
let response
|
||||
|
||||
switch (this.analysisType) {
|
||||
case 'red-red':
|
||||
response = await lotteryApi.redRedPersistenceAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
case 'blue-blue':
|
||||
response = await lotteryApi.blueBluePersistenceAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
case 'red-blue':
|
||||
response = await lotteryApi.redBluePersistenceAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
case 'blue-red':
|
||||
response = await lotteryApi.blueRedPersistenceAnalysis(this.masterBall, this.slaveBall)
|
||||
break
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
this.result = response.data
|
||||
} else {
|
||||
this.error = response.message || '分析失败'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('接续系数分析失败:', error)
|
||||
this.error = '网络请求失败,请重试'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
clearError() {
|
||||
this.error = null
|
||||
},
|
||||
|
||||
resetAnalysis() {
|
||||
this.masterBall = null
|
||||
this.slaveBall = null
|
||||
this.result = null
|
||||
this.error = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-analysis {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.back-button-container {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 28px;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #7f8c8d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.analysis-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.analysis-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.analysis-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.15);
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.analysis-card.active {
|
||||
background-color: #eaf5ff;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
|
||||
.analysis-card.active :deep(.el-card__body) {
|
||||
background: transparent;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.red-ball-icon {
|
||||
background-color: #e74c3c !important;
|
||||
}
|
||||
|
||||
.blue-ball-icon {
|
||||
background-color: #3498db !important;
|
||||
}
|
||||
|
||||
.btn-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.analysis-card.active .btn-title {
|
||||
color: #2980b9;
|
||||
}
|
||||
|
||||
.btn-desc {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.analysis-card.active .btn-desc {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
|
||||
.number-selection {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.number-selection h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ball-selection-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.ball-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
:deep(.el-button.red-ball.is-plain:not(.is-disabled)) {
|
||||
color: #e74c3c;
|
||||
border-color: #e74c3c;
|
||||
background-color: #fff5f5;
|
||||
}
|
||||
|
||||
:deep(.el-button.red-ball:not(.is-plain):not(.is-disabled)) {
|
||||
background-color: #e74c3c;
|
||||
border-color: #c0392b;
|
||||
}
|
||||
|
||||
:deep(.el-button.blue-ball.is-plain:not(.is-disabled)) {
|
||||
color: #3498db;
|
||||
border-color: #3498db;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
:deep(.el-button.blue-ball:not(.is-plain):not(.is-disabled)) {
|
||||
background-color: #3498db;
|
||||
border-color: #2980b9;
|
||||
}
|
||||
|
||||
.analyze-section {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.result-container {
|
||||
border-top: 2px solid #ecf0f1;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
:deep(.el-result) {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.result-details {
|
||||
width: 100%;
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto 20px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table-title-row {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.table-title-cell {
|
||||
padding: 8px;
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.analysis-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #000;
|
||||
border-top: none;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.analysis-table tr {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.analysis-table tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label-cell {
|
||||
width: 150px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #000;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.value-cell {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.highlight-ball {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.highlight-ball.blue-text {
|
||||
color: #3498db; /* 蓝色文字 */
|
||||
}
|
||||
|
||||
.coefficient-value {
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
padding: 5px;
|
||||
white-space: normal;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.surface-analysis {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.back-button-container {
|
||||
position: static;
|
||||
text-align: left;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.analysis-buttons {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ball-grid {
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.table-title-cell {
|
||||
font-size: 14px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.label-cell, .value-cell {
|
||||
padding: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1359
src/views/ssq/Lottery.vue
Normal file
624
src/views/ssq/PrizeStatistics.vue
Normal file
424
src/views/ssq/SsqTableAnalysis.vue
Normal file
@@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<div class="table-analysis-container">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button @click="goBack" type="default" size="medium" icon="ArrowLeft">
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>表相查询</h1>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<el-card v-if="loading" class="loading-container" shadow="never">
|
||||
<el-skeleton :rows="10" animated />
|
||||
</el-card>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<el-card v-else-if="error" class="error-container" shadow="never">
|
||||
<el-alert
|
||||
:title="error"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
/>
|
||||
<el-button @click="fetchData" type="primary">重新加载</el-button>
|
||||
</el-card>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-card v-else-if="drawsData.length > 0" class="table-container" shadow="never">
|
||||
<div class="table-wrapper">
|
||||
<el-table
|
||||
:data="drawsData"
|
||||
border
|
||||
style="width: 100%"
|
||||
:highlight-current-row="true"
|
||||
@current-change="handleCurrentChange"
|
||||
height="calc(100vh - 150px)"
|
||||
>
|
||||
<el-table-column
|
||||
prop="drawId"
|
||||
label="期号"
|
||||
width="80"
|
||||
fixed="left"
|
||||
align="center"
|
||||
/>
|
||||
|
||||
<!-- 红球列 -->
|
||||
<el-table-column label="红球" align="center">
|
||||
<el-table-column
|
||||
v-for="num in 33"
|
||||
:key="`red-${num}`"
|
||||
:label="formatNumberLabel(num)"
|
||||
width="40"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div v-if="isRedBallHit(scope.row, num)" class="ball red-ball" :class="{'double-digits': num >= 10}">
|
||||
{{ num }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 蓝球列 -->
|
||||
<el-table-column label="蓝球" align="center">
|
||||
<el-table-column
|
||||
v-for="num in 16"
|
||||
:key="`blue-${num}`"
|
||||
:label="formatNumberLabel(num)"
|
||||
width="40"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div v-if="isBlueBallHit(scope.row, num)" class="ball blue-ball" :class="{'double-digits': num >= 10}">
|
||||
{{ num }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 无数据状态 -->
|
||||
<el-card v-else class="no-data-container" shadow="never">
|
||||
<el-empty description="暂无开奖数据" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { lotteryApi } from '../../api/index.js'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElSkeleton,
|
||||
ElAlert,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElEmpty
|
||||
} from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'SsqTableAnalysis',
|
||||
components: {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElSkeleton,
|
||||
ElAlert,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElEmpty
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drawsData: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
selectedRowIndex: null // 选中的行索引
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
// 获取当前彩票类型,优先使用路由参数,否则使用默认值 'ssq'
|
||||
const lotteryType = this.$route.query.lotteryType || 'ssq'
|
||||
this.$router.push({
|
||||
path: '/data-analysis',
|
||||
query: { lotteryType: lotteryType }
|
||||
})
|
||||
},
|
||||
|
||||
// 选中行
|
||||
handleCurrentChange(row) {
|
||||
this.selectedRowIndex = row ? this.drawsData.indexOf(row) : null
|
||||
},
|
||||
|
||||
// 获取数据
|
||||
async fetchData() {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
console.log('开始获取最新100期开奖数据')
|
||||
const response = await lotteryApi.getRecent100Draws()
|
||||
|
||||
if (response && response.success && response.data) {
|
||||
this.drawsData = response.data
|
||||
console.log('获取开奖数据成功,共', this.drawsData.length, '期')
|
||||
console.log('第一条数据示例:', this.drawsData[0])
|
||||
if (this.drawsData.length > 0) {
|
||||
console.log('红球数据:', this.getRedBalls(this.drawsData[0]))
|
||||
console.log('蓝球数据:', this.drawsData[0].blueBall)
|
||||
}
|
||||
} else {
|
||||
this.error = response?.message || '获取数据失败'
|
||||
console.error('获取数据失败:', response)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取开奖数据出错:', error)
|
||||
this.error = error?.response?.data?.message || '网络错误,请稍后重试'
|
||||
|
||||
// 添加测试数据以便调试
|
||||
this.drawsData = [
|
||||
{
|
||||
id: 1,
|
||||
drawId: '2025101',
|
||||
drawPeriod: '2025101',
|
||||
drawDate: '2025-01-01',
|
||||
redBall1: 1,
|
||||
redBall2: 2,
|
||||
redBall3: 3,
|
||||
redBall4: 4,
|
||||
redBall5: 5,
|
||||
redBall6: 6,
|
||||
blueBall: 7
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
drawId: '2025100',
|
||||
drawPeriod: '2025100',
|
||||
drawDate: '2025-01-02',
|
||||
redBall1: 6,
|
||||
redBall2: 9,
|
||||
redBall3: 10,
|
||||
redBall4: 13,
|
||||
redBall5: 30,
|
||||
redBall6: 33,
|
||||
blueBall: 7
|
||||
}
|
||||
]
|
||||
console.log('使用测试数据:', this.drawsData)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 获取红球数组
|
||||
getRedBalls(draw) {
|
||||
const balls = []
|
||||
if (draw.redBall1) balls.push(parseInt(draw.redBall1))
|
||||
if (draw.redBall2) balls.push(parseInt(draw.redBall2))
|
||||
if (draw.redBall3) balls.push(parseInt(draw.redBall3))
|
||||
if (draw.redBall4) balls.push(parseInt(draw.redBall4))
|
||||
if (draw.redBall5) balls.push(parseInt(draw.redBall5))
|
||||
if (draw.redBall6) balls.push(parseInt(draw.redBall6))
|
||||
return balls
|
||||
},
|
||||
|
||||
// 判断红球是否命中
|
||||
isRedBallHit(draw, num) {
|
||||
const redBalls = this.getRedBalls(draw)
|
||||
return redBalls.includes(num)
|
||||
},
|
||||
|
||||
// 判断蓝球是否命中
|
||||
isBlueBallHit(draw, num) {
|
||||
return parseInt(draw.blueBall) === num
|
||||
},
|
||||
|
||||
// 格式化数字标签,确保两位数显示在一行
|
||||
formatNumberLabel(num) {
|
||||
// 直接返回数字,CSS会处理双位数的显示
|
||||
return num;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table-analysis-container {
|
||||
min-height: calc(100vh - 70px);
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 工具栏样式 */
|
||||
.toolbar {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.error-container {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* 自定义表格样式 */
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: #f5f7fa;
|
||||
--el-table-header-text-color: #333;
|
||||
--el-table-row-hover-bg-color: #ecf5ff;
|
||||
}
|
||||
|
||||
:deep(.el-table__header) th {
|
||||
padding: 2px 0;
|
||||
height: 32px;
|
||||
font-size: 12px; /* 减小表头字体大小 */
|
||||
}
|
||||
|
||||
:deep(.el-table__header .cell) {
|
||||
padding: 0 !important; /* 移除单元格内边距 */
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 对两位数的表头标签应用特殊样式 */
|
||||
:deep(.el-table__header th[class*="is-leaf"]:nth-child(n+11)) .cell {
|
||||
font-size: 11px; /* 缩小两位数字体 */
|
||||
letter-spacing: -0.5px; /* 减少字母间距 */
|
||||
}
|
||||
|
||||
:deep(.el-table__body) td {
|
||||
padding: 4px 0;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
:deep(.el-table--border .el-table__cell) {
|
||||
border-right: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
:deep(.el-table__fixed-right) {
|
||||
height: 100%;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 无数据状态 */
|
||||
.no-data-container {
|
||||
margin-bottom: 20px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 球体样式 */
|
||||
.ball {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 900;
|
||||
font-size: 10px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 两位数的球使用更小字体 */
|
||||
.double-digits {
|
||||
font-size: 9px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
/* 红球样式 */
|
||||
.red-ball {
|
||||
background: linear-gradient(135deg, #ff5252 0%, #d32f2f 50%, #b71c1c 100%);
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #b71c1c;
|
||||
}
|
||||
|
||||
.red-ball::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 蓝球样式 */
|
||||
.blue-ball {
|
||||
background: linear-gradient(135deg, #42a5f5 0%, #1976d2 50%, #0d47a1 100%);
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #0d47a1;
|
||||
}
|
||||
|
||||
.blue-ball::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.table-analysis-container {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 在移动端调整球体大小 */
|
||||
.ball {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.ball::before {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||