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