Initial commit: Project setup with frontend and backend implementation

This commit is contained in:
Mars 2025-06-13 15:50:11 +08:00
commit 332f4dddd6
38 changed files with 4871 additions and 0 deletions

31
.gitignore vendored Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
"""
Lottery backend package
"""

3
backend/app/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""
Lottery application package
"""

View File

@ -0,0 +1,3 @@
"""
API package
"""

View File

@ -0,0 +1 @@

View 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)

View File

@ -0,0 +1,3 @@
"""
API v1 package
"""

View 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}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,7 @@
class Settings:
PROJECT_NAME = "彩票数据分析系统"
API_V1_STR = "/api/v1"
BACKEND_CORS_ORIGINS = ["*"]
settings = Settings()

View 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()

View 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
View 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)

View File

@ -0,0 +1 @@

View 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="更新时间")

View File

@ -0,0 +1 @@

View 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

View File

@ -0,0 +1 @@

View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
""

13
frontend/index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

24
frontend/package.json Normal file
View 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
View 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>

View 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 })
}
}

View 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
View 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')

View 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
View 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
View 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>

View 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
View 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>

View 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
View 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,
},
},
},
})