Initial commit: Project setup with frontend and backend implementation
This commit is contained in:
commit
332f4dddd6
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
*.local
|
||||||
|
.env
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
*.log
|
||||||
|
*.json
|
||||||
|
!package.json
|
||||||
|
!package-lock.json
|
||||||
171
README.md
Normal file
171
README.md
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
# 彩票数据分析系统
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
本项目是一个彩票数据分析系统,支持双色球和大乐透的数据管理、统计分析和智能选号功能。系统采用前后端分离架构,提供直观的用户界面和强大的数据分析能力。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
### 后端
|
||||||
|
- Python 3.8+
|
||||||
|
- FastAPI
|
||||||
|
- SQLAlchemy
|
||||||
|
- PostgreSQL
|
||||||
|
- Pydantic
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- Vue 3
|
||||||
|
- Vite
|
||||||
|
- Element Plus
|
||||||
|
- ECharts
|
||||||
|
- Pinia
|
||||||
|
- Vue Router
|
||||||
|
|
||||||
|
## 系统功能
|
||||||
|
1. 数据管理
|
||||||
|
- 双色球和大乐透历史数据导入
|
||||||
|
- 数据查询和筛选
|
||||||
|
- 数据导出
|
||||||
|
- 手动数据录入
|
||||||
|
|
||||||
|
2. 统计分析
|
||||||
|
- 号码出现频率统计
|
||||||
|
- 热门号码分析
|
||||||
|
- 冷门号码分析
|
||||||
|
- 数据可视化展示
|
||||||
|
|
||||||
|
3. 智能选号
|
||||||
|
- 随机选号
|
||||||
|
- 频率选号
|
||||||
|
- 热门号码选号
|
||||||
|
- 冷门号码选号
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
```
|
||||||
|
lottery/
|
||||||
|
├── backend/ # 后端代码
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── api/ # API 路由
|
||||||
|
│ │ ├── core/ # 核心配置
|
||||||
|
│ │ ├── models/ # 数据模型
|
||||||
|
│ │ ├── schemas/ # 数据验证
|
||||||
|
│ │ └── services/ # 业务逻辑
|
||||||
|
│ ├── requirements.txt # 依赖包
|
||||||
|
│ └── main.py # 入口文件
|
||||||
|
├── frontend/ # 前端代码
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── api/ # API 接口
|
||||||
|
│ │ ├── assets/ # 静态资源
|
||||||
|
│ │ ├── components/ # 组件
|
||||||
|
│ │ ├── router/ # 路由配置
|
||||||
|
│ │ ├── views/ # 页面
|
||||||
|
│ │ ├── App.vue # 根组件
|
||||||
|
│ │ └── main.js # 入口文件
|
||||||
|
│ ├── package.json # 依赖配置
|
||||||
|
│ └── vite.config.js # Vite 配置
|
||||||
|
└── README.md # 项目文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发环境要求
|
||||||
|
- Python 3.8+
|
||||||
|
- Node.js 16+
|
||||||
|
- PostgreSQL 12+
|
||||||
|
- npm 或 yarn
|
||||||
|
|
||||||
|
## 安装和运行
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
1. 创建虚拟环境
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # Linux/Mac
|
||||||
|
venv\Scripts\activate # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装依赖
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 配置数据库
|
||||||
|
- 创建 PostgreSQL 数据库
|
||||||
|
- 修改 `backend/app/core/database.py` 中的数据库连接配置
|
||||||
|
|
||||||
|
4. 启动服务
|
||||||
|
```bash
|
||||||
|
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
1. 安装依赖
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动开发服务器
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 构建生产版本
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 数据导入
|
||||||
|
1. 在双色球或大乐透页面点击"导入数据"按钮
|
||||||
|
2. 选择对应的 JSON 数据文件
|
||||||
|
3. 等待导入完成
|
||||||
|
|
||||||
|
### 数据查询
|
||||||
|
1. 在查询表单中输入查询条件
|
||||||
|
2. 点击"查询"按钮
|
||||||
|
3. 查看查询结果
|
||||||
|
|
||||||
|
### 统计分析
|
||||||
|
1. 进入统计分析页面
|
||||||
|
2. 选择要分析的彩票类型
|
||||||
|
3. 查看统计图表
|
||||||
|
|
||||||
|
### 智能选号
|
||||||
|
1. 进入智能选号页面
|
||||||
|
2. 选择彩票类型和选号策略
|
||||||
|
3. 设置生成注数
|
||||||
|
4. 点击"生成号码"按钮
|
||||||
|
|
||||||
|
## API 文档
|
||||||
|
启动后端服务后,访问以下地址查看 API 文档:
|
||||||
|
- Swagger UI: http://localhost:8000/docs
|
||||||
|
- ReDoc: http://localhost:8000/redoc
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
1. 数据库连接失败
|
||||||
|
- 检查数据库服务是否启动
|
||||||
|
- 验证数据库连接配置是否正确
|
||||||
|
|
||||||
|
2. 前端无法连接后端
|
||||||
|
- 确认后端服务是否正常运行
|
||||||
|
- 检查前端环境配置中的 API 地址是否正确
|
||||||
|
|
||||||
|
3. 数据导入失败
|
||||||
|
- 检查 JSON 文件格式是否正确
|
||||||
|
- 确认数据库表结构是否完整
|
||||||
|
|
||||||
|
## 开发计划
|
||||||
|
- [ ] 添加用户认证功能
|
||||||
|
- [ ] 实现数据备份和恢复
|
||||||
|
- [ ] 优化数据导入性能
|
||||||
|
- [ ] 添加更多统计分析功能
|
||||||
|
- [ ] 实现自定义选号策略
|
||||||
|
|
||||||
|
## 贡献指南
|
||||||
|
1. Fork 项目
|
||||||
|
2. 创建特性分支
|
||||||
|
3. 提交更改
|
||||||
|
4. 推送到分支
|
||||||
|
5. 创建 Pull Request
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
MIT License
|
||||||
3
backend/__init__.py
Normal file
3
backend/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Lottery backend package
|
||||||
|
"""
|
||||||
3
backend/app/__init__.py
Normal file
3
backend/app/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Lottery application package
|
||||||
|
"""
|
||||||
3
backend/app/api/__init__.py
Normal file
3
backend/app/api/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
API package
|
||||||
|
"""
|
||||||
1
backend/app/api/endpoints/__init__.py
Normal file
1
backend/app/api/endpoints/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
107
backend/app/api/endpoints/lottery.py
Normal file
107
backend/app/api/endpoints/lottery.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
import os
|
||||||
|
from ...core.database import get_db
|
||||||
|
from ...schemas.lottery import (
|
||||||
|
SSQLottery, SSQLotteryCreate,
|
||||||
|
DLTLottery, DLTLotteryCreate,
|
||||||
|
LotteryQuery
|
||||||
|
)
|
||||||
|
from ...services.lottery import LotteryService
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/ssq/", response_model=SSQLottery)
|
||||||
|
def create_ssq_lottery(
|
||||||
|
lottery: SSQLotteryCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
return LotteryService.create_ssq_lottery(db, lottery)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/dlt/", response_model=DLTLottery)
|
||||||
|
def create_dlt_lottery(
|
||||||
|
lottery: DLTLotteryCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
return LotteryService.create_dlt_lottery(db, lottery)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ssq/", response_model=List[SSQLottery])
|
||||||
|
def get_ssq_lotteries(
|
||||||
|
query: LotteryQuery = Depends(),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
lotteries, total = LotteryService.get_ssq_lotteries(db, query)
|
||||||
|
return lotteries
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/dlt/", response_model=List[DLTLottery])
|
||||||
|
def get_dlt_lotteries(
|
||||||
|
query: LotteryQuery = Depends(),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
lotteries, total = LotteryService.get_dlt_lotteries(db, query)
|
||||||
|
return lotteries
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ssq/statistics")
|
||||||
|
def get_ssq_statistics(db: Session = Depends(get_db)):
|
||||||
|
return LotteryService.get_ssq_statistics(db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/dlt/statistics")
|
||||||
|
def get_dlt_statistics(db: Session = Depends(get_db)):
|
||||||
|
return LotteryService.get_dlt_statistics(db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/ssq/import")
|
||||||
|
async def import_ssq_data(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
if not file.filename.endswith('.json'):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Only JSON files are allowed")
|
||||||
|
|
||||||
|
# 保存上传的文件
|
||||||
|
file_path = f"temp_{file.filename}"
|
||||||
|
try:
|
||||||
|
with open(file_path, "wb") as buffer:
|
||||||
|
content = await file.read()
|
||||||
|
buffer.write(content)
|
||||||
|
|
||||||
|
# 导入数据
|
||||||
|
count = LotteryService.import_ssq_data(db, file_path)
|
||||||
|
return {"message": f"Successfully imported {count} records"}
|
||||||
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/dlt/import")
|
||||||
|
async def import_dlt_data(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
if not file.filename.endswith('.json'):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Only JSON files are allowed")
|
||||||
|
|
||||||
|
# 保存上传的文件
|
||||||
|
file_path = f"temp_{file.filename}"
|
||||||
|
try:
|
||||||
|
with open(file_path, "wb") as buffer:
|
||||||
|
content = await file.read()
|
||||||
|
buffer.write(content)
|
||||||
|
|
||||||
|
# 导入数据
|
||||||
|
count = LotteryService.import_dlt_data(db, file_path)
|
||||||
|
return {"message": f"Successfully imported {count} records"}
|
||||||
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
3
backend/app/api/v1/__init__.py
Normal file
3
backend/app/api/v1/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
API v1 package
|
||||||
|
"""
|
||||||
112
backend/app/api/v1/lottery.py
Normal file
112
backend/app/api/v1/lottery.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import date
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.services.lottery import LotteryService
|
||||||
|
from app.schemas.lottery import SSQLottery, DLTLottery, LotteryQuery
|
||||||
|
import random
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ssq/latest", response_model=SSQLottery)
|
||||||
|
def get_latest_ssq(db: Session = Depends(get_db)):
|
||||||
|
"""获取最新一期双色球开奖记录"""
|
||||||
|
return LotteryService.get_latest_ssq(db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/dlt/latest", response_model=DLTLottery)
|
||||||
|
def get_latest_dlt(db: Session = Depends(get_db)):
|
||||||
|
"""获取最新一期大乐透开奖记录"""
|
||||||
|
return LotteryService.get_latest_dlt(db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ssq/", response_model=List[SSQLottery])
|
||||||
|
def get_ssq_lotteries(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
issue: Optional[str] = Query(None, description="期号"),
|
||||||
|
start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||||
|
end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||||
|
page: int = Query(1, ge=1, description="页码"),
|
||||||
|
page_size: int = Query(20, ge=1, le=100, description="每页记录数")
|
||||||
|
):
|
||||||
|
"""获取双色球开奖记录列表"""
|
||||||
|
query = LotteryQuery(
|
||||||
|
issue=issue,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size
|
||||||
|
)
|
||||||
|
lotteries, _ = LotteryService.get_ssq_lotteries(db, query)
|
||||||
|
return lotteries
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/dlt/", response_model=List[DLTLottery])
|
||||||
|
def get_dlt_lotteries(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
issue: Optional[str] = Query(None, description="期号"),
|
||||||
|
start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||||
|
end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||||
|
page: int = Query(1, ge=1, description="页码"),
|
||||||
|
page_size: int = Query(20, ge=1, le=100, description="每页记录数")
|
||||||
|
):
|
||||||
|
"""获取大乐透开奖记录列表"""
|
||||||
|
query = LotteryQuery(
|
||||||
|
issue=issue,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size
|
||||||
|
)
|
||||||
|
lotteries, _ = LotteryService.get_dlt_lotteries(db, query)
|
||||||
|
return lotteries
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ssq/statistics")
|
||||||
|
def get_ssq_statistics(db: Session = Depends(get_db)):
|
||||||
|
"""获取双色球统计数据"""
|
||||||
|
return LotteryService.get_ssq_statistics(db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/dlt/statistics")
|
||||||
|
def get_dlt_statistics(db: Session = Depends(get_db)):
|
||||||
|
"""获取大乐透统计数据"""
|
||||||
|
return LotteryService.get_dlt_statistics(db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ssq/generate")
|
||||||
|
def generate_ssq_numbers(strategy: str = Query("random"), count: int = Query(1)):
|
||||||
|
"""
|
||||||
|
智能生成双色球号码
|
||||||
|
- strategy: 选号策略(目前仅支持 random)
|
||||||
|
- count: 生成注数
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for _ in range(count):
|
||||||
|
red_balls = sorted(random.sample(range(1, 34), 6))
|
||||||
|
blue_ball = random.randint(1, 16)
|
||||||
|
results.append({
|
||||||
|
"red_balls": red_balls,
|
||||||
|
"blue_ball": blue_ball
|
||||||
|
})
|
||||||
|
return {"results": results}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/dlt/generate")
|
||||||
|
def generate_dlt_numbers(strategy: str = Query("random"), count: int = Query(1)):
|
||||||
|
"""
|
||||||
|
智能生成大乐透号码
|
||||||
|
- strategy: 选号策略(目前仅支持 random)
|
||||||
|
- count: 生成注数
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for _ in range(count):
|
||||||
|
front_balls = sorted(random.sample(range(1, 36), 5))
|
||||||
|
back_balls = sorted(random.sample(range(1, 13), 2))
|
||||||
|
results.append({
|
||||||
|
"front_balls": front_balls,
|
||||||
|
"back_balls": back_balls
|
||||||
|
})
|
||||||
|
return {"results": results}
|
||||||
1
backend/app/core/__init__.py
Normal file
1
backend/app/core/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
7
backend/app/core/config.py
Normal file
7
backend/app/core/config.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class Settings:
|
||||||
|
PROJECT_NAME = "彩票数据分析系统"
|
||||||
|
API_V1_STR = "/api/v1"
|
||||||
|
BACKEND_CORS_ORIGINS = ["*"]
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
31
backend/app/core/database.py
Normal file
31
backend/app/core/database.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
# 远程MySQL数据库配置
|
||||||
|
SQLALCHEMY_DATABASE_URL = "mysql+pymysql://root:911Forever@119.28.86.234:3306/lottery"
|
||||||
|
|
||||||
|
# 创建数据库引擎
|
||||||
|
engine = create_engine(
|
||||||
|
SQLALCHEMY_DATABASE_URL,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
pool_recycle=3600,
|
||||||
|
pool_size=5,
|
||||||
|
max_overflow=10
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建会话工厂
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# 创建基类
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
# 获取数据库会话
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
29
backend/app/core/init_db.py
Normal file
29
backend/app/core/init_db.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import pymysql
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from ..models.lottery import Base
|
||||||
|
from .database import SQLALCHEMY_DATABASE_URL
|
||||||
|
|
||||||
|
|
||||||
|
def init_database():
|
||||||
|
# 创建数据库
|
||||||
|
conn = pymysql.connect(
|
||||||
|
host='127.0.0.1',
|
||||||
|
user='root',
|
||||||
|
password='911!Dswybs-1024'
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute('CREATE DATABASE IF NOT EXISTS lottery')
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 创建表
|
||||||
|
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_database()
|
||||||
|
print("数据库初始化完成!")
|
||||||
30
backend/app/main.py
Normal file
30
backend/app/main.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.api.v1.lottery import router as lottery_router
|
||||||
|
from app.core.database import Base, engine
|
||||||
|
|
||||||
|
# 创建数据库表
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title=settings.PROJECT_NAME,
|
||||||
|
openapi_url=f"{settings.API_V1_STR}/openapi.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 配置CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.BACKEND_CORS_ORIGINS,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# 注册路由
|
||||||
|
app.include_router(
|
||||||
|
lottery_router, prefix=f"{settings.API_V1_STR}/lottery", tags=["lottery"])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
1
backend/app/models/__init__.py
Normal file
1
backend/app/models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
38
backend/app/models/lottery.py
Normal file
38
backend/app/models/lottery.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Date, TIMESTAMP, func
|
||||||
|
from ..core.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class SSQLottery(Base):
|
||||||
|
__tablename__ = "ssq_lottery"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
|
issue = Column(String(10), unique=True, nullable=False, comment="期号")
|
||||||
|
open_time = Column(Date, nullable=False, comment="开奖日期")
|
||||||
|
red_ball_1 = Column(Integer, nullable=False, comment="红球1")
|
||||||
|
red_ball_2 = Column(Integer, nullable=False, comment="红球2")
|
||||||
|
red_ball_3 = Column(Integer, nullable=False, comment="红球3")
|
||||||
|
red_ball_4 = Column(Integer, nullable=False, comment="红球4")
|
||||||
|
red_ball_5 = Column(Integer, nullable=False, comment="红球5")
|
||||||
|
red_ball_6 = Column(Integer, nullable=False, comment="红球6")
|
||||||
|
blue_ball = Column(Integer, nullable=False, comment="蓝球")
|
||||||
|
created_at = Column(TIMESTAMP, server_default=func.now(), comment="创建时间")
|
||||||
|
updated_at = Column(TIMESTAMP, server_default=func.now(),
|
||||||
|
onupdate=func.now(), comment="更新时间")
|
||||||
|
|
||||||
|
|
||||||
|
class DLTLottery(Base):
|
||||||
|
__tablename__ = "dlt_lottery"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
|
issue = Column(String(10), unique=True, nullable=False, comment="期号")
|
||||||
|
open_time = Column(Date, nullable=False, comment="开奖日期")
|
||||||
|
front_ball_1 = Column(Integer, nullable=False, comment="前区球1")
|
||||||
|
front_ball_2 = Column(Integer, nullable=False, comment="前区球2")
|
||||||
|
front_ball_3 = Column(Integer, nullable=False, comment="前区球3")
|
||||||
|
front_ball_4 = Column(Integer, nullable=False, comment="前区球4")
|
||||||
|
front_ball_5 = Column(Integer, nullable=False, comment="前区球5")
|
||||||
|
back_ball_1 = Column(Integer, nullable=False, comment="后区球1")
|
||||||
|
back_ball_2 = Column(Integer, nullable=False, comment="后区球2")
|
||||||
|
created_at = Column(TIMESTAMP, server_default=func.now(), comment="创建时间")
|
||||||
|
updated_at = Column(TIMESTAMP, server_default=func.now(),
|
||||||
|
onupdate=func.now(), comment="更新时间")
|
||||||
1
backend/app/schemas/__init__.py
Normal file
1
backend/app/schemas/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
61
backend/app/schemas/lottery.py
Normal file
61
backend/app/schemas/lottery.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import date, datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class SSQLotteryBase(BaseModel):
|
||||||
|
issue: str = Field(..., description="期号")
|
||||||
|
open_time: date = Field(..., description="开奖日期")
|
||||||
|
red_ball_1: int = Field(..., ge=1, le=33, description="红球1")
|
||||||
|
red_ball_2: int = Field(..., ge=1, le=33, description="红球2")
|
||||||
|
red_ball_3: int = Field(..., ge=1, le=33, description="红球3")
|
||||||
|
red_ball_4: int = Field(..., ge=1, le=33, description="红球4")
|
||||||
|
red_ball_5: int = Field(..., ge=1, le=33, description="红球5")
|
||||||
|
red_ball_6: int = Field(..., ge=1, le=33, description="红球6")
|
||||||
|
blue_ball: int = Field(..., ge=1, le=16, description="蓝球")
|
||||||
|
|
||||||
|
|
||||||
|
class SSQLotteryCreate(SSQLotteryBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SSQLottery(SSQLotteryBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class DLTLotteryBase(BaseModel):
|
||||||
|
issue: str = Field(..., description="期号")
|
||||||
|
open_time: date = Field(..., description="开奖日期")
|
||||||
|
front_ball_1: int = Field(..., ge=1, le=35, description="前区球1")
|
||||||
|
front_ball_2: int = Field(..., ge=1, le=35, description="前区球2")
|
||||||
|
front_ball_3: int = Field(..., ge=1, le=35, description="前区球3")
|
||||||
|
front_ball_4: int = Field(..., ge=1, le=35, description="前区球4")
|
||||||
|
front_ball_5: int = Field(..., ge=1, le=35, description="前区球5")
|
||||||
|
back_ball_1: int = Field(..., ge=1, le=12, description="后区球1")
|
||||||
|
back_ball_2: int = Field(..., ge=1, le=12, description="后区球2")
|
||||||
|
|
||||||
|
|
||||||
|
class DLTLotteryCreate(DLTLotteryBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DLTLottery(DLTLotteryBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class LotteryQuery(BaseModel):
|
||||||
|
start_date: Optional[str] = None
|
||||||
|
end_date: Optional[str] = None
|
||||||
|
issue: Optional[str] = None
|
||||||
|
page: int = 1
|
||||||
|
page_size: int = 20
|
||||||
1
backend/app/services/__init__.py
Normal file
1
backend/app/services/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
245
backend/app/services/lottery.py
Normal file
245
backend/app/services/lottery.py
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import func, desc
|
||||||
|
from typing import List, Optional, Dict, Any, Tuple
|
||||||
|
from datetime import date, datetime
|
||||||
|
import pandas as pd
|
||||||
|
from app.models.lottery import SSQLottery, DLTLottery
|
||||||
|
from app.schemas.lottery import SSQLotteryCreate, DLTLotteryCreate, LotteryQuery
|
||||||
|
|
||||||
|
|
||||||
|
class LotteryService:
|
||||||
|
@staticmethod
|
||||||
|
def create_ssq_lottery(db: Session, lottery: SSQLotteryCreate) -> SSQLottery:
|
||||||
|
db_lottery = SSQLottery(**lottery.model_dump())
|
||||||
|
db.add(db_lottery)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_lottery)
|
||||||
|
return db_lottery
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_dlt_lottery(db: Session, lottery: DLTLotteryCreate) -> DLTLottery:
|
||||||
|
db_lottery = DLTLottery(**lottery.model_dump())
|
||||||
|
db.add(db_lottery)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_lottery)
|
||||||
|
return db_lottery
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_ssq_lotteries(
|
||||||
|
db: Session,
|
||||||
|
query: LotteryQuery
|
||||||
|
) -> Tuple[List[SSQLottery], int]:
|
||||||
|
"""获取双色球开奖记录"""
|
||||||
|
db_query = db.query(SSQLottery)
|
||||||
|
|
||||||
|
# 应用过滤条件
|
||||||
|
if query.issue:
|
||||||
|
db_query = db_query.filter(SSQLottery.issue == query.issue)
|
||||||
|
if query.start_date:
|
||||||
|
try:
|
||||||
|
start_date = datetime.strptime(
|
||||||
|
query.start_date, "%Y-%m-%d").date()
|
||||||
|
db_query = db_query.filter(SSQLottery.open_time >= start_date)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if query.end_date:
|
||||||
|
try:
|
||||||
|
end_date = datetime.strptime(query.end_date, "%Y-%m-%d").date()
|
||||||
|
db_query = db_query.filter(SSQLottery.open_time <= end_date)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 获取总记录数
|
||||||
|
total = db_query.count()
|
||||||
|
|
||||||
|
# 应用分页
|
||||||
|
page = query.page or 1
|
||||||
|
page_size = query.page_size or 20
|
||||||
|
db_query = db_query.order_by(desc(SSQLottery.open_time)) \
|
||||||
|
.offset((page - 1) * page_size) \
|
||||||
|
.limit(page_size)
|
||||||
|
|
||||||
|
return db_query.all(), total
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_dlt_lotteries(
|
||||||
|
db: Session,
|
||||||
|
query: LotteryQuery
|
||||||
|
) -> Tuple[List[DLTLottery], int]:
|
||||||
|
"""获取大乐透开奖记录"""
|
||||||
|
db_query = db.query(DLTLottery)
|
||||||
|
|
||||||
|
# 应用过滤条件
|
||||||
|
if query.issue:
|
||||||
|
db_query = db_query.filter(DLTLottery.issue == query.issue)
|
||||||
|
if query.start_date:
|
||||||
|
try:
|
||||||
|
start_date = datetime.strptime(
|
||||||
|
query.start_date, "%Y-%m-%d").date()
|
||||||
|
db_query = db_query.filter(DLTLottery.open_time >= start_date)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if query.end_date:
|
||||||
|
try:
|
||||||
|
end_date = datetime.strptime(query.end_date, "%Y-%m-%d").date()
|
||||||
|
db_query = db_query.filter(DLTLottery.open_time <= end_date)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 获取总记录数
|
||||||
|
total = db_query.count()
|
||||||
|
|
||||||
|
# 应用分页
|
||||||
|
page = query.page or 1
|
||||||
|
page_size = query.page_size or 20
|
||||||
|
db_query = db_query.order_by(desc(DLTLottery.open_time)) \
|
||||||
|
.offset((page - 1) * page_size) \
|
||||||
|
.limit(page_size)
|
||||||
|
|
||||||
|
return db_query.all(), total
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_ssq_statistics(db: Session):
|
||||||
|
# 红球统计
|
||||||
|
red_freq = []
|
||||||
|
for i in range(1, 7):
|
||||||
|
col = getattr(SSQLottery, f"red_ball_{i}")
|
||||||
|
result = db.query(col, func.count().label(
|
||||||
|
'count')).group_by(col).all()
|
||||||
|
for number, count in result:
|
||||||
|
red_freq.append((number, count))
|
||||||
|
# 汇总红球频率
|
||||||
|
red_counter = {}
|
||||||
|
for number, count in red_freq:
|
||||||
|
red_counter[number] = red_counter.get(number, 0) + count
|
||||||
|
red_balls = sorted([(k, v)
|
||||||
|
for k, v in red_counter.items()], key=lambda x: x[0])
|
||||||
|
|
||||||
|
# 蓝球统计
|
||||||
|
blue_freq = db.query(SSQLottery.blue_ball, func.count().label(
|
||||||
|
'count')).group_by(SSQLottery.blue_ball).all()
|
||||||
|
blue_balls = sorted([(k, v) for k, v in blue_freq], key=lambda x: x[0])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"red_balls": red_balls,
|
||||||
|
"blue_balls": blue_balls
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_dlt_statistics(db: Session):
|
||||||
|
# 前区统计
|
||||||
|
front_freq = []
|
||||||
|
for i in range(1, 6):
|
||||||
|
col = getattr(DLTLottery, f"front_ball_{i}")
|
||||||
|
result = db.query(col, func.count().label(
|
||||||
|
'count')).group_by(col).all()
|
||||||
|
for number, count in result:
|
||||||
|
front_freq.append((number, count))
|
||||||
|
front_counter = {}
|
||||||
|
for number, count in front_freq:
|
||||||
|
front_counter[number] = front_counter.get(number, 0) + count
|
||||||
|
front_balls = sorted(
|
||||||
|
[(k, v) for k, v in front_counter.items()], key=lambda x: x[0])
|
||||||
|
|
||||||
|
# 后区统计
|
||||||
|
back_freq = []
|
||||||
|
for i in range(1, 3):
|
||||||
|
col = getattr(DLTLottery, f"back_ball_{i}")
|
||||||
|
result = db.query(col, func.count().label(
|
||||||
|
'count')).group_by(col).all()
|
||||||
|
for number, count in result:
|
||||||
|
back_freq.append((number, count))
|
||||||
|
back_counter = {}
|
||||||
|
for number, count in back_freq:
|
||||||
|
back_counter[number] = back_counter.get(number, 0) + count
|
||||||
|
back_balls = sorted(
|
||||||
|
[(k, v) for k, v in back_counter.items()], key=lambda x: x[0])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"front_balls": front_balls,
|
||||||
|
"back_balls": back_balls
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def import_ssq_data(db: Session, file_path: str) -> int:
|
||||||
|
import pandas as pd
|
||||||
|
from app.models.lottery import SSQLottery
|
||||||
|
|
||||||
|
df = pd.read_json(file_path)
|
||||||
|
# 先查出所有已存在的期号
|
||||||
|
existing_issues = set(
|
||||||
|
i[0] for i in db.query(SSQLottery.issue).filter(SSQLottery.issue.in_(df['issue'].astype(str).tolist())).all()
|
||||||
|
)
|
||||||
|
# pandas 端彻底去重
|
||||||
|
new_rows = df[~df['issue'].astype(str).isin(
|
||||||
|
existing_issues)].drop_duplicates(subset=['issue'])
|
||||||
|
|
||||||
|
objs = [
|
||||||
|
SSQLottery(
|
||||||
|
issue=str(row['issue']),
|
||||||
|
open_time=row['open_time'],
|
||||||
|
red_ball_1=int(row['red_ball_1']),
|
||||||
|
red_ball_2=int(row['red_ball_2']),
|
||||||
|
red_ball_3=int(row['red_ball_3']),
|
||||||
|
red_ball_4=int(row['red_ball_4']),
|
||||||
|
red_ball_5=int(row['red_ball_5']),
|
||||||
|
red_ball_6=int(row['red_ball_6']),
|
||||||
|
blue_ball=int(row['blue_ball'])
|
||||||
|
)
|
||||||
|
for _, row in new_rows.iterrows()
|
||||||
|
]
|
||||||
|
if objs:
|
||||||
|
try:
|
||||||
|
db.bulk_save_objects(objs)
|
||||||
|
db.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
print(f"Bulk insert error: {e}")
|
||||||
|
return len(objs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def import_dlt_data(db: Session, file_path: str) -> int:
|
||||||
|
import pandas as pd
|
||||||
|
from app.models.lottery import DLTLottery
|
||||||
|
|
||||||
|
df = pd.read_json(file_path)
|
||||||
|
# 先查出所有已存在的期号
|
||||||
|
existing_issues = set(
|
||||||
|
i[0] for i in db.query(DLTLottery.issue).filter(DLTLottery.issue.in_(df['issue'].astype(str).tolist())).all()
|
||||||
|
)
|
||||||
|
# pandas 端彻底去重
|
||||||
|
new_rows = df[~df['issue'].astype(str).isin(
|
||||||
|
existing_issues)].drop_duplicates(subset=['issue'])
|
||||||
|
|
||||||
|
objs = [
|
||||||
|
DLTLottery(
|
||||||
|
issue=str(row['issue']),
|
||||||
|
open_time=row['open_time'],
|
||||||
|
front_ball_1=int(row['front_ball_1']),
|
||||||
|
front_ball_2=int(row['front_ball_2']),
|
||||||
|
front_ball_3=int(row['front_ball_3']),
|
||||||
|
front_ball_4=int(row['front_ball_4']),
|
||||||
|
front_ball_5=int(row['front_ball_5']),
|
||||||
|
back_ball_1=int(row['back_ball_1']),
|
||||||
|
back_ball_2=int(row['back_ball_2'])
|
||||||
|
)
|
||||||
|
for _, row in new_rows.iterrows()
|
||||||
|
]
|
||||||
|
if objs:
|
||||||
|
try:
|
||||||
|
db.bulk_save_objects(objs)
|
||||||
|
db.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
print(f"Bulk insert error: {e}")
|
||||||
|
return len(objs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_latest_ssq(db: Session) -> Optional[SSQLottery]:
|
||||||
|
"""获取最新一期双色球开奖记录"""
|
||||||
|
return db.query(SSQLottery).order_by(desc(SSQLottery.issue)).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_latest_dlt(db: Session) -> Optional[DLTLottery]:
|
||||||
|
"""获取最新一期大乐透开奖记录"""
|
||||||
|
return db.query(DLTLottery).order_by(desc(DLTLottery.issue)).first()
|
||||||
13
backend/requirements.txt
Normal file
13
backend/requirements.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn==0.24.0
|
||||||
|
sqlalchemy==2.0.23
|
||||||
|
pydantic==2.5.2
|
||||||
|
python-multipart==0.0.6
|
||||||
|
python-jose==3.3.0
|
||||||
|
passlib==1.7.4
|
||||||
|
bcrypt==4.0.1
|
||||||
|
pymysql==1.1.0
|
||||||
|
cryptography==41.0.5
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
pandas==2.1.3
|
||||||
|
aiofiles==23.2.1
|
||||||
37
clean_dlt.py
Normal file
37
clean_dlt.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# 读取原始 JSON
|
||||||
|
df = pd.read_json('dlt_all_data.json')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_front_balls(s):
|
||||||
|
return [int(x) for x in str(s).split()[:5]]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_back_balls(s):
|
||||||
|
return [int(x) for x in str(s).split()[:2]]
|
||||||
|
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
try:
|
||||||
|
fronts = parse_front_balls(row['frontWinningNum'])
|
||||||
|
backs = parse_back_balls(row['backWinningNum'])
|
||||||
|
records.append({
|
||||||
|
'issue': str(row['issue']),
|
||||||
|
'open_time': str(row['openTime']),
|
||||||
|
'front_ball_1': fronts[0],
|
||||||
|
'front_ball_2': fronts[1],
|
||||||
|
'front_ball_3': fronts[2],
|
||||||
|
'front_ball_4': fronts[3],
|
||||||
|
'front_ball_5': fronts[4],
|
||||||
|
'back_ball_1': backs[0],
|
||||||
|
'back_ball_2': backs[1]
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing row: {row['issue']}, error: {e}")
|
||||||
|
|
||||||
|
# 保存为新 JSON
|
||||||
|
pd.DataFrame(records).to_json('dlt_clean.json',
|
||||||
|
orient='records', force_ascii=False)
|
||||||
|
print('精简后的数据已保存为 dlt_clean.json')
|
||||||
37
clean_ssq.py
Normal file
37
clean_ssq.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# 读取原始 JSON
|
||||||
|
df = pd.read_json('ssq_all_data.json')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_red_balls(s):
|
||||||
|
return [int(x) for x in str(s).split()[:6]]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_blue_ball(s):
|
||||||
|
return int(str(s).split()[0])
|
||||||
|
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
try:
|
||||||
|
reds = parse_red_balls(row['frontWinningNum'])
|
||||||
|
blue = parse_blue_ball(row['backWinningNum'])
|
||||||
|
records.append({
|
||||||
|
'issue': str(row['issue']),
|
||||||
|
'open_time': str(row['openTime']),
|
||||||
|
'red_ball_1': reds[0],
|
||||||
|
'red_ball_2': reds[1],
|
||||||
|
'red_ball_3': reds[2],
|
||||||
|
'red_ball_4': reds[3],
|
||||||
|
'red_ball_5': reds[4],
|
||||||
|
'red_ball_6': reds[5],
|
||||||
|
'blue_ball': blue
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing row: {row['issue']}, error: {e}")
|
||||||
|
|
||||||
|
# 保存为新 JSON
|
||||||
|
pd.DataFrame(records).to_json('ssq_clean.json',
|
||||||
|
orient='records', force_ascii=False)
|
||||||
|
print('精简后的数据已保存为 ssq_clean.json')
|
||||||
1
frontend/favicon.ico
Normal file
1
frontend/favicon.ico
Normal file
@ -0,0 +1 @@
|
|||||||
|
""
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>彩票数据分析系统</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1867
frontend/package-lock.json
generated
Normal file
1867
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "lottery-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.5.0",
|
||||||
|
"dayjs": "^1.11.9",
|
||||||
|
"echarts": "^5.4.3",
|
||||||
|
"element-plus": "^2.10.1",
|
||||||
|
"pinia": "^2.1.6",
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-router": "^4.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.3.4",
|
||||||
|
"sass": "^1.66.1",
|
||||||
|
"vite": "^4.4.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
119
frontend/src/App.vue
Normal file
119
frontend/src/App.vue
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="app-container">
|
||||||
|
<el-header>
|
||||||
|
<div class="header-left">
|
||||||
|
<h1>彩票数据分析系统</h1>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<el-menu
|
||||||
|
mode="horizontal"
|
||||||
|
router
|
||||||
|
:default-active="$route.path"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/">首页</el-menu-item>
|
||||||
|
<el-menu-item index="/ssq">双色球</el-menu-item>
|
||||||
|
<el-menu-item index="/dlt">大乐透</el-menu-item>
|
||||||
|
<el-menu-item index="/statistics">统计分析</el-menu-item>
|
||||||
|
<el-menu-item index="/number-generator">智能选号</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<el-main>
|
||||||
|
<router-view></router-view>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { ElContainer, ElHeader, ElMain, ElMenu, ElMenuItem, ElDrawer, ElButton } from 'element-plus'
|
||||||
|
import { Menu } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const drawer = ref(false)
|
||||||
|
const isMobile = ref(false)
|
||||||
|
const $route = useRoute()
|
||||||
|
|
||||||
|
const checkMobile = () => {
|
||||||
|
isMobile.value = window.innerWidth <= 768
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkMobile()
|
||||||
|
window.addEventListener('resize', checkMobile)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', checkMobile)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.el-header {
|
||||||
|
background-color: #409EFF;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 60px;
|
||||||
|
box-shadow: 0 2px 8px 0 rgba(64,158,255,0.08);
|
||||||
|
}
|
||||||
|
.header-left h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.menu-btn {
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
.el-menu {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
.el-menu-item {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.el-header {
|
||||||
|
flex-direction: row;
|
||||||
|
height: 50px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
.header-left h1 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
/* 不再隐藏el-menu */
|
||||||
|
}
|
||||||
|
.el-header .el-menu-item.is-active {
|
||||||
|
background-color: #fff !important;
|
||||||
|
color: #409EFF !important;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.el-header .el-menu-item {
|
||||||
|
color: #fff !important;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
.el-header .el-menu-item:not(.is-active):hover {
|
||||||
|
background: rgba(255,255,255,0.15) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.el-header .el-menu::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
93
frontend/src/api/lottery.js
Normal file
93
frontend/src/api/lottery.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: '/api/v1/lottery'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const lotteryApi = {
|
||||||
|
// 双色球相关接口
|
||||||
|
getSSQLotteries(params) {
|
||||||
|
return api.get('/ssq/', { params })
|
||||||
|
},
|
||||||
|
|
||||||
|
getLatestSSQ() {
|
||||||
|
return api.get('/ssq/latest')
|
||||||
|
},
|
||||||
|
|
||||||
|
createSSQLottery(data) {
|
||||||
|
return api.post('/ssq/', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
getSSQStatistics() {
|
||||||
|
return api.get('/ssq/statistics').then(response => {
|
||||||
|
const data = response.data
|
||||||
|
return {
|
||||||
|
red_balls: data.red_balls.map(item => ({
|
||||||
|
number: item[0],
|
||||||
|
count: item[1]
|
||||||
|
})),
|
||||||
|
blue_balls: data.blue_balls.map(item => ({
|
||||||
|
number: item[0],
|
||||||
|
count: item[1]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
importSSQData(file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return api.post('/ssq/import', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 大乐透相关接口
|
||||||
|
getDLTLotteries(params) {
|
||||||
|
return api.get('/dlt/', { params })
|
||||||
|
},
|
||||||
|
|
||||||
|
getLatestDLT() {
|
||||||
|
return api.get('/dlt/latest')
|
||||||
|
},
|
||||||
|
|
||||||
|
createDLTLottery(data) {
|
||||||
|
return api.post('/dlt/', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
getDLTStatistics() {
|
||||||
|
return api.get('/dlt/statistics').then(response => {
|
||||||
|
const data = response.data
|
||||||
|
return {
|
||||||
|
front_balls: data.front_balls.map(item => ({
|
||||||
|
number: item[0],
|
||||||
|
count: item[1]
|
||||||
|
})),
|
||||||
|
back_balls: data.back_balls.map(item => ({
|
||||||
|
number: item[0],
|
||||||
|
count: item[1]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
importDLTData(file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return api.post('/dlt/import', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
generateSSQNumbers(params) {
|
||||||
|
return api.get('/ssq/generate', { params })
|
||||||
|
},
|
||||||
|
|
||||||
|
generateDLTNumbers(params) {
|
||||||
|
return api.get('/dlt/generate', { params })
|
||||||
|
}
|
||||||
|
}
|
||||||
175
frontend/src/assets/main.css
Normal file
175
frontend/src/assets/main.css
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
/* 全局样式 */
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: #f6f8fa;
|
||||||
|
color: #222;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus 主题覆盖 */
|
||||||
|
.el-header {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #dcdfe6;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-main {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card {
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 4px 24px 0 rgba(64,158,255,0.08);
|
||||||
|
border: none;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card__header {
|
||||||
|
padding: 18px 24px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #f8fbff;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
min-width: 120px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary {
|
||||||
|
background: linear-gradient(90deg, #409eff 0%, #66b1ff 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary:hover {
|
||||||
|
background-color: #66b1ff;
|
||||||
|
border-color: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table th {
|
||||||
|
background: #f8fbff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination {
|
||||||
|
margin-top: 24px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__item {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input, .el-input-number {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式布局 */
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.el-row {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
.el-col {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.el-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.el-button {
|
||||||
|
min-width: 90px;
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
body, html {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.el-header h1 {
|
||||||
|
font-size: 15px !important;
|
||||||
|
}
|
||||||
|
.el-card__header {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
.el-button {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 0;
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.el-col {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
float: none;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__content {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
frontend/src/main.js
Normal file
23
frontend/src/main.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
// 创建应用实例
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 注册 Element Plus 图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用插件
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
// 挂载应用
|
||||||
|
app.mount('#app')
|
||||||
36
frontend/src/router/index.js
Normal file
36
frontend/src/router/index.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('../views/Home.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ssq',
|
||||||
|
name: 'SSQ',
|
||||||
|
component: () => import('../views/SSQ.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/dlt',
|
||||||
|
name: 'DLT',
|
||||||
|
component: () => import('../views/DLT.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/statistics',
|
||||||
|
name: 'Statistics',
|
||||||
|
component: () => import('../views/Statistics.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/number-generator',
|
||||||
|
name: 'NumberGenerator',
|
||||||
|
component: () => import('../views/NumberGenerator.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
403
frontend/src/views/DLT.vue
Normal file
403
frontend/src/views/DLT.vue
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dlt-container">
|
||||||
|
<el-card class="filter-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>查询条件</span>
|
||||||
|
<el-button type="primary" @click="handleImport">导入数据</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :inline="true" :model="queryForm" class="filter-form">
|
||||||
|
<el-form-item label="期号">
|
||||||
|
<el-input v-model="queryForm.issue" placeholder="请输入期号"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="开奖日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery">查询</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
<el-button type="success" @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="table-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>开奖记录</span>
|
||||||
|
<el-button type="primary" @click="handleAdd">添加记录</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="tableData" style="width: 100%" v-loading="loading">
|
||||||
|
<el-table-column prop="issue" label="期号" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="open_time" label="开奖日期" width="120"></el-table-column>
|
||||||
|
<el-table-column label="开奖号码">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="lottery-numbers">
|
||||||
|
<span v-for="i in 5" :key="i" class="front-ball">{{ row[`front_ball_${i}`] }}</span>
|
||||||
|
<span v-for="i in 2" :key="i" class="back-ball">{{ row[`back_ball_${i}`] }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
:current-page="currentPage.value"
|
||||||
|
:page-size="pageSize.value"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 添加/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogType === 'add' ? '添加记录' : '编辑记录'"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<el-form :model="form" label-width="100px">
|
||||||
|
<el-form-item label="期号">
|
||||||
|
<el-input v-model="form.issue"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="开奖日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.open_time"
|
||||||
|
type="date"
|
||||||
|
placeholder="选择日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="前区号码">
|
||||||
|
<el-input-number
|
||||||
|
v-for="i in 5"
|
||||||
|
:key="i"
|
||||||
|
v-model="form[`front_ball_${i}`]"
|
||||||
|
:min="1"
|
||||||
|
:max="35"
|
||||||
|
:controls="false"
|
||||||
|
class="number-input"
|
||||||
|
></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="后区号码">
|
||||||
|
<el-input-number
|
||||||
|
v-for="i in 2"
|
||||||
|
:key="i"
|
||||||
|
v-model="form[`back_ball_${i}`]"
|
||||||
|
:min="1"
|
||||||
|
:max="12"
|
||||||
|
:controls="false"
|
||||||
|
class="number-input"
|
||||||
|
></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 导入对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="importDialogVisible"
|
||||||
|
title="导入数据"
|
||||||
|
width="400px"
|
||||||
|
>
|
||||||
|
<el-upload
|
||||||
|
class="upload-demo"
|
||||||
|
drag
|
||||||
|
action="#"
|
||||||
|
:auto-upload="false"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
accept=".json"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将文件拖到此处,或<em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
只能上传json文件
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="importDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleImportSubmit">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue'
|
||||||
|
import { lotteryApi } from '../api/lottery'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
// 查询表单
|
||||||
|
const queryForm = reactive({
|
||||||
|
issue: '',
|
||||||
|
start_date: '',
|
||||||
|
end_date: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 日期范围
|
||||||
|
const dateRange = ref([])
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
// 对话框
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogType = ref('add')
|
||||||
|
const form = reactive({
|
||||||
|
issue: '',
|
||||||
|
open_time: '',
|
||||||
|
front_ball_1: 1,
|
||||||
|
front_ball_2: 1,
|
||||||
|
front_ball_3: 1,
|
||||||
|
front_ball_4: 1,
|
||||||
|
front_ball_5: 1,
|
||||||
|
back_ball_1: 1,
|
||||||
|
back_ball_2: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 导入对话框
|
||||||
|
const importDialogVisible = ref(false)
|
||||||
|
const importFile = ref(null)
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
...queryForm,
|
||||||
|
page: currentPage.value,
|
||||||
|
page_size: pageSize.value
|
||||||
|
}
|
||||||
|
const response = await lotteryApi.getDLTLotteries(params)
|
||||||
|
tableData.value = response.data
|
||||||
|
total.value = response.headers['x-total-count']
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取数据失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听日期范围变化
|
||||||
|
watch(dateRange, (val) => {
|
||||||
|
if (val) {
|
||||||
|
queryForm.start_date = val[0]
|
||||||
|
queryForm.end_date = val[1]
|
||||||
|
} else {
|
||||||
|
queryForm.start_date = ''
|
||||||
|
queryForm.end_date = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
const handleQuery = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
queryForm.issue = ''
|
||||||
|
dateRange.value = []
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
const handleExport = () => {
|
||||||
|
// TODO: 实现导出功能
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加记录
|
||||||
|
const handleAdd = () => {
|
||||||
|
dialogType.value = 'add'
|
||||||
|
Object.keys(form).forEach(key => {
|
||||||
|
form[key] = key.includes('ball') ? 1 : ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑记录
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
dialogType.value = 'edit'
|
||||||
|
Object.keys(form).forEach(key => {
|
||||||
|
form[key] = row[key]
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除记录
|
||||||
|
const handleDelete = (row) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'确定要删除这条记录吗?',
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
).then(async () => {
|
||||||
|
try {
|
||||||
|
// TODO: 实现删除功能
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
await lotteryApi.createDLTLottery(form)
|
||||||
|
ElMessage.success('添加成功')
|
||||||
|
} else {
|
||||||
|
// TODO: 实现编辑功能
|
||||||
|
ElMessage.success('编辑成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(dialogType.value === 'add' ? '添加失败' : '编辑失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入数据
|
||||||
|
const handleImport = () => {
|
||||||
|
importDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件变化
|
||||||
|
const handleFileChange = (file) => {
|
||||||
|
importFile.value = file.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交导入
|
||||||
|
const handleImportSubmit = async () => {
|
||||||
|
if (!importFile.value) {
|
||||||
|
ElMessage.warning('请选择文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await lotteryApi.importDLTData(importFile.value)
|
||||||
|
ElMessage.success('导入成功')
|
||||||
|
importDialogVisible.value = false
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('导入失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
pageSize.value = val
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
currentPage.value = val
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dlt-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lottery-numbers {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.front-ball,
|
||||||
|
.back-ball {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.front-ball {
|
||||||
|
background-color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-ball {
|
||||||
|
background-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-input {
|
||||||
|
width: 60px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-demo {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
261
frontend/src/views/Home.vue
Normal file
261
frontend/src/views/Home.vue
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home-container">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="data-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>双色球最新开奖</span>
|
||||||
|
<el-button type="primary" link @click="refreshSSQData">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="ssqLatest" class="lottery-info">
|
||||||
|
<div class="issue-info">
|
||||||
|
<span class="label">期号:</span>
|
||||||
|
<span class="value">{{ ssqLatest.issue }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="draw-time">
|
||||||
|
<span class="label">开奖时间:</span>
|
||||||
|
<span class="value">{{ formatDate(ssqLatest.open_time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="numbers">
|
||||||
|
<div class="red-balls">
|
||||||
|
<span v-for="i in 6" :key="i" class="ball red">{{ ssqLatest['red_ball_' + i] }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="blue-balls">
|
||||||
|
<span class="ball blue">{{ ssqLatest.blue_ball }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="loading">
|
||||||
|
<el-skeleton :rows="3" animated />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="data-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>大乐透最新开奖</span>
|
||||||
|
<el-button type="primary" link @click="refreshDLTData">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="dltLatest" class="lottery-info">
|
||||||
|
<div class="issue-info">
|
||||||
|
<span class="label">期号:</span>
|
||||||
|
<span class="value">{{ dltLatest.issue }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="draw-time">
|
||||||
|
<span class="label">开奖时间:</span>
|
||||||
|
<span class="value">{{ formatDate(dltLatest.open_time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="numbers">
|
||||||
|
<div class="front-balls">
|
||||||
|
<span v-for="i in 5" :key="i" class="ball red">{{ dltLatest['front_ball_' + i] }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="back-balls">
|
||||||
|
<span v-for="i in 2" :key="i" class="ball blue">{{ dltLatest['back_ball_' + i] }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="loading">
|
||||||
|
<el-skeleton :rows="3" animated />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" class="quick-row">
|
||||||
|
<el-col :xs="24" :sm="12" :md="6" v-for="(item, idx) in quickActions" :key="idx">
|
||||||
|
<el-button type="primary" class="quick-btn" @click="navigateTo(item.route)">
|
||||||
|
<el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
|
||||||
|
{{ item.label }}
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { lotteryApi } from '../api/lottery'
|
||||||
|
import { Document, TrendCharts, MagicStick } from '@element-plus/icons-vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const ssqLatest = ref(null)
|
||||||
|
const dltLatest = ref(null)
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date) => {
|
||||||
|
return dayjs(date).format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取双色球最新开奖
|
||||||
|
const fetchSSQLatest = async () => {
|
||||||
|
try {
|
||||||
|
const response = await lotteryApi.getLatestSSQ()
|
||||||
|
if (response && response.data) {
|
||||||
|
ssqLatest.value = response.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取双色球最新开奖失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取大乐透最新开奖
|
||||||
|
const fetchDLTLatest = async () => {
|
||||||
|
try {
|
||||||
|
const response = await lotteryApi.getLatestDLT()
|
||||||
|
if (response && response.data) {
|
||||||
|
dltLatest.value = response.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取大乐透最新开奖失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新双色球数据
|
||||||
|
const refreshSSQData = () => {
|
||||||
|
fetchSSQLatest()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新大乐透数据
|
||||||
|
const refreshDLTData = () => {
|
||||||
|
fetchDLTLatest()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面导航
|
||||||
|
const navigateTo = (route) => {
|
||||||
|
const routeMap = {
|
||||||
|
'ssq': 'SSQ',
|
||||||
|
'dlt': 'DLT',
|
||||||
|
'statistics': 'Statistics',
|
||||||
|
'generator': 'NumberGenerator'
|
||||||
|
}
|
||||||
|
router.push({ name: routeMap[route] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const quickActions = [
|
||||||
|
{ label: '双色球数据', route: 'ssq', icon: Document },
|
||||||
|
{ label: '大乐透数据', route: 'dlt', icon: Document },
|
||||||
|
{ label: '统计分析', route: 'statistics', icon: TrendCharts },
|
||||||
|
{ label: '智能选号', route: 'generator', icon: MagicStick }
|
||||||
|
]
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSSQLatest()
|
||||||
|
fetchDLTLatest()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lottery-info {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-info,
|
||||||
|
.draw-time {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #606266;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.numbers {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 5px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
background-color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue {
|
||||||
|
background-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-balls,
|
||||||
|
.blue-balls,
|
||||||
|
.front-balls,
|
||||||
|
.back-balls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue-balls,
|
||||||
|
.back-balls {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-row {
|
||||||
|
margin-top: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 18px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 8px 0 rgba(64,158,255,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.quick-btn {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 14px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.quick-btn {
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
177
frontend/src/views/NumberGenerator.vue
Normal file
177
frontend/src/views/NumberGenerator.vue
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<div class="number-generator-container">
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="双色球" name="ssq">
|
||||||
|
<el-form :model="ssqForm" label-width="120px">
|
||||||
|
<el-form-item label="选号策略">
|
||||||
|
<el-radio-group v-model="ssqForm.strategy">
|
||||||
|
<el-radio label="random">随机选号</el-radio>
|
||||||
|
<el-radio label="frequency">频率选号</el-radio>
|
||||||
|
<el-radio label="hot">热门号码</el-radio>
|
||||||
|
<el-radio label="cold">冷门号码</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="生成注数">
|
||||||
|
<el-input-number v-model="ssqForm.count" :min="1" :max="10" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="generateSSQNumbers">生成号码</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div v-if="ssqResults.length > 0" class="results-container">
|
||||||
|
<h3>生成结果</h3>
|
||||||
|
<div v-for="(result, index) in ssqResults" :key="index" class="number-group">
|
||||||
|
<div class="red-balls">
|
||||||
|
<span v-for="num in result.red_balls" :key="num" class="ball red">{{ num }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="blue-balls">
|
||||||
|
<span class="ball blue">{{ result.blue_ball }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="大乐透" name="dlt">
|
||||||
|
<el-form :model="dltForm" label-width="120px">
|
||||||
|
<el-form-item label="选号策略">
|
||||||
|
<el-radio-group v-model="dltForm.strategy">
|
||||||
|
<el-radio label="random">随机选号</el-radio>
|
||||||
|
<el-radio label="frequency">频率选号</el-radio>
|
||||||
|
<el-radio label="hot">热门号码</el-radio>
|
||||||
|
<el-radio label="cold">冷门号码</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="生成注数">
|
||||||
|
<el-input-number v-model="dltForm.count" :min="1" :max="10" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="generateDLTNumbers">生成号码</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div v-if="dltResults.length > 0" class="results-container">
|
||||||
|
<h3>生成结果</h3>
|
||||||
|
<div v-for="(result, index) in dltResults" :key="index" class="number-group">
|
||||||
|
<div class="front-balls">
|
||||||
|
<span v-for="num in result.front_balls" :key="num" class="ball red">{{ num }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="back-balls">
|
||||||
|
<span v-for="num in result.back_balls" :key="num" class="ball blue">{{ num }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { lotteryApi } from '../api/lottery'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const activeTab = ref('ssq')
|
||||||
|
|
||||||
|
// 双色球表单
|
||||||
|
const ssqForm = ref({
|
||||||
|
strategy: 'random',
|
||||||
|
count: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 大乐透表单
|
||||||
|
const dltForm = ref({
|
||||||
|
strategy: 'random',
|
||||||
|
count: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成结果
|
||||||
|
const ssqResults = ref([])
|
||||||
|
const dltResults = ref([])
|
||||||
|
|
||||||
|
// 生成双色球号码
|
||||||
|
const generateSSQNumbers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await lotteryApi.generateSSQNumbers({
|
||||||
|
strategy: ssqForm.value.strategy,
|
||||||
|
count: ssqForm.value.count
|
||||||
|
})
|
||||||
|
ssqResults.value = response.data.results
|
||||||
|
ElMessage.success('号码生成成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成号码失败:', error)
|
||||||
|
ElMessage.error('生成号码失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成大乐透号码
|
||||||
|
const generateDLTNumbers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await lotteryApi.generateDLTNumbers({
|
||||||
|
strategy: dltForm.value.strategy,
|
||||||
|
count: dltForm.value.count
|
||||||
|
})
|
||||||
|
dltResults.value = response.data.results
|
||||||
|
ElMessage.success('号码生成成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成号码失败:', error)
|
||||||
|
ElMessage.error('生成号码失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.number-generator-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 5px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
background-color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue {
|
||||||
|
background-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-balls,
|
||||||
|
.blue-balls,
|
||||||
|
.front-balls,
|
||||||
|
.back-balls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue-balls,
|
||||||
|
.back-balls {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
401
frontend/src/views/SSQ.vue
Normal file
401
frontend/src/views/SSQ.vue
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ssq-container">
|
||||||
|
<el-card class="filter-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>查询条件</span>
|
||||||
|
<el-button type="primary" @click="handleImport">导入数据</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :inline="true" :model="queryForm" class="filter-form">
|
||||||
|
<el-form-item label="期号">
|
||||||
|
<el-input v-model="queryForm.issue" placeholder="请输入期号"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="开奖日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery">查询</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
<el-button type="success" @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="table-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>开奖记录</span>
|
||||||
|
<el-button type="primary" @click="handleAdd">添加记录</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="tableData" style="width: 100%" v-loading="loading">
|
||||||
|
<el-table-column prop="issue" label="期号" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="open_time" label="开奖日期" width="120"></el-table-column>
|
||||||
|
<el-table-column label="开奖号码">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="lottery-numbers">
|
||||||
|
<span v-for="i in 6" :key="i" class="red-ball">{{ row[`red_ball_${i}`] }}</span>
|
||||||
|
<span class="blue-ball">{{ row.blue_ball }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
:current-page="currentPage.value"
|
||||||
|
:page-size="pageSize.value"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 添加/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogType === 'add' ? '添加记录' : '编辑记录'"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<el-form :model="form" label-width="100px">
|
||||||
|
<el-form-item label="期号">
|
||||||
|
<el-input v-model="form.issue"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="开奖日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.open_time"
|
||||||
|
type="date"
|
||||||
|
placeholder="选择日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="红球">
|
||||||
|
<el-input-number
|
||||||
|
v-for="i in 6"
|
||||||
|
:key="i"
|
||||||
|
v-model="form[`red_ball_${i}`]"
|
||||||
|
:min="1"
|
||||||
|
:max="33"
|
||||||
|
:controls="false"
|
||||||
|
class="number-input"
|
||||||
|
></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="蓝球">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.blue_ball"
|
||||||
|
:min="1"
|
||||||
|
:max="16"
|
||||||
|
:controls="false"
|
||||||
|
class="number-input"
|
||||||
|
></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 导入对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="importDialogVisible"
|
||||||
|
title="导入数据"
|
||||||
|
width="400px"
|
||||||
|
>
|
||||||
|
<el-upload
|
||||||
|
class="upload-demo"
|
||||||
|
drag
|
||||||
|
action="#"
|
||||||
|
:auto-upload="false"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
accept=".json"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将文件拖到此处,或<em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
只能上传json文件
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="importDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleImportSubmit">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, watch } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue'
|
||||||
|
import { lotteryApi } from '../api/lottery'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
// 查询表单
|
||||||
|
const queryForm = reactive({
|
||||||
|
issue: '',
|
||||||
|
start_date: '',
|
||||||
|
end_date: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 日期范围
|
||||||
|
const dateRange = ref([])
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
// 对话框
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogType = ref('add')
|
||||||
|
const form = reactive({
|
||||||
|
issue: '',
|
||||||
|
open_time: '',
|
||||||
|
red_ball_1: 1,
|
||||||
|
red_ball_2: 1,
|
||||||
|
red_ball_3: 1,
|
||||||
|
red_ball_4: 1,
|
||||||
|
red_ball_5: 1,
|
||||||
|
red_ball_6: 1,
|
||||||
|
blue_ball: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 导入对话框
|
||||||
|
const importDialogVisible = ref(false)
|
||||||
|
const importFile = ref(null)
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
...queryForm,
|
||||||
|
page: currentPage.value,
|
||||||
|
page_size: pageSize.value
|
||||||
|
}
|
||||||
|
const response = await lotteryApi.getSSQLotteries(params)
|
||||||
|
tableData.value = response.data
|
||||||
|
total.value = response.headers['x-total-count']
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取数据失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听日期范围变化
|
||||||
|
watch(dateRange, (val) => {
|
||||||
|
if (val) {
|
||||||
|
queryForm.start_date = val[0]
|
||||||
|
queryForm.end_date = val[1]
|
||||||
|
} else {
|
||||||
|
queryForm.start_date = ''
|
||||||
|
queryForm.end_date = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
const handleQuery = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
queryForm.issue = ''
|
||||||
|
dateRange.value = []
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
const handleExport = () => {
|
||||||
|
// TODO: 实现导出功能
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加记录
|
||||||
|
const handleAdd = () => {
|
||||||
|
dialogType.value = 'add'
|
||||||
|
Object.keys(form).forEach(key => {
|
||||||
|
form[key] = key.includes('ball') ? 1 : ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑记录
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
dialogType.value = 'edit'
|
||||||
|
Object.keys(form).forEach(key => {
|
||||||
|
form[key] = row[key]
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除记录
|
||||||
|
const handleDelete = (row) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'确定要删除这条记录吗?',
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
).then(async () => {
|
||||||
|
try {
|
||||||
|
// TODO: 实现删除功能
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
await lotteryApi.createSSQLottery(form)
|
||||||
|
ElMessage.success('添加成功')
|
||||||
|
} else {
|
||||||
|
// TODO: 实现编辑功能
|
||||||
|
ElMessage.success('编辑成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(dialogType.value === 'add' ? '添加失败' : '编辑失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入数据
|
||||||
|
const handleImport = () => {
|
||||||
|
importDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件变化
|
||||||
|
const handleFileChange = (file) => {
|
||||||
|
importFile.value = file.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交导入
|
||||||
|
const handleImportSubmit = async () => {
|
||||||
|
if (!importFile.value) {
|
||||||
|
ElMessage.warning('请选择文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await lotteryApi.importSSQData(importFile.value)
|
||||||
|
ElMessage.success('导入成功')
|
||||||
|
importDialogVisible.value = false
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('导入失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
pageSize.value = val
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
currentPage.value = val
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ssq-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lottery-numbers {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-ball,
|
||||||
|
.blue-ball {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-ball {
|
||||||
|
background-color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue-ball {
|
||||||
|
background-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-input {
|
||||||
|
width: 60px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-demo {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
291
frontend/src/views/Statistics.vue
Normal file
291
frontend/src/views/Statistics.vue
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
<template>
|
||||||
|
<div class="statistics-container">
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="双色球" name="ssq">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>红球出现频率</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="ssqRedChart" class="chart"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>蓝球出现频率</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="ssqBlueChart" class="chart"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="大乐透" name="dlt">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>前区号码出现频率</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="dltFrontChart" class="chart"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>后区号码出现频率</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="dltBackChart" class="chart"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, onUnmounted, nextTick } from 'vue'
|
||||||
|
import { lotteryApi } from '../api/lottery'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
const activeTab = ref('ssq')
|
||||||
|
const ssqRedChart = ref(null)
|
||||||
|
const ssqBlueChart = ref(null)
|
||||||
|
const dltFrontChart = ref(null)
|
||||||
|
const dltBackChart = ref(null)
|
||||||
|
|
||||||
|
let ssqRedChartInstance = null
|
||||||
|
let ssqBlueChartInstance = null
|
||||||
|
let dltFrontChartInstance = null
|
||||||
|
let dltBackChartInstance = null
|
||||||
|
|
||||||
|
// 按需初始化图表
|
||||||
|
const initSSQCharts = () => {
|
||||||
|
if (!ssqRedChartInstance && ssqRedChart.value) {
|
||||||
|
ssqRedChartInstance = echarts.init(ssqRedChart.value)
|
||||||
|
}
|
||||||
|
if (!ssqBlueChartInstance && ssqBlueChart.value) {
|
||||||
|
ssqBlueChartInstance = echarts.init(ssqBlueChart.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const initDLTCharts = () => {
|
||||||
|
if (!dltFrontChartInstance && dltFrontChart.value) {
|
||||||
|
dltFrontChartInstance = echarts.init(dltFrontChart.value)
|
||||||
|
}
|
||||||
|
if (!dltBackChartInstance && dltBackChart.value) {
|
||||||
|
dltBackChartInstance = echarts.init(dltBackChart.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新双色球红球图表
|
||||||
|
const updateSSQRedChart = (data) => {
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(item => item.number),
|
||||||
|
name: '号码'
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '出现次数'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: data.map(item => item.count),
|
||||||
|
type: 'bar',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#f56c6c'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
ssqRedChartInstance.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新双色球蓝球图表
|
||||||
|
const updateSSQBlueChart = (data) => {
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(item => item.number),
|
||||||
|
name: '号码'
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '出现次数'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: data.map(item => item.count),
|
||||||
|
type: 'bar',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#409eff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
ssqBlueChartInstance.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新大乐透前区图表
|
||||||
|
const updateDLTFrontChart = (data) => {
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(item => item.number),
|
||||||
|
name: '号码'
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '出现次数'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: data.map(item => item.count),
|
||||||
|
type: 'bar',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#f56c6c'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
dltFrontChartInstance.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新大乐透后区图表
|
||||||
|
const updateDLTBackChart = (data) => {
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(item => item.number),
|
||||||
|
name: '号码'
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '出现次数'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: data.map(item => item.count),
|
||||||
|
type: 'bar',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#409eff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
dltBackChartInstance.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取统计数据
|
||||||
|
const fetchStatistics = async () => {
|
||||||
|
try {
|
||||||
|
if (activeTab.value === 'ssq') {
|
||||||
|
initSSQCharts()
|
||||||
|
const ssqData = await lotteryApi.getSSQStatistics()
|
||||||
|
updateSSQRedChart(ssqData.red_balls)
|
||||||
|
updateSSQBlueChart(ssqData.blue_balls)
|
||||||
|
} else {
|
||||||
|
initDLTCharts()
|
||||||
|
const dltData = await lotteryApi.getDLTStatistics()
|
||||||
|
updateDLTFrontChart(dltData.front_balls)
|
||||||
|
updateDLTBackChart(dltData.back_balls)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听标签页切换
|
||||||
|
watch(activeTab, async () => {
|
||||||
|
await nextTick() // 等待DOM渲染完成
|
||||||
|
if (activeTab.value === 'ssq') {
|
||||||
|
initSSQCharts()
|
||||||
|
} else {
|
||||||
|
initDLTCharts()
|
||||||
|
}
|
||||||
|
fetchStatistics()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
const handleResize = () => {
|
||||||
|
ssqRedChartInstance?.resize()
|
||||||
|
ssqBlueChartInstance?.resize()
|
||||||
|
dltFrontChartInstance?.resize()
|
||||||
|
dltBackChartInstance?.resize()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick() // 等待DOM渲染完成
|
||||||
|
if (activeTab.value === 'ssq') {
|
||||||
|
initSSQCharts()
|
||||||
|
} else {
|
||||||
|
initDLTCharts()
|
||||||
|
}
|
||||||
|
fetchStatistics()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
ssqRedChartInstance?.dispose()
|
||||||
|
ssqBlueChartInstance?.dispose()
|
||||||
|
dltFrontChartInstance?.dispose()
|
||||||
|
dltBackChartInstance?.dispose()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.statistics-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
21
frontend/vite.config.js
Normal file
21
frontend/vite.config.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user