彩票助手版本1.0

This commit is contained in:
lihanqi
2025-08-01 19:03:57 +08:00
commit 653e84562f
94 changed files with 26389 additions and 0 deletions

402
Controller.txt Normal file
View File

@@ -0,0 +1,402 @@
package com.xy.xyaicpzs.controller;
import com.xy.xyaicpzs.common.ErrorCode;
import com.xy.xyaicpzs.common.ResultUtils;
import com.xy.xyaicpzs.common.response.ApiResponse;
import com.xy.xyaicpzs.domain.entity.LotteryDraws;
import com.xy.xyaicpzs.domain.entity.PredictRecord;
import com.xy.xyaicpzs.service.BallAnalysisService;
import com.xy.xyaicpzs.service.LotteryDrawsService;
import com.xy.xyaicpzs.service.PredictRecordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* 球号分析控制器
* 提供球号分析算法的API接口
*/
@Slf4j
@RestController
@RequestMapping("/ball-analysis")
@Tag(name = "球号分析", description = "球号分析算法API")
public class BallAnalysisController {
@Autowired
private BallAnalysisService ballAnalysisService;
@Autowired
private LotteryDrawsService lotteryDrawsService;
@Autowired
private PredictRecordService predictRecordService;
@GetMapping("/predict-records/{userId}")
@Operation(summary = "获取用户推测记录", description = "根据用户ID获取该用户的所有推测记录按推测时间倒序排列")
public ApiResponse<List<PredictRecord>> getPredictRecordsByUserId(
@Parameter(description = "用户ID例如1001", required = true)
@PathVariable Long userId
, HttpServletRequest request) {
User loginUser = userService.getLoginUser(request);
if (loginUser == null){
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
}
try {
log.info("接收到获取用户推测记录请求用户ID={}", userId);
// 调用服务获取用户推测记录
List<PredictRecord> result = predictRecordService.getPredictRecordsByUserId(userId);
log.info("获取用户推测记录完成用户ID{},返回{}条记录", userId, result.size());
return ResultUtils.success(result);
} catch (Exception e) {
log.error("获取用户推测记录失败:{}", e.getMessage(), e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取用户推测记录失败:" + e.getMessage());
}
}
/**
* 获取近期开奖信息
* @param limit 获取条数可选参数默认15条
* @return 近期开奖信息列表
*/
@GetMapping("/recent-draws")
@Operation(summary = "获取近期开奖信息", description = "获取最近的开奖信息默认返回15条按开奖期号倒序排列")
public ApiResponse<List<LotteryDraws>> getRecentDraws(
@Parameter(description = "获取条数默认15条", required = false)
@RequestParam(required = false, defaultValue = "15") Integer limit) {
try {
log.info("接收到获取近期开奖信息请求:条数={}", limit);
// 调用服务获取近期开奖信息
List<LotteryDraws> result = lotteryDrawsService.getRecentDraws(limit);
log.info("获取近期开奖信息完成,返回{}条记录", result.size());
return ResultUtils.success(result);
} catch (Exception e) {
log.error("获取近期开奖信息失败:{}", e.getMessage(), e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取近期开奖信息失败:" + e.getMessage());
}
}
/**
* 根据日期范围查询开奖信息
* @param startDate 开始日期可选格式yyyy-MM-dd
* @param endDate 结束日期可选格式yyyy-MM-dd
* @return 开奖信息列表
*/
@GetMapping("/query-draws")
@Operation(summary = "按日期范围查询开奖信息", description = "根据日期范围查询开奖信息,支持单边日期查询")
public ApiResponse<List<LotteryDraws>> queryDraws(
@Parameter(description = "开始日期格式yyyy-MM-dd例如2025-01-01", required = false)
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@Parameter(description = "结束日期格式yyyy-MM-dd例如2025-01-31", required = false)
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
try {
log.info("接收到按日期范围查询开奖信息请求:开始日期={},结束日期={}", startDate, endDate);
// 日期范围验证
if (startDate != null && endDate != null && startDate.after(endDate)) {
return ResultUtils.error(ErrorCode.PARAMS_ERROR, "开始日期不能晚于结束日期");
}
// 调用服务按日期范围查询开奖信息
List<LotteryDraws> result = lotteryDrawsService.getByDateRange(startDate, endDate);
log.info("按日期范围查询开奖信息完成,返回{}条记录", result.size());
return ResultUtils.success(result);
} catch (Exception e) {
log.error("按日期范围查询开奖信息失败:{}", e.getMessage(), e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "查询开奖信息失败:" + e.getMessage());
}
}
/**
* 根据期号精准查询单条开奖信息
* @param drawId 开奖期号
* @return 开奖信息
*/
@GetMapping("/draw/{drawId}")
@Operation(summary = "根据期号查询开奖信息", description = "根据期号精准查询单条开奖信息")
public ApiResponse<LotteryDraws> getDrawById(
@Parameter(description = "开奖期号例如2025056", required = true)
@PathVariable Long drawId) {
try {
log.info("接收到根据期号查询开奖信息请求:期号={}", drawId);
// 调用服务查询开奖信息
LotteryDraws result = lotteryDrawsService.getByDrawId(drawId);
if (result == null) {
log.warn("未找到期号为{}的开奖信息", drawId);
return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "未找到期号为" + drawId + "的开奖信息");
}
log.info("根据期号查询开奖信息完成:{}", result.getDrawId());
return ResultUtils.success(result);
} catch (Exception e) {
log.error("根据期号查询开奖信息失败:{}", e.getMessage(), e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "查询开奖信息失败:" + e.getMessage());
}
}
/**
* 创建推测记录
* @param userId 用户ID
* @param drawId 开奖期号
* @param drawDate 开奖日期
* @param redBalls 6个红球号码用逗号分隔
* @param blueBall 蓝球号码
* @return 创建的推测记录
*/
@PostMapping("/create-predict")
@Operation(summary = "创建推测记录", description = "向predict_record表插入一条推测记录数据")
public ApiResponse<PredictRecord> createPredictRecord(
@Parameter(description = "用户ID例如1001", required = true)
@RequestParam Long userId,
@Parameter(description = "开奖期号例如2025056", required = true)
@RequestParam Long drawId,
@Parameter(description = "开奖日期格式yyyy-MM-dd", required = true)
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date drawDate,
@Parameter(description = "6个红球号码用逗号分隔例如1,5,12,18,25,33", required = true)
@RequestParam String redBalls,
@Parameter(description = "蓝球号码例如8", required = true)
@RequestParam Integer blueBall) {
try {
log.info("接收到创建推测记录请求用户ID={},期号={},开奖日期={},红球={},蓝球={}",
userId, drawId, drawDate, redBalls, blueBall);
// 解析红球号码
List<Integer> redBallList = parseRedBalls(redBalls, 6, "红球");
// 调用服务创建推测记录
PredictRecord result = predictRecordService.createPredictRecord(userId, drawId, drawDate, redBallList, blueBall);
log.info("创建推测记录完成用户ID{}记录ID{}", userId, result.getId());
return ResultUtils.success(result);
} catch (Exception e) {
log.error("创建推测记录失败:{}", e.getMessage(), e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "创建推测记录失败:" + e.getMessage());
}
}
/**
* 球号分析算法
* @param level 高位/中位/低位标识 (H/M/L)
* @param redBalls 6个红球号码用逗号分隔
* @param blueBall 蓝球号码
* @return 分析结果出现频率最高的前11位数字
*/
@PostMapping("/analyze")
@Operation(summary = "首球算法", description = "根据输入的级别、红球和蓝球分析出现频率最高的前11位数字")
public ApiResponse<List<Integer>> analyzeBalls(
@Parameter(description = "级别H(高位)/M(中位)/L(低位)", required = true)
@RequestParam String level,
@Parameter(description = "6个红球号码用逗号分隔例如1,5,12,18,25,33", required = true)
@RequestParam String redBalls,
@Parameter(description = "蓝球号码例如8", required = true)
@RequestParam Integer blueBall) {
try {
log.info("接收到球号分析请求:级别={},红球={},蓝球={}", level, redBalls, blueBall);
// 解析红球号码
List<Integer> redBallList = parseRedBalls(redBalls, 6, "红球");
// 调用分析服务
List<Integer> result = ballAnalysisService.analyzeBalls(level, redBallList, blueBall);
log.info("球号分析完成,结果:{}", result);
return ResultUtils.success(result);
} catch (Exception e) {
log.error("球号分析失败:{}", e.getMessage(), e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "球号分析失败:" + e.getMessage());
}
}
/**
* 跟随球号分析算法
* @param level 高位/中位/低位标识 (H/M/L)
* @param firstThreeRedBalls 前3个红球号码用逗号分隔
* @param lastSixRedBalls 后6个红球号码用逗号分隔
* @param blueBall 蓝球号码
* @return 分析结果出现频率最高的前8位数字
*/
@PostMapping("/fallow")
@Operation(summary = "跟随球号分析算法", description = "根据输入的级别、前3个红球、后6个红球和蓝球分析出现频率最高的前8位数字")
public ApiResponse<List<Integer>> fallowBallAnalysis(
@Parameter(description = "级别H(高位)/M(中位)/L(低位)", required = true)
@RequestParam String level,
@Parameter(description = "前3个红球号码用逗号分隔例如7,24,27", required = true)
@RequestParam String firstThreeRedBalls,
@Parameter(description = "后6个红球号码用逗号分隔例如21,10,5,15,23,28", required = true)
@RequestParam String lastSixRedBalls,
@Parameter(description = "蓝球号码例如16", required = true)
@RequestParam Integer blueBall) {
try {
log.info("接收到跟随球号分析请求:级别={}前3个红球={}后6个红球={},蓝球={}",
level, firstThreeRedBalls, lastSixRedBalls, blueBall);
// 解析红球号码
List<Integer> firstThreeRedBallList = parseRedBalls(firstThreeRedBalls, 3, "前3个红球");
List<Integer> lastSixRedBallList = parseRedBalls(lastSixRedBalls, 6, "后6个红球");
// 调用分析服务
List<Integer> result = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBallList, lastSixRedBallList, blueBall);
log.info("跟随球号分析完成,结果:{}", result);
return ResultUtils.success(result);
} catch (Exception e) {
log.error("跟随球号分析失败:{}", e.getMessage(), e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "跟随球号分析失败:" + e.getMessage());
}
}
/**
* 解析红球号码字符串
*/
private List<Integer> parseRedBalls(String redBalls, int expectedCount, String ballType) {
if (redBalls == null || redBalls.trim().isEmpty()) {
throw new IllegalArgumentException(ballType + "号码不能为空");
}
try {
String[] parts = redBalls.split(",");
if (parts.length != expectedCount) {
throw new IllegalArgumentException(ballType + "数量必须为" + expectedCount + "个,实际:" + parts.length);
}
List<Integer> result = Arrays.stream(parts)
.map(String::trim)
.map(Integer::parseInt)
.collect(java.util.stream.Collectors.toList());
// 验证红球号码范围
for (Integer ball : result) {
if (ball < 1 || ball > 33) {
throw new IllegalArgumentException(ballType + "号码必须在1-33范围内错误值" + ball);
}
}
return result;
} catch (NumberFormatException e) {
throw new IllegalArgumentException(ballType + "号码格式错误,请使用逗号分隔的数字");
}
}
/**
* 蓝球分析算法
* @param level 高位/中位/低位标识 (H/M/L)
* @param predictedRedBalls 6个推测红球号码用逗号分隔
* @param predictedBlueBalls 2个推测蓝球号码用逗号分隔
* @param lastRedBalls 6个上期红球号码用逗号分隔
* @param lastBlueBall 上期蓝球号码
* @return 分析结果频率最高的前4个蓝球号码
*/
@PostMapping("/blue-ball")
@Operation(summary = "蓝球分析算法", description = "根据输入的级别、推测红球、推测蓝球、上期红球和上期蓝球分析出频率最高的前4个蓝球号码")
public ApiResponse<List<Integer>> blueBallAnalysis(
@Parameter(description = "级别H(高位)/M(中位)/L(低位)", required = true)
@RequestParam String level,
@Parameter(description = "6个推测红球号码用逗号分隔例如26,20,18,32,10,14", required = true)
@RequestParam String predictedRedBalls,
@Parameter(description = "2个推测蓝球号码用逗号分隔例如5,8", required = true)
@RequestParam String predictedBlueBalls,
@Parameter(description = "6个上期红球号码用逗号分隔例如7,24,27,21,10,5", required = true)
@RequestParam String lastRedBalls,
@Parameter(description = "上期蓝球号码例如16", required = true)
@RequestParam Integer lastBlueBall) {
try {
log.info("接收到蓝球分析请求:级别={},推测红球={},推测蓝球={},上期红球={},上期蓝球={}",
level, predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall);
// 解析球号
List<Integer> predictedRedBallList = parseRedBalls(predictedRedBalls, 6, "推测红球");
List<Integer> predictedBlueBallList = parseBlueBalls(predictedBlueBalls, 2, "推测蓝球");
List<Integer> lastRedBallList = parseRedBalls(lastRedBalls, 6, "上期红球");
// 调用分析服务
List<Integer> result = ballAnalysisService.blueBallAnalysis(
level, predictedRedBallList, predictedBlueBallList, lastRedBallList, lastBlueBall);
log.info("蓝球分析完成,结果:{}", result);
return ResultUtils.success(result);
} catch (Exception e) {
log.error("蓝球分析失败:{}", e.getMessage(), e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "蓝球分析失败:" + e.getMessage());
}
}
/**
* 解析蓝球号码字符串
*/
private List<Integer> parseBlueBalls(String blueBalls, int expectedCount, String ballType) {
if (blueBalls == null || blueBalls.trim().isEmpty()) {
throw new IllegalArgumentException(ballType + "号码不能为空");
}
try {
String[] parts = blueBalls.split(",");
if (parts.length != expectedCount) {
throw new IllegalArgumentException(ballType + "数量必须为" + expectedCount + "个,实际:" + parts.length);
}
List<Integer> result = Arrays.stream(parts)
.map(String::trim)
.map(Integer::parseInt)
.collect(java.util.stream.Collectors.toList());
// 验证蓝球号码范围
for (Integer ball : result) {
if (ball < 1 || ball > 16) {
throw new IllegalArgumentException(ballType + "号码必须在1-16范围内错误值" + ball);
}
}
return result;
} catch (NumberFormatException e) {
throw new IllegalArgumentException(ballType + "号码格式错误,请使用逗号分隔的数字");
}
}
}

BIN
lottery-app.zip Normal file

Binary file not shown.

30
lottery-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

3
lottery-app/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

134
lottery-app/README.md Normal file
View File

@@ -0,0 +1,134 @@
# 双色球智能推测系统
这是一个基于Vue 3开发的双色球智能推测前端应用提供智能算法分析、开奖信息查询等功能。
## 功能特性
### 🎯 主要功能
- **智能推测**: 5步推测流程包含首球算法、跟随球分析、蓝球分析
- **开奖查询**: 支持期号查询和日期范围查询
- **用户中心**: 个人信息管理和会员权益展示
### 📱 页面结构
1. **首页** - 智能推测功能
- 选择算法级别(高位/中位/低位)
- 输入上期开奖号码
- 首球算法分析
- 跟随球分析
- 蓝球分析
- 最终号码确认
2. **开奖信息** - 查询历史开奖
- 期号精确查询
- 日期范围查询
- 近期开奖记录展示
3. **我的页面** - 用户信息管理
- 用户信息展示
- 会员权益介绍
- 功能菜单导航
- 使用统计数据
## 技术栈
- **框架**: Vue 3
- **路由**: Vue Router 4
- **HTTP客户端**: Axios
- **构建工具**: Vite
- **CSS**: 原生CSS + Scoped Styles
## 后端接口
应用连接到SpringBoot后端服务接口前缀`http://localhost:8123/api`
### 主要接口
- `GET /ball-analysis/recent-draws` - 获取近期开奖信息
- `GET /ball-analysis/query-draws` - 按日期范围查询
- `GET /ball-analysis/draw/{drawId}` - 根据期号查询
- `POST /ball-analysis/analyze` - 首球算法分析
- `POST /ball-analysis/fallow` - 跟随球分析
- `POST /ball-analysis/blue-ball` - 蓝球分析
- `POST /ball-analysis/create-predict` - 创建推测记录
## 快速开始
### 安装依赖
```bash
npm install
```
### 开发环境运行
```bash
npm run dev
```
### 构建生产版本
```bash
npm run build
```
### 预览生产构建
```bash
npm run preview
```
## 项目结构
```
src/
├── api/
│ └── index.js # API接口封装
├── components/ # 公共组件
├── router/
│ └── index.js # 路由配置
├── views/
│ ├── Home.vue # 主页 - 智能推测
│ ├── LotteryInfo.vue # 开奖信息页
│ └── Profile.vue # 我的页面
├── App.vue # 根组件
└── main.js # 入口文件
```
## 使用说明
### 推测流程
1. **第一步**: 选择推测级别(高位/中位/低位),输入开奖期号、日期和上期中奖号码
2. **第二步**: 查看首球算法推荐的11个红球号码选择1个首球和2个随球
3. **第三步**: 查看跟随球分析推荐的8个号码组合完整的6个红球
4. **第四步**: 选择2个蓝球进行分析获得4个推荐蓝球选择最终蓝球
5. **第五步**: 确认推测号码并提交
### 开奖查询
- **期号查询**: 输入具体期号如2025056进行精确查询
- **日期查询**: 设置日期范围进行批量查询
- **历史记录**: 自动加载最近15期开奖信息
## 注意事项
⚠️ **重要提醒**: 彩票开奖系统随机,本应用提供的推测结果仅供参考,不保证中奖。投注需谨慎,请理性购彩。
## 浏览器支持
- Chrome >= 87
- Firefox >= 78
- Safari >= 14
- Edge >= 88
## 开发指南
### 代码规范
- 使用ES6+语法
- 组件采用SFCSingle File Component格式
- CSS使用scoped样式避免污染
- 遵循Vue 3 Composition API最佳实践
### API集成
所有API调用都封装在`src/api/index.js`中,统一处理请求和响应。
### 样式规范
- 采用移动端优先的响应式设计
- 主色调:红色(#e53e3e),蓝色(#3182ce)
- 遵循Material Design设计原则

13
lottery-app/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="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>

View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

3662
lottery-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
lottery-app/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "lottery-app",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.10.0",
"element-plus": "^2.10.4",
"qrcode": "^1.5.4",
"vue": "^3.5.13",
"vue-router": "^4.5.1",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

500
lottery-app/src/App.vue Normal file
View File

@@ -0,0 +1,500 @@
<script setup>
import { onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { userStore } from './store/user'
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
// 获取当前路由
const route = useRoute()
// 判断是否是后台管理路由
const isAdminRoute = computed(() => {
return route.path.startsWith('/admin')
})
// 应用启动时获取用户信息
onMounted(async () => {
// 如果用户已登录但没有完整的用户信息,则从后端获取
if (userStore.isLoggedIn && (!userStore.user?.id || typeof userStore.user.id === 'number')) {
try {
console.log('应用启动,尝试获取用户信息...')
await userStore.fetchLoginUser()
console.log('用户信息获取成功:', userStore.user)
} catch (error) {
console.error('获取用户信息失败:', error)
// 如果获取失败可能是token过期清除登录状态
if (error.response?.status === 401) {
userStore.logout()
}
}
}
// 如果是后台路由给body添加admin-body类
if (isAdminRoute.value) {
document.body.classList.add('admin-body')
} else {
document.body.classList.remove('admin-body')
}
})
</script>
<template>
<!-- 后台管理路由直接显示内容不显示底部导航 -->
<template v-if="isAdminRoute">
<router-view />
</template>
<!-- 前台用户路由显示应用容器和底部导航 -->
<div v-else class="app-container">
<main class="main-content">
<router-view />
</main>
<!-- 底部导航栏 -->
<nav class="bottom-nav">
<router-link to="/" class="nav-item" :class="{ active: $route.path === '/' }">
<div class="nav-icon">
<img src="@/assets/banner/home.png" alt="首页" class="nav-icon-img" />
</div>
<div class="nav-text">首页</div>
</router-link>
<router-link to="/lottery-info" class="nav-item" :class="{ active: $route.path === '/lottery-info' }">
<div class="nav-icon">
<img src="@/assets/banner/kaijiang.png" alt="开奖" class="nav-icon-img" />
</div>
<div class="nav-text">开奖</div>
</router-link>
<router-link to="/profile" class="nav-item" :class="{ active: $route.path === '/profile' }">
<div class="nav-icon">
<img src="@/assets/banner/wode.png" alt="我的" class="nav-icon-img" />
</div>
<div class="nav-text">我的</div>
</router-link>
</nav>
</div>
</template>
<style>
/* 全局重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
background: #f0f2f5 !important;
min-height: 100vh;
width: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #f0f2f5 !important;
line-height: 1.5;
min-height: 100vh;
margin: 0 !important;
padding: 30px !important;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
/* 应用容器 - 包含主内容和底部导航 */
.app-container {
min-height: calc(100vh - 60px);
display: flex;
flex-direction: column;
position: relative;
max-width: 900px;
width: 100%;
margin: 0 auto;
background: white;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.08);
border-radius: 12px;
overflow: hidden;
}
/* 主要内容区域 */
.main-content {
flex: 1;
background: #f0f2f5;
position: relative;
}
/* 底部导航栏 */
.bottom-nav {
height: 75px;
background: white;
display: flex;
align-items: center;
justify-content: space-around;
border-top: 1px solid #f0f0f0;
z-index: 1000;
padding: 5px 0;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.1);
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-decoration: none;
color: #9ca3af;
transition: all 0.3s ease;
padding: 0;
min-width: 65px;
position: relative;
}
.nav-item:hover {
color: #e53e3e;
}
.nav-item:hover .nav-icon-img {
transform: scale(1.05);
}
.nav-item.active {
color: #e53e3e;
}
.nav-item.active .nav-icon {
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
border-radius: 50%;
box-shadow: 0 4px 12px rgba(229, 62, 62, 0.3);
}
.nav-item.active .nav-icon-img {
filter: brightness(0) invert(1);
transform: scale(1.1);
}
.nav-item.active .nav-text {
color: #e53e3e;
font-weight: 700;
}
.nav-icon {
margin-bottom: 5px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
}
.nav-icon-img {
width: 26px;
height: 26px;
object-fit: contain;
transition: all 0.3s ease;
}
.nav-icon svg {
width: 24px;
height: 24px;
}
.nav-text {
font-size: 12px;
font-weight: 500;
text-align: center;
transition: all 0.3s ease;
}
/* 移动端适配 */
@media (max-width: 768px) {
.bottom-nav {
height: 70px;
padding: 3px 0;
}
.nav-item {
padding: 0;
min-width: 60px;
}
.nav-icon-img {
width: 28px;
height: 28px;
}
.nav-text {
font-size: 11px;
}
.nav-item.active::before {
width: 30px;
height: 3px;
}
}
/* 通用容器样式 */
.container {
width: 100%;
background: #f0f2f5;
position: relative;
}
/* 页面头部样式 */
.page-header {
background: url('@/assets/banner/banner.png') center/cover no-repeat, linear-gradient(135deg, #e53e3e 0%, #ff6b6b 100%);
color: white;
padding: 60px 20px 30px;
text-align: center;
position: relative;
overflow: hidden;
}
.page-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="20" cy="20" r="2" fill="rgba(255,255,255,0.1)"/><circle cx="80" cy="40" r="1.5" fill="rgba(255,255,255,0.1)"/><circle cx="40" cy="70" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="70" cy="80" r="2.5" fill="rgba(255,255,255,0.1)"/></svg>');
pointer-events: none;
}
.page-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
position: relative;
z-index: 1;
}
.page-subtitle {
font-size: 16px;
opacity: 0.9;
position: relative;
z-index: 1;
}
/* 通用按钮样式 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 12px;
padding: 14px 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.btn:hover::before {
left: 100%;
}
.btn-primary {
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
color: white;
box-shadow: 0 4px 15px rgba(229, 62, 62, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(229, 62, 62, 0.4);
}
.btn-secondary {
background: #f8f9fa;
color: #6c757d;
border: 1px solid #e9ecef;
}
.btn-secondary:hover {
background: #e9ecef;
border-color: #d1d5db;
transform: translateY(-1px);
}
/* 球号样式 */
.ball-red {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
color: white;
border-radius: 50%;
font-size: 14px;
font-weight: bold;
margin: 3px;
box-shadow: 0 2px 8px rgba(229, 62, 62, 0.3);
transition: transform 0.2s ease;
}
.ball-red:hover {
transform: scale(1.1);
}
.ball-blue {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: white;
border-radius: 50%;
font-size: 14px;
font-weight: bold;
margin: 3px;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
transition: transform 0.2s ease;
}
.ball-blue:hover {
transform: scale(1.1);
}
/* 输入框样式 */
.form-input {
width: 100%;
padding: 16px;
border: 2px solid #e9ecef;
border-radius: 12px;
font-size: 16px;
transition: all 0.3s ease;
background: #f8f9fa;
}
.form-input:focus {
outline: none;
border-color: #e53e3e;
background: white;
box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.1);
}
.form-input::placeholder {
color: #9ca3af;
}
/* 卡片样式 */
.card {
background: white;
border-radius: 16px;
padding: 20px;
margin: 16px;
box-shadow: 0 4px 25px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 35px rgba(0, 0, 0, 0.15);
}
/* 加载动画 */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 1024px) {
.app-container {
max-width: 800px;
}
}
@media (max-width: 768px) {
body {
padding: 15px !important;
}
.app-container {
max-width: 100%;
min-height: calc(100vh - 30px);
border-radius: 8px;
}
}
@media (max-width: 480px) {
body {
padding: 10px !important;
}
.app-container {
border-radius: 0;
min-height: 100vh;
}
.page-header {
background: url('@/assets/banner/banner.png') center/cover no-repeat, linear-gradient(135deg, #e53e3e 0%, #ff6b6b 100%);
padding: 50px 16px 25px;
}
.page-title {
font-size: 24px;
}
.page-subtitle {
font-size: 14px;
}
.card {
margin: 12px;
padding: 16px;
}
.btn {
padding: 12px 20px;
font-size: 14px;
}
}
/* 平滑滚动 */
html {
scroll-behavior: smooth;
}
/* 选择高亮颜色 */
::selection {
background: rgba(229, 62, 62, 0.2);
color: #e53e3e;
}
</style>

View File

@@ -0,0 +1,663 @@
import axios from 'axios'
// 创建axios实例
const api = axios.create({
baseURL: 'http://localhost:8123/api',
// baseURL: 'https://www.jingcaishuju.com/api',
timeout: 300000, // 5分钟超时时间300秒
withCredentials: true, // 关键支持跨域携带cookie和session
headers: {
'Content-Type': 'application/json'
}
})
// 响应拦截器
api.interceptors.response.use(
response => {
const data = response.data
// 检查是否是session过期的响应
if (data && data.success === false) {
// 可以根据后端返回的错误码或消息判断是否是session过期
const message = data.message || ''
if (message.includes('未登录') || message.includes('登录过期') || message.includes('无权限') || message.includes('Invalid session')) {
console.log('检测到session过期清除本地登录状态')
// 检查当前路径是否为后台管理路径
if (window.location.pathname.startsWith('/admin') && window.location.pathname !== '/admin/login') {
console.log('后台管理会话过期,正在注销...')
// 动态导入userStore避免循环依赖
import('../store/user.js').then(({ userStore }) => {
userStore.adminLogout()
window.location.href = '/admin/login'
})
} else {
// 前台用户会话过期处理
import('../store/user.js').then(({ userStore }) => {
userStore.logout()
})
}
}
}
return data
},
error => {
console.error('API请求错误:', error)
// 检查HTTP状态码401/403通常表示未授权/session过期
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
console.log('检测到401/403错误可能是session过期或无权限')
// 检查当前路径是否为后台管理路径
if (window.location.pathname.startsWith('/admin') && window.location.pathname !== '/admin/login') {
console.log('后台管理会话过期,正在注销...')
// 动态导入userStore避免循环依赖
import('../store/user.js').then(({ userStore }) => {
userStore.adminLogout()
window.location.href = '/admin/login'
})
} else {
// 前台用户会话过期处理
import('../store/user.js').then(({ userStore }) => {
userStore.logout()
})
}
}
return Promise.reject(error)
}
)
// API接口方法
export const lotteryApi = {
// 用户登录
userLogin(userAccount, userPassword) {
return api.post('/user/login', {
userAccount,
userPassword
})
},
// 用户注册
userRegister(userAccount, userPassword, checkPassword, userName) {
return api.post('/user/register', {
userAccount,
userPassword,
checkPassword,
userName
})
},
// 用户注销
userLogout() {
return api.post('/user/logout')
},
// 获取当前登录用户信息
getLoginUser() {
return api.get('/user/get/login')
},
// 获取用户统计信息总用户数和VIP用户数
getUserCount() {
return api.get('/user/count')
},
// 获取用户推测记录(支持分页)
getPredictRecordsByUserId(userId, page = 1) {
return api.get(`/ball-analysis/predict-records/${userId}?page=${page}`)
},
// 按条件查询推测记录(支持分页和状态筛选)
queryPredictRecords(userId, predictStatus, page = 1, pageSize = 10) {
return api.post('/data-analysis/query-predict-records', {
userId: Number(userId),
predictStatus,
current: page,
pageSize
})
},
// 获取近期开奖信息
getRecentDraws(limit = 6) {
return api.get(`/ball-analysis/recent-draws?limit=${limit}`)
},
// 获取最新100条开奖信息表相性分析
getRecent100Draws() {
return api.get('/ball-analysis/recent-100-draws')
},
// 按日期范围查询开奖信息
queryDraws(startDate, endDate) {
const params = new URLSearchParams()
if (startDate) params.append('startDate', startDate)
if (endDate) params.append('endDate', endDate)
return api.get(`/ball-analysis/query-draws?${params.toString()}`)
},
// 根据期号查询开奖信息
getDrawById(drawId) {
return api.get(`/ball-analysis/draw/${drawId}`)
},
// 创建推测记录
createPredictRecord(userId, drawId, drawDate, redBalls, blueBall) {
const params = new URLSearchParams()
params.append('userId', userId)
params.append('drawId', drawId)
params.append('drawDate', drawDate)
params.append('redBalls', redBalls)
params.append('blueBall', blueBall)
return api.post('/ball-analysis/create-predict', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
},
// 首球算法
analyzeBalls(userId, level, redBalls, blueBall) {
const params = new URLSearchParams()
params.append('userId', userId)
params.append('level', level)
params.append('redBalls', redBalls)
params.append('blueBall', blueBall)
return api.post('/ball-analysis/analyze', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
},
// 跟随球号分析算法
fallowBallAnalysis(userId, level, firstThreeRedBalls, lastSixRedBalls, blueBall) {
const params = new URLSearchParams()
params.append('userId', userId)
params.append('level', level)
params.append('firstThreeRedBalls', firstThreeRedBalls)
params.append('lastSixRedBalls', lastSixRedBalls)
params.append('blueBall', blueBall)
return api.post('/ball-analysis/fallow', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
},
// 蓝球分析算法
blueBallAnalysis(userId, level, predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall) {
const params = new URLSearchParams()
params.append('userId', userId)
params.append('level', level)
params.append('predictedRedBalls', predictedRedBalls)
params.append('predictedBlueBalls', predictedBlueBalls)
params.append('lastRedBalls', lastRedBalls)
params.append('lastBlueBall', lastBlueBall)
return api.post('/ball-analysis/blue-ball', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
},
// 获取用户推测统计数据
getUserPredictStat(userId) {
return api.get(`/data-analysis/user-predict-stat/${userId}`)
},
// 获取首球命中率统计
getFirstBallHitRate() {
return api.get('/ball-analysis/first-ball-hit-rate')
},
// 获取蓝球命中率统计
getBlueBallHitRate() {
return api.get('/ball-analysis/blue-ball-hit-rate')
},
// 获取红球命中率统计
getRedBallHitRate() {
return api.get('/ball-analysis/red-ball-hit-rate')
},
// 获取奖金统计
getPrizeStatistics() {
return api.get('/ball-analysis/prize-statistics')
},
// 激活会员码
activateVipCode(userId, code) {
return api.post('/user/activate-vip', {
userId,
code
})
},
// 批量生成会员码
generateVipCodes(numCodes, vipExpireTime) {
return api.post('/vip-code/generate', {
numCodes,
vipExpireTime
})
},
// 获取可用会员码
getAvailableVipCode(vipExpireTime) {
return api.get(`/vip-code/available?vipExpireTime=${vipExpireTime}`)
},
// 上传Excel文件导入数据T1-T7 sheet
uploadExcelFile(file) {
const formData = new FormData()
formData.append('file', file)
return api.post('/excel/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
},
// 上传Excel文件导入开奖数据T10工作表
uploadLotteryDrawsFile(file) {
const formData = new FormData()
formData.append('file', file)
return api.post('/excel/upload-lottery-draws', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
},
// 上传Excel文件追加导入开奖数据T10工作表
appendLotteryDrawsFile(file) {
const formData = new FormData()
formData.append('file', file)
return api.post('/excel/append-lottery-draws', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
},
// 获取用户兑换记录
getExchangeRecordsByUserId(userId) {
return api.get(`/vip-exchange-record/user/${userId}`)
},
// 获取用户列表
getUserList(params) {
console.log('调用获取用户列表接口:', params)
const queryParams = new URLSearchParams()
// 添加查询参数
if (params?.userAccount) {
queryParams.append('userAccount', params.userAccount)
}
if (params?.userName) {
queryParams.append('userName', params.userName)
}
if (params?.phone) {
queryParams.append('phone', params.phone)
}
if (params?.userRole) {
queryParams.append('userRole', params.userRole)
}
if (params?.status !== undefined && params?.status !== '') {
queryParams.append('status', params.status)
}
if (params?.isVip !== undefined && params?.isVip !== '') {
queryParams.append('isVip', params.isVip)
}
// 分页参数
if (params?.page) {
queryParams.append('page', params.page)
}
if (params?.size) {
queryParams.append('size', params.size)
}
return api.get(`/user/list${queryParams.toString() ? '?' + queryParams.toString() : ''}`)
},
// 更新用户状态
updateUserStatus(params) {
console.log('调用更新用户状态接口:', params)
// 使用Content-Type: application/json 调用接口
return api.post('/user/update-status', params, {
headers: {
'Content-Type': 'application/json'
},
timeout: 10000 // 设置10秒超时
}).then(response => {
console.log('更新用户状态接口响应:', response)
return response
}).catch(error => {
console.error('更新用户状态接口错误:', error)
throw error
})
},
// 添加用户
addUser(userForm) {
console.log('调用添加用户接口:', userForm)
return api.post('/user/add', userForm)
},
// 更新用户
updateUser(userForm) {
console.log('调用更新用户接口:', userForm)
return api.post('/user/update', userForm)
},
// 删除用户
deleteUser(userId) {
console.log('调用删除用户接口:', userId)
return api.post('/user/delete', { id: userId }, {
headers: {
'Content-Type': 'application/json'
}
})
},
// 获取所有推测记录总数
getTotalPredictCount() {
console.log('调用getTotalPredictCount接口...')
return api.get('/data-analysis/total-predict-count').then(response => {
console.log('getTotalPredictCount接口响应:', response)
return response
}).catch(error => {
console.error('getTotalPredictCount接口错误:', error)
throw error
})
},
// 根据用户ID和操作模块获取操作历史
getOperationHistoryByUserIdAndModule(userId, operationModule) {
return api.get(`/operation-history/user/${userId}/module/${operationModule}`)
},
// 管理员登录
adminLogin(username, password) {
// 模拟API请求实际项目中应该调用真实的后端API
return new Promise((resolve) => {
setTimeout(() => {
// 模拟验证管理员账号密码
if (username === 'admin' && password === '123456') {
resolve({
success: true,
data: {
id: 1,
userName: '系统管理员',
userAccount: 'admin',
userRole: 'admin',
avatar: null,
createTime: new Date().toISOString()
},
message: '登录成功'
})
} else {
resolve({
success: false,
data: null,
message: '账号或密码错误'
})
}
}, 1000) // 模拟网络延迟
})
},
// 获取会员码统计数据
getVipCodeStats() {
return api.get('/vip-code/stats')
},
// 获取会员码统计数量
getVipCodeCount() {
console.log('调用getVipCodeCount接口...')
return api.get('/vip-code/count').then(response => {
console.log('getVipCodeCount接口响应:', response)
return response
}).catch(error => {
console.error('getVipCodeCount接口错误:', error)
throw error
})
},
// 获取会员码列表
getVipCodeList(params) {
// 构建查询参数
const queryParams = new URLSearchParams()
// 分页参数
if (params.page) queryParams.append('current', params.page)
if (params.size) queryParams.append('pageSize', params.size)
// 搜索关键词(会员码)
if (params.keyword) queryParams.append('code', params.keyword)
// 状态筛选
if (params.status !== undefined && params.status !== '') {
// 直接使用status值作为isUse参数
queryParams.append('isUse', params.status)
}
// 有效期筛选
if (params.expireTime) queryParams.append('vipExpireTime', params.expireTime)
// 时间范围筛选
if (params.startTime) queryParams.append('startTime', params.startTime)
if (params.endTime) queryParams.append('endTime', params.endTime)
// 发起请求
return api.get(`/vip-code/list/page?${queryParams.toString()}`)
},
// 删除会员码
deleteVipCode(id) {
return api.post(`/vip-code/delete/${id}`)
},
// 获取红球历史数据全部记录
getHistoryAll() {
return api.get('/ball-analysis/history-all')
},
// 获取红球最近100期数据记录
getHistory100() {
return api.get('/ball-analysis/history-100')
},
// 获取红球历史数据排行记录
getHistoryTop() {
return api.get('/ball-analysis/history-top')
},
// 获取红球100期数据排行记录
getHistoryTop100() {
return api.get('/ball-analysis/history-top-100')
},
// 获取蓝球历史数据全部记录
getBlueHistoryAll() {
return api.get('/ball-analysis/blue-history-all')
},
// 获取蓝球最近100期数据记录
getBlueHistory100() {
return api.get('/ball-analysis/blue-history-100')
},
// 获取蓝球历史数据排行记录
getBlueHistoryTop() {
return api.get('/ball-analysis/blue-history-top')
},
// 获取蓝球100期数据排行记录
getBlueHistoryTop100() {
return api.get('/ball-analysis/blue-history-top-100')
},
// 发送短信验证码
sendSmsCode(phoneNumber) {
return api.post('/sms/sendCode', null, {
params: {
phoneNumber
}
})
},
// 手机号登录
userPhoneLogin(phone, code) {
return api.post('/user/phone/login', {
phone,
code
})
},
// 手机号注册
userPhoneRegister(userAccount, userPassword, checkPassword, phone, code, userName) {
return api.post('/user/phone/register', {
userAccount,
userPassword,
checkPassword,
phone,
code,
userName
})
},
// 重置密码
resetPassword(phone, code, newPassword, confirmPassword) {
console.log('调用重置密码接口:', { phone, code, newPassword, confirmPassword })
return api.post('/user/reset-password', {
phone,
code,
newPassword,
confirmPassword
}, {
headers: {
'Content-Type': 'application/json'
}
})
},
// 红球组合分析
redBallCombinationAnalysis(masterBall, slaveBall) {
const params = new URLSearchParams()
params.append('masterBall', masterBall)
params.append('slaveBall', slaveBall)
return api.get(`/ball-analysis/red-ball-combination-analysis?${params.toString()}`)
},
// 红球与蓝球的组合性分析
redBlueCombinationAnalysis(masterBall, slaveBall) {
const params = new URLSearchParams()
params.append('masterBall', masterBall)
params.append('slaveBall', slaveBall)
return api.get(`/ball-analysis/red-blue-combination-analysis?${params.toString()}`)
},
// 蓝球与红球的组合性分析
blueRedCombinationAnalysis(masterBall, slaveBall) {
const params = new URLSearchParams()
params.append('masterBall', masterBall)
params.append('slaveBall', slaveBall)
return api.get(`/ball-analysis/blue-red-combination-analysis?${params.toString()}`)
},
// 红球与红球的接续性分析
redRedPersistenceAnalysis(masterBall, slaveBall) {
const params = new URLSearchParams()
params.append('masterBall', masterBall)
params.append('slaveBall', slaveBall)
return api.get(`/ball-analysis/red-red-persistence-analysis?${params.toString()}`)
},
// 蓝球与蓝球的接续性分析
blueBluePersistenceAnalysis(masterBall, slaveBall) {
const params = new URLSearchParams()
params.append('masterBall', masterBall)
params.append('slaveBall', slaveBall)
return api.get(`/ball-analysis/blue-blue-persistence-analysis?${params.toString()}`)
},
// 红球与蓝球的接续性分析
redBluePersistenceAnalysis(masterBall, slaveBall) {
const params = new URLSearchParams()
params.append('masterBall', masterBall)
params.append('slaveBall', slaveBall)
return api.get(`/ball-analysis/red-blue-persistence-analysis?${params.toString()}`)
},
// 蓝球与红球的接续性分析
blueRedPersistenceAnalysis(masterBall, slaveBall) {
const params = new URLSearchParams()
params.append('masterBall', masterBall)
params.append('slaveBall', slaveBall)
return api.get(`/ball-analysis/blue-red-persistence-analysis?${params.toString()}`)
},
// 根据开奖期号查询开奖球号
getDrawNumbersById(drawId) {
return api.get(`/ball-analysis/draw/${drawId}/numbers`)
},
// 获取近10期开奖期号
getRecent10DrawIds() {
return api.get('/ball-analysis/recent-10-draw-ids')
},
// 根据操作模块获取操作历史(真实接口)
getOperationHistoryByModule(operationModule) {
console.log('调用根据操作模块获取操作历史接口:', operationModule)
return api.get(`/operation-history/module/${operationModule}`)
},
// 根据操作结果获取操作历史(真实接口)
getOperationHistoryByResult(operationResult) {
console.log('调用根据操作结果获取操作历史接口:', operationResult)
return api.get(`/operation-history/result/${operationResult}`)
},
// 获取操作历史列表(统一接口)
getOperationHistoryList(params) {
console.log('调用获取操作历史列表接口:', params)
const queryParams = new URLSearchParams()
if (params?.operationModule !== undefined && params.operationModule !== '') {
queryParams.append('operationModule', params.operationModule)
}
if (params?.operationResult !== undefined && params.operationResult !== '') {
queryParams.append('operationResult', params.operationResult)
}
if (params?.keyword !== undefined && params.keyword !== '') {
queryParams.append('keyword', params.keyword)
}
return api.get(`/operation-history/list${queryParams.toString() ? '?' + queryParams.toString() : ''}`)
}
}
export default api

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

View File

@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,4 @@
<svg t="1753944963328" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6211" width="32" height="32">
<path d="M603.904 244.992c134.4 0 243.3536 108.9536 243.3536 243.3536v202.8032c0 134.4-108.9536 243.3536-243.3536 243.3536H320c-134.4 0-243.3536-108.9536-243.3536-243.3536V488.3456c0-134.4 108.9536-243.3536 243.3536-243.3536h283.904z m0 81.1008H320c-89.6 0-162.2528 72.6528-162.2528 162.2528v202.8032c0 89.6 72.6528 162.2528 162.2528 162.2528h283.904c89.6 0 162.2528-72.6528 162.2528-162.2528V488.3456c0-89.6-72.6528-162.2528-162.2528-162.2528z" fill="#1296db" p-id="6212"></path>
<path d="M340.224 508.6208c27.0336 0 40.5504 13.5168 40.5504 40.5504v81.1008c0 27.0336-13.5168 40.5504-40.5504 40.5504-27.0336 0-40.5504-13.5168-40.5504-40.5504v-81.1008c0-27.0336 13.5168-40.5504 40.5504-40.5504zM583.2192 501.3504c15.2576-11.4176 36.864-8.3456 48.2816 6.912a34.4832 34.4832 0 0 1-6.912 48.2816l-44.3904 33.28 44.3904 33.28a34.53952 34.53952 0 0 1 9.472 44.3392l-2.56 3.9424c-11.4176 15.2576-33.024 18.3296-48.2816 6.912l-59.4944-44.5952c-13.7728-10.3424-21.9136-26.5728-21.9136-43.8272s8.0896-33.4848 21.9136-43.8272l59.4944-44.5952zM883.5072 261.12l-19.7632 47.3088c-2.7648 6.656-9.2672 10.9568-16.4864 10.9568s-13.6704-4.3008-16.4864-10.9568L811.008 261.12a44.416 44.416 0 0 0-19.7632-21.9648l-34.8672-19.0976c-5.7344-3.1232-9.2672-9.1136-9.2672-15.6672s3.5328-12.544 9.2672-15.6672l34.8672-19.0464a44.89728 44.89728 0 0 0 19.7632-21.9648l19.7632-47.3088c2.7648-6.656 9.2672-10.9568 16.4864-10.9568s13.6704 4.3008 16.4864 10.9568l19.7632 47.3088a44.416 44.416 0 0 0 19.7632 21.9648l34.8672 19.0976c5.7344 3.1232 9.2672 9.1136 9.2672 15.6672s-3.584 12.544-9.2672 15.6672l-34.8672 19.0464c-8.9088 4.864-15.872 12.6464-19.7632 21.9648z" fill="#1296db" p-id="6213"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg t="1753948292582" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18768" width="32" height="32"><path d="M891.033093 98.392071 649.86931 98.392071c-56.248614 0-106.30472 27.178229-137.783303 68.977658-31.478582-41.799429-81.534688-68.977658-137.783303-68.977658L132.966907 98.392071c-38.015118 0-68.977658 30.96254-68.977658 68.977658l0 637.484294c0 38.015118 30.96254 68.977658 68.977658 68.977658l253.376785 0c33.88678 33.026709 78.782463 51.604233 125.742315 51.604233s91.855535-18.577524 125.742315-51.604233L891.033093 873.831682c38.015118 0 68.977658-30.96254 68.977658-68.977658L960.010751 167.36973C960.010751 129.354611 929.048211 98.392071 891.033093 98.392071zM891.033093 804.854023 622.863094 804.854023c-9.976818 0-19.609609 4.300353-26.146145 12.040988-21.501764 25.11406-52.464304 39.563245-84.630942 39.563245-32.166639 0-63.129179-14.449185-84.630942-39.563245-6.536536-7.740635-16.169326-12.040988-26.146145-12.040988l-268.342012 0L132.966907 167.36973l241.163783 0c56.936671 0 103.38048 46.44381 103.38048 103.38048l0 292.94003c0 19.093566 15.48127 34.402822 34.402822 34.402822 19.093566 0 34.402822-15.48127 34.402822-34.402822L546.316815 270.75021c0-56.936671 46.44381-103.38048 103.38048-103.38048L891.033093 167.36973 891.033093 804.854023z" fill="#2c2c2c" p-id="18769"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg t="1753948236353" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17518" width="32" height="32"><path d="M828.61 878.22H195.46c-42.09 0-76.34-34.25-76.34-76.34V699.84c0-25.27 14.05-48.16 36.67-59.74 37.8-19.36 63.2-69.43 63.2-124.59 0-55.01-25.31-105.04-62.99-124.48-22.75-11.74-36.88-34.81-36.88-60.21V228.31c0-42.09 34.25-76.34 76.34-76.34h633.16c42.09 0 76.33 34.24 76.33 76.33v102.78c0 25.3-14.06 48.27-36.7 59.95-37.68 19.44-62.99 69.46-62.99 124.48 0 55 25.3 105.02 62.96 124.47 22.66 11.7 36.73 34.7 36.73 60.01v101.89c-0.01 42.1-34.25 76.34-76.34 76.34zM195.12 705.25v96.63c0 0.19 0.15 0.34 0.34 0.34h633.16c0.18 0 0.33-0.15 0.33-0.33v-96.74c-60.72-33.82-99.69-107.62-99.69-189.63 0-82.02 38.97-155.81 99.69-189.63V228.3c0-0.18-0.15-0.33-0.33-0.33H195.46c-0.19 0-0.34 0.15-0.34 0.34v97.47c60.82 33.78 99.87 107.63 99.87 189.73 0 82.16-39.03 155.96-99.87 189.74z" p-id="17519" fill="#2c2c2c"></path><path d="M643.78 421.79H380.22c-20.99 0-38-17.01-38-38s17.01-38 38-38h263.57c20.99 0 38 17.01 38 38-0.01 20.99-17.02 38-38.01 38zM643.78 616.58H380.22c-20.99 0-38-17.01-38-38s17.01-38 38-38h263.57c20.99 0 38 17.01 38 38-0.01 20.99-17.02 38-38.01 38z" p-id="17520" fill="#2c2c2c"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg t="1753948313670" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19844" width="32" height="32"><path d="M513 5C232.9 5 5 232.9 5 513s227.9 508 508 508 508-227.9 508-508S793.1 5 513 5zM217.4 867.4c63.9-145 177.4-234.3 300.3-234.3 121.6 0 232.5 85.3 297.6 228.2-81.1 70.5-186.7 113.5-302.3 113.5-112.4 0-215.4-40.4-295.6-107.4zM513 560.8c-73.6 0-133.6-59.9-133.6-133.6s60-133.6 133.6-133.6 133.6 59.9 133.6 133.6-60 133.6-133.6 133.6z m337.3 266.6c-62.2-127.2-159.4-210.7-269.7-233.8 65.7-26.8 112.2-91.2 112.2-166.5 0-99.1-80.6-179.8-179.8-179.8s-179.8 80.6-179.8 179.8c0 76.6 48.3 142.1 116 167.9-109.9 25.5-207.1 111.6-267.5 239-80.6-83.1-130.5-196.3-130.5-321C51.2 258.4 258.4 51.2 513 51.2S974.8 258.4 974.8 513c0 121.5-47.5 231.9-124.5 314.4z" fill="#2c2c2c" p-id="19845"></path></svg>

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

View File

@@ -0,0 +1 @@
<svg t="1753945117321" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7804" width="32" height="32"><path d="M259.462 675.528H118.034a64.55 64.55 0 0 0-62.502 66.128V957.83a64.55 64.55 0 0 0 62.502 66.127h141.428a64.592 64.592 0 0 0 62.715-66.127V741.656a64.592 64.592 0 0 0-62.715-66.128z m0 304.401H118.034a19.198 19.198 0 0 1-19.199-22.12v-213.53a21.332 21.332 0 0 1 19.199-24.488h141.428a22.12 22.12 0 0 1 20.691 24.488v213.55a20.052 20.052 0 0 1-20.691 22.121z m678.343-668.595H796.377a65.061 65.061 0 0 0-62.501 67.301V956.72a65.04 65.04 0 0 0 62.501 67.28h141.428a65.19 65.19 0 0 0 62.502-67.28V378.635a65.061 65.061 0 0 0-62.502-67.3z m2.133 665.78H796.377a16.425 16.425 0 0 1-17.705-20.33V385.205a19.198 19.198 0 0 1 17.705-23.827h141.428a19.54 19.54 0 0 1 17.919 23.827v571.58c0 14.227-3.627 20.328-15.786 20.328zM440.993 476.866a64.955 64.955 0 0 0-62.714 66.81v413.449a64.933 64.933 0 0 0 62.714 66.81h141.642a64.933 64.933 0 0 0 62.715-66.81V543.677a64.955 64.955 0 0 0-62.715-66.81H440.993zM582.635 977.88H440.993a17.77 17.77 0 0 1-19.198-20.755V543.677a19.05 19.05 0 0 1 19.198-22.376h141.642a19.326 19.326 0 0 1 19.625 22.376v413.449a18.046 18.046 0 0 1-19.625 20.755zM561.73 447.493L904.528 97.272l62.928 65.061L975.99 0.021h-173l63.356 67.045-333.84 334.053-176.411-181.767a41.831 41.831 0 0 0-58.662 0L36.334 488.13a29.694 29.694 0 0 0-3.627 45.842c13.44 13.908 34.13-1.131 49.703-17.215L330.922 264.81l173 179.889a39.271 39.271 0 0 0 28.584 12.052 38.14 38.14 0 0 0 29.224-9.172z" p-id="7805" fill="#2c2c2c"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg t="1753948072083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9272" width="32" height="32"><path d="M754.346667 212.906667v-81.066667s7.68-58.453333-65.706667-58.453333H126.293333s-48.213333-2.56-48.213333 48.213333v706.56s5.12 58.453333 60.586667 58.453333h144.213333s27.733333 5.12 27.733333 35.413334-27.733333 30.293333-27.733333 30.293333h-179.2S7.253333 934.4 7.253333 846.08V129.28S-2.986667 5.12 128.853333 5.12h582.4s103.68 0 106.24 96.426667 2.56 111.36 2.56 111.36-5.12 30.293333-32.853333 30.293333c-27.733333 2.56-32.853333-25.6-32.853333-30.293333z m0 0" p-id="9273" fill="#2c2c2c"></path><path d="M619.946667 506.453333v136.96s-2.56 15.36 12.8 30.293334l134.4 134.4s22.613333 10.24 40.533333-7.68c20.053333-20.053333-2.56-45.653333-2.56-45.653334l-111.36-116.48s-7.68-5.12-7.68-27.733333v-106.24s-10.24-27.733333-32.853333-27.733333c-23.04 2.133333-33.28 22.186667-33.28 29.866666z m0 0" p-id="9274" fill="#2c2c2c"></path><path d="M686.08 346.88c-184.746667 0-336.64 149.333333-336.64 336.64 0 184.746667 149.333333 336.64 336.64 336.64 184.746667 0 336.64-149.333333 336.64-336.64-2.56-187.306667-151.893333-336.64-336.64-336.64z m0 608c-149.333333 0-270.933333-121.6-270.933333-270.933333 0-149.333333 121.6-270.933333 270.933333-270.933334 149.333333 0 270.933333 121.6 270.933333 270.933334 0 149.333333-121.6 270.933333-270.933333 270.933333zM290.986667 478.72H199.68c-15.36 0-27.733333-12.8-27.733333-27.733333v-15.36c0-15.36 12.8-27.733333 27.733333-27.733334h91.306667c15.36 0 27.733333 12.8 27.733333 27.733334v15.36c0 17.493333-12.8 27.733333-27.733333 27.733333z m339.2-202.666667H199.68c-15.36 0-27.733333-12.8-27.733333-27.733333v-15.36c0-15.36 12.8-27.733333 27.733333-27.733333h427.946667c15.36 0 27.733333 12.8 27.733333 27.733333v15.36c0 14.933333-12.373333 27.733333-25.173333 27.733333z m0 0" p-id="9275" fill="#2c2c2c"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1,24 @@
@import './base.css';
#app {
margin: 0 auto;
font-weight: normal;
width: 100%;
min-height: 100vh;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
/* 移除可能影响布局的媒体查询 */

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,44 @@
<script setup>
defineProps({
msg: {
type: String,
required: true,
},
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<script setup>
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

46
lottery-app/src/main.js Normal file
View File

@@ -0,0 +1,46 @@
import './assets/main.css'
import './styles/global.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 引入用户状态管理,确保在应用启动时初始化
import './store/user'
// 导入 Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 导入 Toast 通知组件
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'
// Toast 配置选项
const toastOptions = {
position: "top-right",
timeout: 3000,
closeOnClick: true,
pauseOnFocusLoss: true,
pauseOnHover: true,
draggable: true,
draggablePercent: 0.6,
showCloseButtonOnHover: false,
hideProgressBar: false,
closeButton: "button",
icon: true,
rtl: false
}
const app = createApp(App)
// 注册所有 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(Toast, toastOptions)
app.use(ElementPlus)
app.mount('#app')

View File

@@ -0,0 +1,269 @@
import { createRouter, createWebHistory } from 'vue-router'
import { ElMessage } from 'element-plus'
import LotterySelection from '../views/LotterySelection.vue'
import Home from '../views/Home.vue'
import LotteryInfo from '../views/LotteryInfo.vue'
import Profile from '../views/Profile.vue'
import Login from '../views/Login.vue'
import Register from '../views/Register.vue'
import ResetPassword from '../views/ResetPassword.vue'
import PredictRecords from '../views/PredictRecords.vue'
import VipCodeManagement from '../views/VipCodeManagement.vue'
import ExcelImportManagement from '../views/ExcelImportManagement.vue'
import ExchangeRecords from '../views/ExchangeRecords.vue'
import TrendAnalysis from '../views/TrendAnalysis.vue'
import SurfaceAnalysis from '../views/SurfaceAnalysis.vue'
import LineAnalysis from '../views/LineAnalysis.vue'
import TableAnalysis from '../views/TableAnalysis.vue'
import DataAnalysis from '../views/DataAnalysis.vue'
import HelpCenter from '../views/HelpCenter.vue'
import AboutUs from '../views/AboutUs.vue'
import UserAgreement from '../views/UserAgreement.vue'
import AIAssistant from '../views/AIAssistant.vue'
// 后台管理相关组件
import AdminLogin from '../views/admin/AdminLogin.vue'
import AdminLayout from '../views/admin/layout/AdminLayout.vue'
import AdminVipCodeManagement from '../views/admin/VipCodeManagement.vue'
import AdminExcelImportManagement from '../views/admin/ExcelImportManagement.vue'
const routes = [
// 前台用户路由
{
path: '/',
name: 'LotterySelection',
component: LotterySelection
},
{
path: '/shuangseqiu',
name: 'Shuangseqiu',
component: Home
},
{
path: '/lottery-info',
name: 'LotteryInfo',
component: LotteryInfo
},
{
path: '/profile',
name: 'Profile',
component: Profile
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/reset-password',
name: 'ResetPassword',
component: ResetPassword
},
{
path: '/register',
name: 'Register',
component: Register
},
{
path: '/predict-records',
name: 'PredictRecords',
component: PredictRecords
},
{
path: '/vip-management',
name: 'VipCodeManagement',
component: VipCodeManagement
},
{
path: '/excel-import',
name: 'ExcelImportManagement',
component: ExcelImportManagement
},
{
path: '/exchange-records',
name: 'ExchangeRecords',
component: ExchangeRecords
},
{
path: '/trend-analysis',
name: 'TrendAnalysis',
component: TrendAnalysis
},
{
path: '/surface-analysis',
name: 'SurfaceAnalysis',
component: SurfaceAnalysis
},
{
path: '/line-analysis',
name: 'LineAnalysis',
component: LineAnalysis,
meta: { requiresAuth: true }
},
{
path: '/data-analysis',
name: 'DataAnalysis',
component: DataAnalysis,
meta: { requiresAuth: true }
},
{
path: '/help-center',
name: 'HelpCenter',
component: HelpCenter,
meta: { requiresAuth: true }
},
{
path: '/about-us',
name: 'AboutUs',
component: AboutUs,
meta: { requiresAuth: true }
},
{
path: '/user-agreement',
name: 'UserAgreement',
component: UserAgreement,
meta: { requiresAuth: true }
},
{
path: '/table-analysis',
name: 'TableAnalysis',
component: TableAnalysis
},
{
path: '/ai-assistant',
name: 'AIAssistant',
component: AIAssistant
},
// 后台管理路由 - 完全隔离
{
path: '/admin/login',
name: 'AdminLogin',
component: AdminLogin,
meta: {
title: '后台登录',
requiresAuth: false,
isAdmin: true
}
},
{
path: '/admin',
component: AdminLayout,
meta: {
requiresAuth: true,
isAdmin: true
},
children: [
{
path: '',
redirect: '/admin/dashboard'
},
{
path: 'dashboard',
name: 'AdminDashboard',
component: () => import('../views/admin/Dashboard.vue'),
meta: {
title: '控制面板',
requiresAuth: true,
isAdmin: true
}
},
{
path: 'vip-code',
name: 'AdminVipCodeManagement',
component: AdminVipCodeManagement,
meta: {
title: '会员码管理',
requiresAuth: true,
isAdmin: true
}
},
{
path: 'excel-import',
name: 'AdminExcelImportManagement',
component: AdminExcelImportManagement,
meta: {
title: '数据导入',
requiresAuth: true,
isAdmin: true
}
},
{
path: 'user-list',
name: 'AdminUserList',
component: () => import('../views/admin/UserList.vue'),
meta: {
title: '用户列表',
requiresAuth: true,
isAdmin: true
}
},
{
path: 'operation-history',
name: 'AdminOperationHistory',
component: () => import('../views/admin/OperationHistory.vue'),
meta: {
title: '操作历史',
requiresAuth: true,
isAdmin: true
}
}
]
},
// 404 页面
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫 - 权限控制
router.beforeEach((to, from, next) => {
// 设置页面标题
if (to.meta.title) {
document.title = to.meta.title + ' - 彩票推测系统'
}
// 后台管理路由权限检查
if (to.meta.isAdmin) {
// 如果是登录页面,直接放行
if (to.name === 'AdminLogin') {
next()
return
}
// 从store中导入userStore
import('../store/user.js').then(({ userStore }) => {
// 检查是否已登录使用session存储
if (!userStore.isAdminLoggedIn()) {
ElMessage.error('请先登录后台管理系统')
next('/admin/login')
return
}
// 检查是否是管理员或VIP用户
const adminInfo = JSON.parse(sessionStorage.getItem('adminInfo') || '{}')
if (adminInfo.userRole === 'user') {
ElMessage.error('您没有权限访问后台管理系统')
next('/admin/login')
return
}
next()
}).catch(error => {
console.error('加载用户状态出错:', error)
next('/admin/login')
})
} else {
next()
}
})
export default router

View File

@@ -0,0 +1,215 @@
import { reactive } from 'vue'
import { lotteryApi } from '../api/index.js'
// 用户状态管理
export const userStore = reactive({
// 用户信息
user: null,
isLoggedIn: false,
// 获取登录用户信息
async fetchLoginUser() {
try {
const response = await lotteryApi.getLoginUser()
if (response.success === true) {
const userData = response.data
// 更新用户信息,保留现有的本地数据结构
this.user = {
id: userData.id,
username: userData.userName || userData.userAccount || userData.username || userData.name,
email: userData.email,
phone: userData.phone,
nickname: userData.nickname || userData.userName || userData.userAccount || userData.username || userData.name,
avatar: userData.userAvatar || userData.avatar || null,
userType: userData.userType || 'trial',
isVip: userData.isVip,
expireDate: userData.vipExpire || userData.expireDate || '2025-06-30',
registeredAt: userData.createTime || userData.registeredAt || new Date().toISOString(),
status: userData.status !== undefined ? userData.status : 0, // 添加status字段默认为0 (正常)
stats: {
predictCount: userData.stats?.predictCount || 0,
hitCount: userData.stats?.hitCount || 0,
hitRate: userData.stats?.hitRate || 0
}
}
this.isLoggedIn = true
// 保存到本地存储
localStorage.setItem('user', JSON.stringify(this.user))
localStorage.setItem('isLoggedIn', 'true')
return this.user
} else {
console.error('获取用户信息失败:', response.message)
this.logout()
return null
}
} catch (error) {
console.error('获取用户信息出错:', error)
this.logout()
return null
}
},
// 登录
login(userInfo) {
this.user = {
id: userInfo.id || Date.now(),
username: userInfo.username,
email: userInfo.email,
phone: userInfo.phone,
nickname: userInfo.nickname || userInfo.username,
avatar: userInfo.avatar || null,
userType: userInfo.userType || 'trial', // trial: 体验版, premium: 正式版
expireDate: userInfo.expireDate || '2025-06-30',
registeredAt: userInfo.registeredAt || new Date().toISOString(),
stats: {
predictCount: userInfo.stats?.predictCount || 0,
hitCount: userInfo.stats?.hitCount || 0,
hitRate: userInfo.stats?.hitRate || 0
}
}
this.isLoggedIn = true
// 保存到本地存储
localStorage.setItem('user', JSON.stringify(this.user))
localStorage.setItem('isLoggedIn', 'true')
},
// 登出
logout() {
this.user = null;
this.isLoggedIn = false;
// 清除所有本地存储的用户信息
localStorage.removeItem('user');
localStorage.removeItem('userInfo');
localStorage.removeItem('isLoggedIn');
// 清除可能存在的其他相关存储
sessionStorage.removeItem('user');
sessionStorage.removeItem('userInfo');
sessionStorage.removeItem('isLoggedIn');
},
// 更新用户信息
updateUser(updates) {
if (this.user) {
Object.assign(this.user, updates)
localStorage.setItem('user', JSON.stringify(this.user))
}
},
// 从本地存储恢复用户状态
restoreFromStorage() {
const savedUser = localStorage.getItem('user')
const savedLoginState = localStorage.getItem('isLoggedIn')
if (savedUser && savedLoginState === 'true') {
this.user = JSON.parse(savedUser)
this.isLoggedIn = true
}
},
// 更新用户统计
updateStats(stats) {
if (this.user) {
this.user.stats = { ...this.user.stats, ...stats }
localStorage.setItem('user', JSON.stringify(this.user))
}
},
// 检查用户是否为付费用户
isPremiumUser() {
return this.user?.userType === 'premium'
},
// 获取用户ID
getUserId() {
return this.user?.id
},
// 设置用户信息(用于管理员登录)
setUserInfo(userInfo) {
this.user = {
id: userInfo.id,
username: userInfo.userAccount || userInfo.userName,
nickname: userInfo.userName || userInfo.userAccount,
avatar: userInfo.avatar || null,
userRole: userInfo.userRole || 'user',
status: userInfo.status !== undefined ? userInfo.status : 0, // 添加status字段默认为0 (正常)
createTime: userInfo.createTime || new Date().toISOString()
}
this.isLoggedIn = true
// 使用sessionStorage而不是localStorage存储管理员登录状态
sessionStorage.setItem('adminInfo', JSON.stringify(this.user))
sessionStorage.setItem('adminLoggedIn', 'true')
},
// 获取用户信息
getUserInfo() {
// 如果内存中有用户信息,直接返回
if (this.user) {
return this.user;
}
// 尝试从sessionStorage中获取管理员信息
const adminInfo = sessionStorage.getItem('adminInfo');
if (adminInfo) {
try {
const parsedAdminInfo = JSON.parse(adminInfo);
// 将解析后的信息赋值给this.user确保内存中也有
this.user = parsedAdminInfo;
return parsedAdminInfo;
} catch (e) {
console.error('解析管理员信息失败:', e);
}
}
// 尝试从localStorage中获取普通用户信息
const userInfo = localStorage.getItem('userInfo');
if (userInfo) {
try {
return JSON.parse(userInfo);
} catch (e) {
console.error('解析用户信息失败:', e);
}
}
// 如果都没有,返回空对象
return {};
},
// 检查管理员是否已登录
isAdminLoggedIn() {
return sessionStorage.getItem('adminLoggedIn') === 'true'
},
// 管理员登出
adminLogout() {
this.user = null;
this.isLoggedIn = false;
// 清除所有session存储的管理员信息
sessionStorage.removeItem('adminInfo');
sessionStorage.removeItem('adminLoggedIn');
}
})
// 初始化时从本地存储恢复状态
// 检查是否有管理员登录信息,优先恢复管理员状态
const adminInfo = sessionStorage.getItem('adminInfo');
if (adminInfo) {
try {
userStore.user = JSON.parse(adminInfo);
userStore.isLoggedIn = true;
} catch (e) {
console.error('恢复管理员状态失败:', e);
// 如果管理员信息恢复失败,尝试恢复普通用户状态
userStore.restoreFromStorage();
}
} else {
// 没有管理员信息,尝试恢复普通用户状态
userStore.restoreFromStorage();
}

View File

@@ -0,0 +1,253 @@
/* 全局样式 */
* {
box-sizing: border-box;
}
/* 强制设置根元素背景 */
:root {
background: #f0f2f5 !important;
}
html {
background: #f0f2f5 !important;
min-height: 100vh;
width: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0 !important;
padding: 0 !important;
background: #f0f2f5 !important;
min-height: 100vh;
width: 100%;
}
/* 后台管理系统样式 */
.admin-layout,
.admin-login {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
overflow: hidden !important;
z-index: 9999 !important;
}
/* 后台管理系统覆盖全局背景 */
body.admin-body {
background: #f0f2f5 !important;
overflow: hidden;
}
/* 页面头部样式 */
.page-header {
text-align: center;
padding: 60px 20px 30px;
background: linear-gradient(135deg, #e53e3e 0%, #ff6b6b 100%);
color: white;
margin-bottom: 0;
position: relative;
overflow: hidden;
}
.page-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="pattern" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse"><circle cx="20" cy="20" r="1.5" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23pattern)"/></svg>');
pointer-events: none;
}
.page-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
position: relative;
z-index: 1;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.page-subtitle {
font-size: 16px;
opacity: 0.9;
position: relative;
z-index: 1;
}
/* 通用按钮样式 */
.btn {
display: inline-block;
padding: 12px 24px;
border-radius: 8px;
text-decoration: none;
font-weight: bold;
text-align: center;
transition: all 0.3s;
border: none;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(229, 62, 62, 0.3);
}
.btn-secondary {
background: #f8f9fa;
color: #666;
border: 1px solid #e0e0e0;
}
.btn-secondary:hover:not(:disabled) {
background: #e9ecef;
border-color: #ccc;
}
.btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none !important;
}
/* 通用模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 12px;
max-width: 400px;
width: 90%;
text-align: center;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
}
.modal-content h3 {
margin-bottom: 15px;
color: #333;
font-size: 18px;
}
.modal-content p {
margin-bottom: 20px;
color: #666;
line-height: 1.5;
font-size: 14px;
}
/* 容器样式 */
.container {
width: 100%;
position: relative;
}
/* 主页特殊容器 */
.home-container {
background: #f0f2f5;
position: relative;
display: flex;
flex-direction: column;
flex: 1;
}
/* 各页面容器 */
.lottery-container,
.profile-container,
.login-page-container,
.register-page-container {
background: #f0f2f5;
position: relative;
padding: 0;
flex: 1;
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-header {
padding: 20px 15px 15px;
}
.page-title {
font-size: 20px;
}
.page-subtitle {
font-size: 13px;
}
.modal-content {
padding: 25px 20px;
}
.lottery-container,
.profile-container,
.login-page-container,
.register-page-container {
padding: 0 15px;
}
}
/* 加载动画 */
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 错误提示样式 */
.error-message {
background: #ffebee;
color: #c62828;
padding: 12px;
border-radius: 6px;
font-size: 14px;
margin-bottom: 15px;
border-left: 4px solid #e53e3e;
}
/* 成功提示样式 */
.success-message {
background: #e8f5e8;
color: #2e7d32;
padding: 12px;
border-radius: 6px;
font-size: 14px;
margin-bottom: 15px;
border-left: 4px solid #4caf50;
}

View File

@@ -0,0 +1,636 @@
<template>
<div class="ai-assistant-container">
<!-- 顶部导航栏 -->
<div class="top-nav">
<div class="back-btn" @click="$router.push('/profile')">
<span class="back-icon"></span>
<span class="back-text">返回</span>
</div>
<div class="session-info">
<div class="info-item">
<span class="info-label">会话ID:</span>
<span class="info-value">{{ conversationId }}</span>
</div>
</div>
</div>
<!-- 聊天主体区域 -->
<div class="chat-container">
<div class="chat-messages" ref="chatMessages">
<!-- 聊天消息列表 -->
<div v-if="messages.length === 0" class="empty-chat">
<div class="welcome-message">
<h3>欢迎使用AI智能助手</h3>
<p>您可以向我咨询彩票相关的任何问题我将尽力为您解答</p>
</div>
</div>
<div v-for="(msg, index) in messages" :key="index" class="message-wrapper" :class="{ 'user-message': msg.type === 'USER', 'ai-message': msg.type === 'AI' }">
<div class="message-avatar">
<div v-if="msg.type === 'USER'" class="user-avatar">用户</div>
<div v-else class="ai-avatar">AI</div>
</div>
<div class="message-content">
<div class="message-bubble" :class="{ 'user-bubble': msg.type === 'USER', 'ai-bubble': msg.type === 'AI' }">
<div class="message-text" v-html="formatMessage(msg.content)"></div>
<div v-if="msg.type === 'USER'" class="message-time">{{ msg.time }}</div>
<div v-else class="message-time">{{ msg.time }}</div>
</div>
</div>
</div>
<!-- 正在输入指示器 -->
<div v-if="isAITyping" class="message-wrapper ai-message">
<div class="message-avatar">
<div class="ai-avatar">AI</div>
</div>
<div class="message-content">
<div class="message-bubble ai-bubble">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="chat-input-container">
<div class="input-wrapper">
<textarea
v-model="userInput"
class="chat-input"
placeholder="请输入您的问题..."
@keydown.enter.prevent="sendMessage"
:disabled="isAITyping"
ref="chatInput"
></textarea>
<button
class="send-btn"
@click="sendMessage"
:disabled="!userInput.trim() || isAITyping"
>
发送
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { userStore } from '../store/user'
export default {
name: 'AIAssistant',
data() {
return {
userInput: '',
messages: [],
conversationId: '',
isAITyping: false,
eventSource: null
}
},
mounted() {
// 生成会话ID
this.generateConversationId()
// 添加欢迎消息
this.addAIMessage('您好!我是彩票智能助手,请问有什么可以帮助您的?')
// 自动聚焦输入框
this.$nextTick(() => {
this.$refs.chatInput.focus()
})
},
beforeUnmount() {
// 组件销毁前关闭SSE连接
this.closeEventSource()
},
methods: {
// 生成10位随机会话ID
generateConversationId() {
const min = 1000000000 // 10位数的最小值
const max = 9999999999 // 10位数的最大值
this.conversationId = Math.floor(Math.random() * (max - min + 1) + min).toString()
},
// 发送消息
sendMessage() {
if (!this.userInput.trim() || this.isAITyping) return
// 获取用户ID
const userId = userStore.user?.id
if (!userId) {
this.$router.push('/login')
return
}
// 添加用户消息到列表
this.addUserMessage(this.userInput)
// 保存用户输入并清空输入框
const message = this.userInput
this.userInput = ''
// 显示AI正在输入状态
this.isAITyping = true
// 滚动到底部
this.scrollToBottom()
// 关闭之前的SSE连接
this.closeEventSource()
// 创建新的SSE连接
const url = `http://47.117.22.239:8123/api/chat/sse?message=${encodeURIComponent(message)}&conversationId=${this.conversationId}&userId=${userId}`
this.eventSource = new EventSource(url)
let aiResponse = ''
this.eventSource.onmessage = (event) => {
// 将收到的文本添加到响应中
aiResponse += event.data
// 更新最后一条AI消息或添加新消息
if (this.messages.length > 0 && this.messages[this.messages.length - 1].type === 'AI') {
this.messages[this.messages.length - 1].content = aiResponse
} else {
this.addAIMessage(aiResponse)
}
// 滚动到底部
this.scrollToBottom()
}
this.eventSource.onerror = (error) => {
console.error('SSE错误:', error)
this.isAITyping = false
this.closeEventSource()
// 如果没有收到任何响应,添加错误消息
if (!aiResponse) {
this.addAIMessage('抱歉,服务器连接出现问题,请稍后再试。')
}
}
this.eventSource.onopen = () => {
console.log('SSE连接已打开')
}
// 添加完成事件处理
this.eventSource.addEventListener('complete', () => {
console.log('流式响应完成')
this.isAITyping = false
this.closeEventSource()
})
},
// 关闭SSE连接
closeEventSource() {
if (this.eventSource) {
this.eventSource.close()
this.eventSource = null
}
this.isAITyping = false
},
// 添加用户消息
addUserMessage(content) {
this.messages.push({
type: 'USER',
content: content,
time: this.getCurrentTime()
})
},
// 添加AI消息
addAIMessage(content) {
this.messages.push({
type: 'AI',
content: content,
time: this.getCurrentTime()
})
},
// 获取当前时间
getCurrentTime() {
const now = new Date()
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
},
// 滚动到底部
scrollToBottom() {
this.$nextTick(() => {
if (this.$refs.chatMessages) {
this.$refs.chatMessages.scrollTop = this.$refs.chatMessages.scrollHeight
}
})
},
// 格式化消息内容(处理换行符等)
formatMessage(text) {
if (!text) return ''
return text.replace(/\n/g, '<br>')
}
}
}
</script>
<style scoped>
/* 整体容器样式 */
.ai-assistant-container {
min-height: 100vh;
height: 100vh;
width: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
background: #f8f9fa;
overflow: hidden;
}
/* 顶部导航栏 */
.top-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: #f44336;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.back-btn {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.back-btn:hover {
opacity: 0.8;
}
.back-icon {
font-size: 16px;
font-weight: bold;
}
.back-text {
font-size: 14px;
font-weight: 500;
}
.session-info {
font-size: 12px;
}
.info-item {
display: flex;
align-items: center;
gap: 5px;
}
.info-label {
font-weight: 600;
}
.info-value {
font-family: monospace;
}
/* 聊天区域样式 */
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
background: white;
width: 100%;
height: calc(100vh - 50px); /* 减去顶部导航栏高度 */
position: relative;
overflow: hidden;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
height: calc(100vh - 130px); /* 减去顶部导航栏和输入区域的高度 */
}
/* 空聊天时的欢迎消息 */
.empty-chat {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 20px;
}
.welcome-message {
text-align: center;
padding: 30px;
background: #f0f7ff;
border-radius: 12px;
max-width: 80%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.welcome-message h3 {
color: #4a6fff;
margin-bottom: 15px;
font-size: 22px;
}
.welcome-message p {
color: #666;
line-height: 1.6;
font-size: 16px;
}
/* 消息样式 */
.message-wrapper {
display: flex;
margin-bottom: 15px;
animation: fadeIn 0.3s ease;
}
.user-message {
justify-content: flex-end;
}
.ai-message {
justify-content: flex-start;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
margin: 0 10px;
}
.user-avatar {
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.ai-avatar {
background: linear-gradient(135deg, #4a6fff, #6c4aff);
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.message-content {
max-width: 70%;
}
.message-bubble {
padding: 12px 16px;
border-radius: 18px;
position: relative;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.user-bubble {
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
color: white;
border-top-right-radius: 4px;
}
.ai-bubble {
background: #f0f7ff;
color: #333;
border-top-left-radius: 4px;
}
.message-text {
line-height: 1.5;
word-break: break-word;
font-size: 15px;
}
.message-time {
font-size: 11px;
opacity: 0.7;
text-align: right;
margin-top: 5px;
}
/* 输入区域样式 */
.chat-input-container {
padding: 15px 20px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
width: 100%;
box-sizing: border-box;
}
.input-wrapper {
display: flex;
gap: 10px;
position: relative;
}
.chat-input {
flex: 1;
border: 1px solid #ced4da;
border-radius: 24px;
padding: 12px 20px;
font-size: 15px;
resize: none;
height: 50px;
max-height: 100px;
overflow-y: auto;
background: white;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.chat-input:focus {
outline: none;
border-color: #4a6fff;
box-shadow: 0 0 0 3px rgba(74, 111, 255, 0.1);
}
.chat-input:disabled {
background: #e9ecef;
cursor: not-allowed;
}
.send-btn {
background: linear-gradient(135deg, #4a6fff, #6c4aff);
color: white;
border: none;
border-radius: 24px;
padding: 0 20px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
min-width: 80px;
}
.send-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(74, 111, 255, 0.3);
}
.send-btn:disabled {
background: #cbd3ff;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 正在输入指示器 */
.typing-indicator {
display: flex;
align-items: center;
gap: 4px;
padding: 5px 0;
}
.typing-indicator span {
display: block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #6c4aff;
opacity: 0.7;
animation: typing 1s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) {
animation-delay: 0s;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
100% {
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式样式 */
@media (max-width: 768px) {
.message-content {
max-width: 80%;
}
}
@media (max-width: 480px) {
.chat-messages {
padding: 15px 10px;
}
.message-avatar {
width: 35px;
height: 35px;
font-size: 12px;
margin: 0 5px;
}
.message-content {
max-width: 90%;
}
.message-bubble {
padding: 10px 12px;
}
.message-text {
font-size: 14px;
}
.welcome-message {
padding: 20px;
}
.welcome-message h3 {
font-size: 18px;
}
.welcome-message p {
font-size: 14px;
}
.chat-input {
padding: 10px 15px;
font-size: 14px;
height: 45px;
}
.send-btn {
min-width: 70px;
padding: 0 15px;
height: 45px;
font-size: 13px;
}
.top-nav {
padding: 8px 10px;
}
.back-text {
font-size: 12px;
}
.session-info {
font-size: 11px;
}
}
</style>

View File

@@ -0,0 +1,257 @@
<template>
<div class="about-us-page">
<el-page-header @back="goBack" content="关于我们">
<template #title>
<span>返回</span>
</template>
</el-page-header>
<el-card class="about-content-card">
<div class="company-info">
<h4>彩票猪手</h4>
<div class="intro-content">
<p>彩票猪手的宗旨是脚踏实地做一个对用户有价值的专属彩票数据助理</p>
<p>彩票猪手的靓点是其独有的彩票数据活跃性组合性接续性分析和开奖辅助推测简称为"三性一推"</p>
<p>彩票猪手摒弃现行的彩票数据"和值""区比""奇偶"等观察统计方法开创数据姿态逻辑研究新领域</p>
<p>彩票猪手以个人用户为目标努力探索"数据戏彩"的新途径开启"数据变现"的新典范</p>
<p>彩票猪手契合AI智能体犹如镶嵌了时代的印记目的和意义不仅仅是渲染和炫耀</p>
<p>彩票猪手只专注于对号球"交叉现隐"规律的研究有关具体投注的方法与技巧不在其列</p>
<p>彩票猪手的三项基本服务可以归纳为开奖信息查询开奖数据分析开奖号码推测</p>
<p>彩票猪手可以同时满足用户自行研判选号和程序辅助推号的需求</p>
</div>
</div>
<div class="contact-section">
<h4>联系我们</h4>
<div class="contact-item">
<div class="contact-icon">
<el-icon :size="20"><Phone /></el-icon>
</div>
<div class="contact-info">
<span class="contact-label">客服热线</span>
<span class="contact-value">400-888-9999</span>
</div>
</div>
<div class="contact-item">
<div class="contact-icon">
<el-icon :size="20"><Message /></el-icon>
</div>
<div class="contact-info">
<span class="contact-label">邮箱地址</span>
<span class="contact-value">service@lottery-ai.com</span>
</div>
</div>
<div class="contact-item">
<div class="contact-icon">
<el-icon :size="20"><ChatDotRound /></el-icon>
</div>
<div class="contact-info">
<span class="contact-label">在线客服</span>
<span class="contact-value">右上角悬浮弹窗</span>
</div>
</div>
<div class="contact-item">
<div class="contact-icon">
<el-icon :size="20"><Clock /></el-icon>
</div>
<div class="contact-info">
<span class="contact-label">服务时间</span>
<span class="contact-value">周一至周日 9:00-21:00</span>
</div>
</div>
</div>
<div class="version-info">
<div class="version-row">
<span>版本号v1.0.0</span>
<div class="user-agreement-link" @click="$router.push('/user-agreement')">
用户协议
</div>
</div>
<p>© 2025 彩票智能推测系统. 保留所有权利.</p>
</div>
</el-card>
</div>
</template>
<script>
import { ElPageHeader, ElCard, ElIcon } from 'element-plus'
import { Phone, Message, ChatDotRound, Clock } from '@element-plus/icons-vue'
export default {
name: 'AboutUs',
components: {
ElPageHeader,
ElCard,
ElIcon,
Phone,
Message,
ChatDotRound,
Clock
},
methods: {
goBack() {
this.$router.go(-1)
}
}
}
</script>
<style scoped>
.about-us-page {
padding: 20px;
background-color: #f0f2f5;
}
/* 自定义返回按钮样式 */
:deep(.el-page-header__header) {
background: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
:deep(.el-page-header__back) {
color: #409EFF !important;
font-weight: 600;
font-size: 16px;
}
:deep(.el-page-header__back:hover) {
color: #66b1ff !important;
}
:deep(.el-page-header__content) {
color: #333 !important;
font-weight: 600;
font-size: 18px;
}
.about-content-card {
margin-top: 20px;
border-radius: 8px;
overflow: hidden;
}
.company-info {
margin-bottom: 25px;
text-align: center;
}
.company-info h4 {
color: #e53e3e;
font-size: 18px;
font-weight: 600;
margin-bottom: 10px;
}
.intro-content {
text-align: left;
margin-top: 15px;
}
.intro-content p {
color: #666;
line-height: 1.8;
margin: 0 0 12px 0;
font-size: 14px;
text-align: justify;
}
.intro-content p:last-child {
margin-bottom: 0;
}
.contact-section h4 {
color: #333;
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.contact-item {
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
transition: all 0.3s ease;
}
.contact-item:hover {
background: #e9ecef;
transform: translateY(-1px);
}
.contact-icon {
font-size: 20px;
margin-right: 12px;
width: 32px;
height: 32px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
color: #409EFF;
}
.contact-info {
flex: 1;
display: flex;
flex-direction: column;
}
.contact-label {
font-size: 12px;
color: #999;
margin-bottom: 2px;
font-weight: 500;
}
.contact-value {
font-size: 14px;
color: #333;
font-weight: 600;
}
.version-info {
text-align: center;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
}
.version-row {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 8px;
gap: 20px;
}
.version-row span {
font-size: 12px;
color: #999;
line-height: 1.4;
}
.version-info p {
font-size: 12px;
color: #999;
margin: 4px 0;
line-height: 1.4;
}
.user-agreement-link {
color: #409EFF;
font-size: 12px;
cursor: pointer;
text-decoration: underline;
transition: color 0.3s ease;
}
.user-agreement-link:hover {
color: #66b1ff;
}
</style>

View File

@@ -0,0 +1,546 @@
<template>
<div class="data-analysis-container">
<!-- 页面头部 -->
<el-page-header @back="goBack" class="page-header">
<template #title>
返回
</template>
<template #content>
<div class="header-content">
<el-icon :size="24" style="margin-right: 8px;"><DataAnalysisIcon /></el-icon>
<span class="header-title">数据分析中心</span>
</div>
</template>
</el-page-header>
<div class="subtitle-container">
<p class="subtitle">专业的彩票数据分析工具</p>
</div>
<!-- 彩票种类选择区域 -->
<el-card class="lottery-types" shadow="never">
<h2 class="section-title">选择彩票种类</h2>
<div class="lottery-options">
<!-- 中国福彩 -->
<div class="lottery-category">
<h3 class="category-title">中国福彩</h3>
<div class="lottery-category-options">
<!-- 双色球 -->
<div
class="lottery-option"
:class="{ active: currentLotteryType === 'ssq' }"
@click="switchLotteryType('ssq')"
>
<div class="lottery-option-image">
<img src="@/assets/shuangseqiu.png" alt="双色球" />
</div>
<div class="lottery-option-name">双色球</div>
</div>
<!-- 福彩3D -->
<div
class="lottery-option"
@click="switchLotteryType('3d')"
>
<div class="lottery-option-image">
<img src="@/assets/3D.png" alt="福彩3D" />
</div>
<div class="lottery-option-name">福彩3D</div>
</div>
<!-- 七乐彩 -->
<div
class="lottery-option"
@click="switchLotteryType('qlc')"
>
<div class="lottery-option-image">
<img src="@/assets/7lecai.png" alt="七乐彩" />
</div>
<div class="lottery-option-name">七乐彩</div>
</div>
<!-- 快乐8 -->
<div
class="lottery-option"
@click="switchLotteryType('kl8')"
>
<div class="lottery-option-image">
<img src="@/assets/kuaile8.png" alt="快乐8" />
</div>
<div class="lottery-option-name">快乐8</div>
</div>
</div>
</div>
<!-- 中国体彩 -->
<div class="lottery-category">
<h3 class="category-title">中国体彩</h3>
<div class="lottery-category-options">
<!-- 大乐透 -->
<div
class="lottery-option"
@click="switchLotteryType('dlt')"
>
<div class="lottery-option-image">
<img src="@/assets/daletou.png" alt="大乐透" />
</div>
<div class="lottery-option-name">大乐透</div>
</div>
<!-- 排列3 -->
<div
class="lottery-option"
@click="switchLotteryType('pl3')"
>
<div class="lottery-option-image">
<img src="@/assets/pailie3.png" alt="排列3" />
</div>
<div class="lottery-option-name">排列3</div>
</div>
<!-- 排列5 -->
<div
class="lottery-option"
@click="switchLotteryType('pl5')"
>
<div class="lottery-option-image">
<img src="@/assets/pailie5.png" alt="排列5" />
</div>
<div class="lottery-option-name">排列5</div>
</div>
<!-- 七星彩 -->
<div
class="lottery-option"
@click="switchLotteryType('qxc')"
>
<div class="lottery-option-image">
<img src="@/assets/7星彩.png" alt="七星彩" />
</div>
<div class="lottery-option-name">七星彩</div>
</div>
</div>
</div>
</div>
</el-card>
<!-- 分析类型选择 - 只有在选择彩票种类后才显示 -->
<el-card v-if="currentLotteryType" class="analysis-section" shadow="never">
<h2 class="section-title">选择分析类型</h2>
<div class="analysis-options">
<!-- 表相性分析 -->
<div class="analysis-option" @click="goToAnalysis('table')">
<div class="analysis-icon">
<el-icon :size="32"><Grid /></el-icon>
</div>
<div class="analysis-name">表相性分析</div>
<div class="analysis-desc">提供基于最新100期开奖数据的详细表格分析</div>
</div>
<!-- 组合性分析 -->
<div class="analysis-option" @click="goToAnalysis('surface')">
<div class="analysis-icon">
<el-icon :size="32"><Box /></el-icon>
</div>
<div class="analysis-name">组合性分析</div>
<div class="analysis-desc">分析号码之间的组合关系和规律</div>
</div>
<!-- 活跃性分析 -->
<div class="analysis-option" @click="goToAnalysis('trend')">
<div class="analysis-icon">
<el-icon :size="32"><TrendCharts /></el-icon>
</div>
<div class="analysis-name">活跃性分析</div>
<div class="analysis-desc">分析号码的活跃度和热度趋势</div>
</div>
<!-- 接续性分析 -->
<div class="analysis-option" @click="goToAnalysis('line')">
<div class="analysis-icon">
<el-icon :size="32"><Connection /></el-icon>
</div>
<div class="analysis-name">接续性分析</div>
<div class="analysis-desc">分析号码的连续性和接续规律</div>
</div>
</div>
</el-card>
<!-- 开发中提示 -->
<div v-if="showTip" class="tip-overlay" @click="hideTip">
<div class="tip-content" @click.stop>
<div class="tip-icon">🚧</div>
<h3>功能开发中</h3>
<p>该彩票种类的分析功能正在开发中目前仅支持双色球分析请稍后再试或选择双色球进行分析</p>
<button class="tip-button" @click="hideTip">我知道了</button>
</div>
</div>
</div>
</template>
<script>
import { ElCard, ElIcon, ElPageHeader } from 'element-plus'
import { Grid, Box, TrendCharts, Connection, DataAnalysis as DataAnalysisIcon } from '@element-plus/icons-vue'
export default {
name: 'DataAnalysis',
components: {
ElCard,
ElIcon,
ElPageHeader,
Grid,
Box,
TrendCharts,
Connection,
DataAnalysis: DataAnalysisIcon
},
data() {
return {
currentLotteryType: '', // 默认为空,不选择任何彩票
showTip: false, // 提示框显示状态
}
},
methods: {
// 返回上一页
goBack() {
try {
// 检查是否有历史记录可以返回
if (window.history.length > 1) {
this.$router.go(-1);
} else {
// 如果没有历史记录,返回首页
this.$router.push('/');
}
} catch (error) {
console.error('返回操作失败:', error);
// 如果出现错误,返回首页
this.$router.push('/');
}
},
// 切换彩票类型
switchLotteryType(type) {
// 只有双色球功能可用,其他彩票类型显示开发中提示
if (type !== 'ssq') {
this.showDevelopingTip();
return;
}
this.currentLotteryType = type;
},
// 跳转到具体分析页面
goToAnalysis(analysisType) {
const routes = {
'table': '/table-analysis',
'surface': '/surface-analysis',
'trend': '/trend-analysis',
'line': '/line-analysis'
};
if (routes[analysisType]) {
this.$router.push({
path: routes[analysisType],
query: { lotteryType: this.currentLotteryType }
});
}
},
// 显示开发中提示
showDevelopingTip() {
this.showTip = true;
},
// 隐藏提示
hideTip() {
this.showTip = false;
}
}
}
</script>
<style scoped>
.data-analysis-container {
padding: 20px 20px 0px 20px;
background-color: #f0f2f5;
min-height: calc(100vh - 70px);
}
.toolbar {
margin-bottom: 15px;
}
.page-header {
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
padding: 30px 24px;
border-radius: 8px;
margin-bottom: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
:deep(.el-page-header__back .el-icon),
:deep(.el-page-header__back .el-page-header__title) {
color: white;
}
.header-content {
display: flex;
align-items: center;
color: white;
}
.header-title {
font-size: 20px;
font-weight: 600;
}
.subtitle-container {
text-align: center;
margin-bottom: 20px;
}
.subtitle {
font-size: 18px;
opacity: 0.9;
color: #333;
text-align: center;
width: 100%;
margin: 0 auto;
font-weight: 500;
}
.lottery-types, .analysis-section {
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
}
/* 彩票类型选择区域 */
.section-title {
text-align: center;
color: #333;
font-size: 20px;
margin-bottom: 25px;
font-weight: 600;
}
.lottery-options {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
}
.lottery-category {
margin-bottom: 25px;
}
.category-title {
text-align: left;
color: #333;
font-size: 18px;
margin-bottom: 15px;
font-weight: 600;
padding-bottom: 5px;
border-bottom: 1px solid #f0f0f0;
}
.lottery-category-options {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: flex-start;
}
.lottery-option {
width: 100px;
padding: 15px 10px;
border-radius: 12px;
text-align: center;
background: white;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
cursor: pointer;
}
.lottery-option.active {
border-color: #4caf50;
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.2);
transform: translateY(-2px);
background-color: #f1f8e9;
}
.lottery-option:hover:not(.active) {
transform: translateY(-3px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
border-color: #e0e0e0;
}
.lottery-option-image {
width: 50px;
height: 50px;
margin: 0 auto 10px;
display: flex;
align-items: center;
justify-content: center;
}
.lottery-option-image img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.lottery-option-name {
font-size: 14px;
font-weight: 600;
color: #333;
}
/* 分析选项样式 */
.analysis-options {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-top: 20px;
}
.analysis-option {
background: white;
border-radius: 12px;
padding: 25px 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
cursor: pointer;
text-align: center;
border: 1px solid #f0f0f0;
}
.analysis-option:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.analysis-icon {
margin-bottom: 15px;
color: #3a7bd5;
}
.analysis-name {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.analysis-desc {
font-size: 14px;
color: #666;
line-height: 1.5;
}
/* 开发中提示框 */
.tip-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.tip-content {
background: white;
padding: 30px;
border-radius: 16px;
text-align: center;
max-width: 320px;
width: 90%;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
}
.tip-icon {
font-size: 40px;
margin-bottom: 15px;
}
.tip-content h3 {
font-size: 20px;
margin-bottom: 10px;
}
.tip-content p {
color: #666;
margin-bottom: 20px;
}
.tip-button {
background: #3a7bd5;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
.tip-button:hover {
background: #2c5aa0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.data-analysis-container {
padding: 10px 10px 0px 10px;
}
.header-title {
font-size: 18px;
}
.subtitle {
font-size: 16px;
}
.lottery-category-options {
justify-content: center;
gap: 10px;
}
.lottery-option {
width: calc(25% - 10px);
min-width: 80px;
padding: 10px 5px;
}
.lottery-option-image {
width: 40px;
height: 40px;
}
.lottery-option-name {
font-size: 12px;
}
.category-title {
text-align: center;
font-size: 16px;
}
.analysis-options {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.lottery-option {
width: calc(33% - 10px);
}
}
</style>

View File

@@ -0,0 +1,699 @@
<template>
<div class="excel-import-management">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<h1>📊 Excel数据导入系统</h1>
<p>管理员专用 - Excel文件数据批量导入</p>
</div>
</div>
<!-- 权限检查中 -->
<div v-if="permissionChecking" class="permission-checking">
<div class="checking-content">
<div class="loading-spinner"></div>
<p>正在验证权限...</p>
</div>
</div>
<!-- 主要内容 - 只有有权限时才显示 -->
<div v-else-if="hasPermission" class="import-container">
<!-- 完整数据导入 -->
<div class="import-card">
<div class="card-header">
<div>
<h2>📋 完整数据导入</h2>
<p>上传包含T1-T7工作表的Excel文件导入红球蓝球接续系数和组合系数数据</p>
</div>
</div>
<div class="upload-section">
<div class="file-input-container">
<input
type="file"
ref="fullDataFileInput"
@change="handleFileSelect($event, 'fullData')"
accept=".xlsx,.xls"
class="file-input"
id="fullDataFile"
/>
<label for="fullDataFile" class="file-label">
<span class="file-icon">📁</span>
<span class="file-text">
{{ fullDataFile ? fullDataFile.name : '选择Excel文件包含T1-T7工作表' }}
</span>
</label>
</div>
<button
@click="uploadFullData"
:disabled="!fullDataFile || fullDataUploading"
class="btn btn-primary"
>
{{ fullDataUploading ? '导入中...' : '开始导入' }}
</button>
<div v-if="fullDataResult" class="result-message" :class="fullDataResult.type">
{{ fullDataResult.message }}
</div>
</div>
</div>
<!-- 开奖数据导入覆盖 -->
<div class="import-card">
<div class="card-header">
<div>
<h2>🎯 开奖数据导入覆盖</h2>
<p>上传包含T10工作表的Excel文件清空并重新导入开奖数据</p>
</div>
</div>
<div class="upload-section">
<div class="file-input-container">
<input
type="file"
ref="lotteryFileInput"
@change="handleFileSelect($event, 'lottery')"
accept=".xlsx,.xls"
class="file-input"
id="lotteryFile"
/>
<label for="lotteryFile" class="file-label">
<span class="file-icon">📁</span>
<span class="file-text">
{{ lotteryFile ? lotteryFile.name : '选择Excel文件包含T10工作表' }}
</span>
</label>
</div>
<button
@click="uploadLotteryData"
:disabled="!lotteryFile || lotteryUploading"
class="btn btn-warning"
>
{{ lotteryUploading ? '导入中...' : '覆盖导入' }}
</button>
<div v-if="lotteryResult" class="result-message" :class="lotteryResult.type">
{{ lotteryResult.message }}
</div>
</div>
</div>
<!-- 开奖数据追加 -->
<div class="import-card">
<div class="card-header">
<div>
<h2> 开奖数据追加</h2>
<p>上传包含T10工作表的Excel文件追加导入开奖数据跳过重复期号</p>
</div>
</div>
<div class="upload-section">
<div class="file-input-container">
<input
type="file"
ref="appendFileInput"
@change="handleFileSelect($event, 'append')"
accept=".xlsx,.xls"
class="file-input"
id="appendFile"
/>
<label for="appendFile" class="file-label">
<span class="file-icon">📁</span>
<span class="file-text">
{{ appendFile ? appendFile.name : '选择Excel文件包含T10工作表' }}
</span>
</label>
</div>
<button
@click="appendLotteryData"
:disabled="!appendFile || appendUploading"
class="btn btn-success"
>
{{ appendUploading ? '追加中...' : '追加导入' }}
</button>
<div v-if="appendResult" class="result-message" :class="appendResult.type">
{{ appendResult.message }}
</div>
</div>
</div>
<!-- 导入说明 -->
<div class="info-card">
<div class="card-header">
<h2> 导入说明</h2>
</div>
<div class="info-content">
<div class="info-item">
<h4>📋 完整数据导入</h4>
<p> 需要包含T1T2T3T4T5T6T7工作表的Excel文件</p>
<p> 导入红球蓝球接续系数和组合系数数据到相应的数据库表</p>
<p> 适用于系统初始化或全量数据更新</p>
</div>
<div class="info-item">
<h4>🎯 开奖数据导入覆盖</h4>
<p> 需要包含T10工作表的Excel文件</p>
<p> 清空lottery_draws表的现有数据重新导入</p>
<p> 适用于完全替换开奖数据</p>
</div>
<div class="info-item">
<h4> 开奖数据追加</h4>
<p> 需要包含T10工作表的Excel文件</p>
<p> 保留现有数据只添加新的开奖记录</p>
<p> 自动跳过重复的期号适用于增量更新</p>
</div>
</div>
</div>
</div>
<!-- 错误提示弹窗 -->
<div v-if="showErrorModal" class="modal-overlay" @click="hideErrorModal">
<div class="modal-content error-modal" @click.stop>
<h3> 导入失败</h3>
<p>{{ errorMessage }}</p>
<button class="btn btn-primary" @click="hideErrorModal">确定</button>
</div>
</div>
</div>
</template>
<script>
import { lotteryApi } from '../api/index.js'
import { userStore } from '../store/user.js'
export default {
name: 'ExcelImportManagement',
data() {
return {
// 权限验证
hasPermission: false,
permissionChecking: true,
// 文件对象
fullDataFile: null,
lotteryFile: null,
appendFile: null,
// 上传状态
fullDataUploading: false,
lotteryUploading: false,
appendUploading: false,
// 结果信息
fullDataResult: null,
lotteryResult: null,
appendResult: null,
// 错误处理
showErrorModal: false,
errorMessage: ''
}
},
async mounted() {
await this.checkPermission()
},
methods: {
// 检查用户权限
async checkPermission() {
try {
const response = await lotteryApi.getLoginUser()
if (response && response.success && response.data) {
const userRole = response.data.userRole
if (userRole === 'admin' || userRole === 'superAdmin') {
this.hasPermission = true
} else {
this.showError('无权限访问此页面,仅限管理员或超级管理员使用')
// 3秒后跳转到首页
setTimeout(() => {
this.$router.push('/')
}, 3000)
}
} else {
this.showError('获取用户信息失败,请重新登录')
setTimeout(() => {
this.$router.push('/login')
}, 3000)
}
} catch (error) {
console.error('权限检查失败:', error)
this.showError('权限验证失败,请重新登录')
setTimeout(() => {
this.$router.push('/login')
}, 3000)
} finally {
this.permissionChecking = false
}
},
// 文件选择处理
handleFileSelect(event, type) {
const file = event.target.files[0]
if (!file) return
// 验证文件类型
if (!this.validateFileType(file)) {
this.showError('请选择.xlsx或.xls格式的Excel文件')
event.target.value = ''
return
}
// 验证文件大小限制50MB
if (file.size > 50 * 1024 * 1024) {
this.showError('文件大小不能超过50MB')
event.target.value = ''
return
}
switch (type) {
case 'fullData':
this.fullDataFile = file
this.fullDataResult = null
break
case 'lottery':
this.lotteryFile = file
this.lotteryResult = null
break
case 'append':
this.appendFile = file
this.appendResult = null
break
}
},
// 验证文件类型
validateFileType(file) {
const allowedTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.ms-excel' // .xls
]
return allowedTypes.includes(file.type) ||
file.name.endsWith('.xlsx') ||
file.name.endsWith('.xls')
},
// 上传完整数据
async uploadFullData() {
if (!this.fullDataFile) return
this.fullDataUploading = true
try {
const response = await lotteryApi.uploadExcelFile(this.fullDataFile)
this.fullDataResult = {
type: 'success',
message: '✅ ' + (response || '完整数据导入成功!')
}
// 清空文件选择
this.fullDataFile = null
this.$refs.fullDataFileInput.value = ''
} catch (error) {
console.error('完整数据导入失败:', error)
this.fullDataResult = {
type: 'error',
message: '❌ ' + (error?.response?.data || error?.message || '导入失败,请重试')
}
} finally {
this.fullDataUploading = false
}
},
// 上传开奖数据(覆盖)
async uploadLotteryData() {
if (!this.lotteryFile) return
this.lotteryUploading = true
try {
const response = await lotteryApi.uploadLotteryDrawsFile(this.lotteryFile)
this.lotteryResult = {
type: 'success',
message: '✅ ' + (response || '开奖数据导入成功!')
}
// 清空文件选择
this.lotteryFile = null
this.$refs.lotteryFileInput.value = ''
} catch (error) {
console.error('开奖数据导入失败:', error)
this.lotteryResult = {
type: 'error',
message: '❌ ' + (error?.response?.data || error?.message || '导入失败,请重试')
}
} finally {
this.lotteryUploading = false
}
},
// 追加开奖数据
async appendLotteryData() {
if (!this.appendFile) return
this.appendUploading = true
try {
const response = await lotteryApi.appendLotteryDrawsFile(this.appendFile)
this.appendResult = {
type: 'success',
message: '✅ ' + (response || '开奖数据追加成功!')
}
// 清空文件选择
this.appendFile = null
this.$refs.appendFileInput.value = ''
} catch (error) {
console.error('开奖数据追加失败:', error)
this.appendResult = {
type: 'error',
message: '❌ ' + (error?.response?.data || error?.message || '追加失败,请重试')
}
} finally {
this.appendUploading = false
}
},
// 显示错误信息
showError(message) {
this.errorMessage = message
this.showErrorModal = true
},
// 隐藏错误弹窗
hideErrorModal() {
this.showErrorModal = false
this.errorMessage = ''
},
}
}
</script>
<style scoped>
.excel-import-management {
min-height: 100vh;
background: #f5f5f5;
padding: 20px;
}
/* 页面头部 */
.page-header {
text-align: center;
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
padding: 30px;
border-radius: 8px;
color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
/* 权限检查样式 */
.permission-checking {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.checking-content {
text-align: center;
color: white;
}
.checking-content p {
margin-top: 20px;
font-size: 16px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.header-content h1 {
font-size: 32px;
margin-bottom: 10px;
font-weight: 600;
}
.header-content p {
font-size: 16px;
opacity: 0.9;
}
/* 主容器 */
.import-container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 25px;
margin-bottom: 30px;
}
/* 导入卡片 */
.import-card,
.info-card {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.import-card:hover {
transform: translateY(-5px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 25px;
}
.card-header h2 {
color: #333;
margin: 0 0 5px 0;
font-size: 20px;
}
.card-header p {
color: #666;
margin: 0;
font-size: 14px;
}
/* 上传区域 */
.upload-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.file-input-container {
position: relative;
}
.file-input {
display: none;
}
.file-label {
display: flex;
align-items: center;
padding: 15px 20px;
border: 2px dashed #ddd;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
background: #f8f9fa;
}
.file-label:hover {
border-color: #74b9ff;
background: #e3f2fd;
}
.file-icon {
font-size: 24px;
margin-right: 12px;
}
.file-text {
color: #555;
font-size: 14px;
}
/* 按钮样式 */
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
font-size: 16px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: linear-gradient(135deg, #74b9ff, #0984e3);
color: white;
}
.btn-warning {
background: linear-gradient(135deg, #fdcb6e, #e17055);
color: white;
}
.btn-success {
background: linear-gradient(135deg, #00b894, #00cec9);
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-small {
padding: 8px 16px;
font-size: 12px;
}
.btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* 结果消息 */
.result-message {
padding: 15px;
border-radius: 8px;
font-weight: 600;
font-size: 14px;
}
.result-message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.result-message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* 信息说明 */
.info-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.info-item h4 {
color: #333;
margin-bottom: 8px;
font-size: 16px;
}
.info-item p {
color: #666;
margin: 2px 0;
font-size: 14px;
line-height: 1.5;
}
/* 模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 12px;
max-width: 400px;
width: 90%;
text-align: center;
}
.error-modal h3 {
color: #dc3545;
margin-bottom: 15px;
}
.error-modal p {
margin-bottom: 20px;
color: #666;
line-height: 1.5;
}
/* 响应式设计 */
@media (max-width: 768px) {
.import-container {
grid-template-columns: 1fr;
gap: 20px;
}
.import-card,
.info-card {
padding: 20px;
}
.page-header h1 {
font-size: 24px;
}
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
}
</style>

View File

@@ -0,0 +1,341 @@
<template>
<div class="exchange-records-page">
<!-- 页面头部 -->
<el-page-header @back="goBack" class="page-header">
<template #title>
返回
</template>
<template #content>
<div class="header-content">
<el-icon :size="24" style="margin-right: 8px;"><ShoppingBag /></el-icon>
<span class="header-title">兑换记录</span>
</div>
</template>
</el-page-header>
<!-- 主要内容 -->
<div class="records-container" v-loading="loading" element-loading-text="正在加载兑换记录...">
<!-- 错误提示 -->
<el-result
v-if="errorMessage"
status="error"
title="加载失败"
:sub-title="errorMessage"
>
<template #extra>
<el-button type="primary" @click="loadExchangeRecords" :loading="loading">重新加载</el-button>
</template>
</el-result>
<!-- 空状态 -->
<el-empty v-else-if="records.length === 0 && !loading" description="暂无兑换记录">
<el-button type="primary" @click="goToProfile">去兑换会员码</el-button>
</el-empty>
<!-- 兑换记录列表 -->
<el-card v-else class="records-list-card" shadow="never">
<template #header>
<div class="card-header">
<span>兑换记录 ({{ records.length }})</span>
<el-button :icon="Refresh" circle @click="loadExchangeRecords" :loading="loading" />
</div>
</template>
<div class="records-grid">
<el-card
v-for="record in paginatedRecords"
:key="record.id"
class="record-card"
shadow="hover"
>
<template #header>
<div class="record-card-header">
<el-tag :type="getStatusClass(record.isUse)" effect="dark" size="large">
{{ getStatusText(record.isUse) }}
</el-tag>
<span class="record-date">{{ formatDate(record.exchangeTime) }}</span>
</div>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label-class-name="record-label" label="订单号">{{ record.orderNo || '-' }}</el-descriptions-item>
<el-descriptions-item label-class-name="record-label" label="兑换类型">{{ getExchangeTypeText(record.type) }}</el-descriptions-item>
<el-descriptions-item label-class-name="record-label" label="兑换模式">{{ getExchangeModeText(record.exchangeMode) }}</el-descriptions-item>
<el-descriptions-item v-if="record.orderAmount" label-class-name="record-label" label="订单金额">
<span class="amount-value">¥{{ record.orderAmount }}</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<!-- 分页组件 -->
<div v-if="totalPages > 1" class="pagination-container">
<el-pagination
background
layout="prev, pager, next"
:total="records.length"
:page-size="pageSize"
v-model:current-page="currentPage"
@current-change="goToPage"
/>
</div>
</el-card>
</div>
</div>
</template>
<script>
import { lotteryApi } from '../api/index.js'
import { userStore } from '../store/user.js'
import {
ElPageHeader,
ElCard,
ElButton,
ElIcon,
ElResult,
ElEmpty,
ElTag,
ElDescriptions,
ElDescriptionsItem,
ElPagination
} from 'element-plus'
import { ShoppingBag, Refresh } from '@element-plus/icons-vue'
export default {
name: 'ExchangeRecords',
components: {
ElPageHeader,
ElCard,
ElButton,
ElIcon,
ElResult,
ElEmpty,
ElTag,
ElDescriptions,
ElDescriptionsItem,
ElPagination,
ShoppingBag,
Refresh
},
data() {
return {
loading: false,
errorMessage: '',
records: [],
// 分页相关
currentPage: 1,
pageSize: 5
}
},
computed: {
// 总页数
totalPages() {
return Math.ceil(this.records.length / this.pageSize)
},
// 当前页的记录
paginatedRecords() {
const start = (this.currentPage - 1) * this.pageSize
const end = start + this.pageSize
return this.records.slice(start, end)
}
},
async mounted() {
await this.loadExchangeRecords()
},
methods: {
// 加载兑换记录
async loadExchangeRecords() {
const userId = userStore.getUserId()
if (!userId) {
this.errorMessage = '请先登录'
return
}
this.loading = true
this.errorMessage = ''
try {
const response = await lotteryApi.getExchangeRecordsByUserId(userId)
if (response && response.success) {
this.records = response.data || []
// 按兑换时间倒序排列
this.records.sort((a, b) => new Date(b.exchangeTime) - new Date(a.exchangeTime))
} else {
this.errorMessage = response?.message || '获取兑换记录失败'
}
} catch (error) {
console.error('获取兑换记录失败:', error)
this.errorMessage = error?.response?.data?.message || '网络连接失败,请稍后重试'
} finally {
this.loading = false
}
},
// 获取状态样式类
getStatusClass(isUse) {
switch (isUse) {
case 1:
return 'success'
default:
return 'info'
}
},
// 获取状态文本
getStatusText(isUse) {
switch (isUse) {
case 1:
return '成功'
default:
return '未知'
}
},
// 获取兑换类型文本
getExchangeTypeText(type) {
switch (type) {
case '月度会员':
return '月度会员'
case '年度会员':
return '年度会员'
default:
return type || '未知类型'
}
},
// 获取兑换模式文本
getExchangeModeText(mode) {
switch (mode) {
case 1:
return '兑换码兑换'
case 2:
return '在线支付'
default:
return mode || '未知模式'
}
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return '-'
const date = new Date(dateStr)
const formatted = date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
return formatted.replace(/\//g, '-')
},
// 跳转到个人中心
goToProfile() {
this.$router.push('/profile')
},
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 跳转到指定页
goToPage(page) {
if (page >= 1 && page <= this.totalPages) {
this.currentPage = page
}
}
}
}
</script>
<style scoped>
.exchange-records-page {
background-color: #f0f2f5;
padding: 20px;
min-height: calc(100vh - 70px);
}
.page-header {
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
padding: 30px 24px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
:deep(.el-page-header__back .el-icon),
:deep(.el-page-header__back .el-page-header__title) {
color: white;
}
:deep(.el-divider) {
border-color: rgba(255, 255, 255, 0.5);
}
.header-content {
display: flex;
align-items: center;
color: white;
}
.header-title {
font-size: 20px;
font-weight: 600;
}
.records-container {
max-width: 1200px;
margin: 0 auto;
}
.records-list-card {
border-radius: 8px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.records-grid {
display: grid;
gap: 16px;
}
.record-card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.record-date {
font-size: 14px;
color: #909399;
}
.amount-value {
color: #F56C6C;
font-weight: 600;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 24px;
}
:deep(.record-label) {
width: 100px;
font-weight: 600;
color: #606266;
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<div class="help-center-page">
<el-page-header @back="goBack" content="帮助中心">
<template #title>
<span>返回</span>
</template>
</el-page-header>
<el-card class="help-content-card">
<div class="faq-list">
<div class="faq-item">
<div class="faq-question">
<span class="question-marker">Q1</span>
<span class="question-text">如何开通会员</span>
</div>
<div class="faq-answer">
进入'我的'点击右上角悬浮的微信客服添加微信客服后发送付款截图客服会发送给您一个兑换码拿到兑换码后页面右上角有一个兑换会员的按钮填写后即完成兑换完成兑换后会员立即生效
</div>
</div>
<div class="faq-item">
<div class="faq-question">
<span class="question-marker">Q2</span>
<span class="question-text">目前有两种会员可供选择</span>
</div>
<div class="faq-answer">
月度会员和年度会员月度会员10元年度会员100元
</div>
</div>
<div class="faq-item">
<div class="faq-question">
<span class="question-marker">Q3</span>
<span class="question-text">如何推测号码查询活跃性图等</span>
</div>
<div class="faq-answer">
进入"我的"页后下滑到底部有个数据分析模块包含了表相性分析组合性分析活跃性分析接续性分析等仅对会员开放
</div>
</div>
</div>
<div class="disclaimer">
<div class="disclaimer-title">免责声明</div>
<p>彩票中奖号码是随机产生的没有任何方法可以保证100%中奖购买彩票应该是基于娱乐目的而并非一种可行的投资策略建议您理性对待彩票不要过度投入</p>
</div>
</el-card>
</div>
</template>
<script>
import { ElPageHeader, ElCard } from 'element-plus'
export default {
name: 'HelpCenter',
components: {
ElPageHeader,
ElCard
},
methods: {
goBack() {
this.$router.go(-1)
}
}
}
</script>
<style scoped>
.help-center-page {
padding: 20px;
background-color: #f0f2f5;
}
/* 自定义返回按钮样式 */
:deep(.el-page-header__header) {
background: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
:deep(.el-page-header__back) {
color: #409EFF !important;
font-weight: 600;
font-size: 16px;
}
:deep(.el-page-header__back:hover) {
color: #66b1ff !important;
}
:deep(.el-page-header__content) {
color: #333 !important;
font-weight: 600;
font-size: 18px;
}
.help-content-card {
margin-top: 20px;
border-radius: 8px;
overflow: hidden;
}
.faq-list {
margin-bottom: 20px;
}
.faq-item {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
}
.faq-item:last-child {
border-bottom: none;
}
.faq-question {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.question-marker {
background: #409EFF;
color: white;
font-weight: bold;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
margin-right: 10px;
}
.question-text {
font-size: 16px;
font-weight: 600;
color: #333;
}
.faq-answer {
padding-left: 40px;
color: #555;
line-height: 1.6;
font-size: 15px;
}
.disclaimer {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
border-radius: 4px;
margin-top: 20px;
}
.disclaimer-title {
font-weight: bold;
color: #856404;
margin-bottom: 8px;
}
.disclaimer p {
color: #856404;
margin: 0;
line-height: 1.6;
font-size: 14px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,755 @@
<template>
<div class="line-analysis">
<div class="header">
<!-- 返回按钮 -->
<div class="back-button-container">
<el-button @click="$router.back()" icon="ArrowLeft" size="medium">
返回
</el-button>
</div>
<h2>接续性分析</h2>
<p>球号接续性分析计算接续系数值</p>
</div>
<!-- 分析类型选择 -->
<div class="analysis-buttons">
<el-card
class="analysis-card"
:class="{ active: analysisType === 'red-red' }"
@click="selectAnalysisType('red-red')"
shadow="hover"
>
<div class="btn-icon">
<el-avatar class="red-ball-icon"></el-avatar>
<el-avatar class="red-ball-icon"></el-avatar>
</div>
<div class="btn-text">
<div class="btn-title">红球与红球</div>
<div class="btn-desc">红球接续性分析</div>
</div>
</el-card>
<el-card
class="analysis-card"
:class="{ active: analysisType === 'blue-blue' }"
@click="selectAnalysisType('blue-blue')"
shadow="hover"
>
<div class="btn-icon">
<el-avatar class="blue-ball-icon"></el-avatar>
<el-avatar class="blue-ball-icon"></el-avatar>
</div>
<div class="btn-text">
<div class="btn-title">蓝球与蓝球</div>
<div class="btn-desc">蓝球接续性分析</div>
</div>
</el-card>
<el-card
class="analysis-card"
:class="{ active: analysisType === 'red-blue' }"
@click="selectAnalysisType('red-blue')"
shadow="hover"
>
<div class="btn-icon">
<el-avatar class="red-ball-icon"></el-avatar>
<el-avatar class="blue-ball-icon"></el-avatar>
</div>
<div class="btn-text">
<div class="btn-title">红球与蓝球</div>
<div class="btn-desc">红蓝接续性分析</div>
</div>
</el-card>
<el-card
class="analysis-card"
:class="{ active: analysisType === 'blue-red' }"
@click="selectAnalysisType('blue-red')"
shadow="hover"
>
<div class="btn-icon">
<el-avatar class="blue-ball-icon"></el-avatar>
<el-avatar class="red-ball-icon"></el-avatar>
</div>
<div class="btn-text">
<div class="btn-title">蓝球与红球</div>
<div class="btn-desc">蓝红接续性分析</div>
</div>
</el-card>
</div>
<!-- 号码选择和分析区域 -->
<el-card v-if="analysisType" class="analysis-container" shadow="never">
<div class="number-selection">
<h3>{{ getSelectionTitle() }}</h3>
<!-- 主球选择 -->
<div class="ball-selection-group">
<el-divider>{{ getMasterBallLabel() }}</el-divider>
<div class="ball-grid">
<el-button
v-for="num in getMasterBallRange()"
:key="'master-' + num"
:class="{
active: masterBall === num,
'red-ball': isMasterRed(),
'blue-ball': !isMasterRed()
}"
:type="masterBall === num ? 'primary' : 'default'"
:plain="masterBall !== num"
circle
@click="selectMasterBall(num)"
>
{{ num }}
</el-button>
</div>
</div>
<!-- 随球选择 -->
<div class="ball-selection-group">
<el-divider>{{ getSlaveBallLabel() }}</el-divider>
<div class="ball-grid">
<el-button
v-for="num in getSlaveBallRange()"
:key="'slave-' + num"
:class="{
active: slaveBall === num,
'red-ball': isSlaveRed(),
'blue-ball': !isSlaveRed()
}"
:type="slaveBall === num ? 'primary' : 'default'"
:plain="slaveBall !== num"
circle
@click="selectSlaveBall(num)"
>
{{ num }}
</el-button>
</div>
</div>
<!-- 分析按钮 -->
<div class="analyze-section">
<el-button
type="success"
size="large"
round
:disabled="!canAnalyze || loading"
:loading="loading"
@click="performAnalysis"
>
{{ loading ? '分析中...' : '开始分析' }}
</el-button>
</div>
</div>
<!-- 分析结果 -->
<div v-if="result !== null || error" class="result-container">
<el-alert
v-if="error"
:title="error"
type="error"
show-icon
:closable="false"
style="margin-bottom: 20px;"
>
<template #default>
<el-button @click="clearError" type="primary" size="small">重试</el-button>
</template>
</el-alert>
<el-result
v-else-if="result !== null"
icon="success"
title="接续性分析结果"
>
<template #extra>
<div class="result-details">
<div class="table-wrapper">
<div class="table-title-row">
<div class="table-title-cell">
<span class="highlight-ball">{{masterBall}}</span>{{isMasterRed() ? '红' : '蓝'}}球与<span class="highlight-ball">{{slaveBall}}</span>{{isSlaveRed() ? '红' : '蓝'}}球接续性分析报告
</div>
</div>
<table class="analysis-table">
<tbody>
<tr>
<td class="label-cell">引用数据截至</td>
<td class="value-cell">{{resultData[0]?.latestDrawId || '-'}}</td>
</tr>
<tr>
<td class="label-cell">两号接续性系数</td>
<td class="value-cell">{{resultData[0]?.lineCoefficient || '-'}}</td>
</tr>
<tr>
<td class="label-cell">同号组最高系数球号及系数</td>
<td class="value-cell">
<span class="highlight-ball">{{resultData[0]?.highestBall || '--'}}</span>
<span class="coefficient-value">{{resultData[0]?.highestCoefficient || '-'}}</span>
</td>
</tr>
<tr>
<td class="label-cell">同号组最低系数球号及系数</td>
<td class="value-cell">
<span class="highlight-ball">{{resultData[0]?.lowestBall || '--'}}</span>
<span class="coefficient-value">{{resultData[0]?.lowestCoefficient || '-'}}</span>
</td>
</tr>
<tr>
<td class="label-cell">同号组平均系数</td>
<td class="value-cell">{{resultData[0]?.averageCoefficient || '-'}}</td>
</tr>
<tr>
<td class="label-cell">建议</td>
<td class="value-cell recommendation">
系数越高表示彼此接续越频繁可进行多球接续系数比对一般观察高于平均系数的接续关系更值得关注
</td>
</tr>
</tbody>
</table>
</div>
<el-button type="primary" @click="resetAnalysis">重新分析</el-button>
</div>
</template>
</el-result>
</div>
</el-card>
<!-- 使用说明 -->
<el-card v-if="!analysisType" class="instruction-container" shadow="hover">
<template #header>
<div class="card-header">
<span><el-icon><InfoFilled /></el-icon> 接续性分析说明</span>
</div>
</template>
<el-collapse accordion>
<el-collapse-item title="红球与红球分析" name="red-red">
<div class="collapse-content">
<p>分析两个红球号码之间的接续性关系计算红球配对的接续系数</p>
</div>
</el-collapse-item>
<el-collapse-item title="蓝球与蓝球分析" name="blue-blue">
<div class="collapse-content">
<p>分析两个蓝球号码之间的接续性关系计算蓝球配对的接续系数</p>
</div>
</el-collapse-item>
<el-collapse-item title="红球与蓝球分析" name="red-blue">
<div class="collapse-content">
<p>分析红球与蓝球号码的接续性关系计算红蓝配对的接续系数</p>
</div>
</el-collapse-item>
<el-collapse-item title="蓝球与红球分析" name="blue-red">
<div class="collapse-content">
<p>分析蓝球与红球号码的接续性关系计算蓝红配对的接续系数</p>
</div>
</el-collapse-item>
</el-collapse>
<el-alert
type="info"
show-icon
:closable="false"
style="margin-top: 20px;"
>
<template #title>
<span>选择分析类型后依次选择主球和随球号码点击"开始分析"获取接续系数值</span>
</template>
</el-alert>
</el-card>
</div>
</template>
<script>
import { lotteryApi } from '@/api'
import {
ElButton,
ElCard,
ElAvatar,
ElDivider,
ElAlert,
ElResult,
ElIcon,
ElCollapse,
ElCollapseItem
} from 'element-plus'
import { ArrowLeft, InfoFilled } from '@element-plus/icons-vue'
export default {
name: 'LineAnalysis',
components: {
ElButton,
ElCard,
ElAvatar,
ElDivider,
ElAlert,
ElResult,
ElIcon,
ElCollapse,
ElCollapseItem,
InfoFilled
},
data() {
return {
analysisType: '', // 'red-red', 'blue-blue', 'red-blue', 'blue-red'
masterBall: null,
slaveBall: null,
loading: false,
result: null,
error: null
}
},
computed: {
canAnalyze() {
return this.masterBall !== null && this.slaveBall !== null && this.analysisType
},
resultData() {
if (!this.result) return [];
try {
const data = typeof this.result === 'string' ? JSON.parse(this.result) : this.result;
return [{
latestDrawId: data.latestDrawId || '',
lineCoefficient: data.lineCoefficient || 0,
highestCoefficient: data.highestCoefficient || 0,
lowestCoefficient: data.lowestCoefficient || 0,
averageCoefficient: data.averageCoefficient || 0,
highestBall: data.highestBall || '-',
lowestBall: data.lowestBall || '-'
}];
} catch (e) {
console.error('解析结果数据失败', e);
return [{
latestDrawId: '',
lineCoefficient: 0,
highestCoefficient: 0,
lowestCoefficient: 0,
averageCoefficient: 0,
highestBall: '-',
lowestBall: '-'
}];
}
}
},
methods: {
selectAnalysisType(type) {
this.analysisType = type
this.masterBall = null
this.slaveBall = null
this.result = null
this.error = null
},
getSelectionTitle() {
const titles = {
'red-red': '红球与红球接续性分析',
'blue-blue': '蓝球与蓝球接续性分析',
'red-blue': '红球与蓝球接续性分析',
'blue-red': '蓝球与红球接续性分析'
}
return titles[this.analysisType] || ''
},
getMasterBallLabel() {
const labels = {
'red-red': '主球(红球)',
'blue-blue': '主球(蓝球)',
'red-blue': '主球(红球)',
'blue-red': '主球(蓝球)'
}
return labels[this.analysisType] || ''
},
getSlaveBallLabel() {
const labels = {
'red-red': '随球(红球)',
'blue-blue': '随球(蓝球)',
'red-blue': '随球(蓝球)',
'blue-red': '随球(红球)'
}
return labels[this.analysisType] || ''
},
getMasterBallRange() {
if (this.analysisType === 'blue-blue' || this.analysisType === 'blue-red') {
return Array.from({ length: 16 }, (_, i) => i + 1) // 1-16
}
return Array.from({ length: 33 }, (_, i) => i + 1) // 1-33
},
getSlaveBallRange() {
if (this.analysisType === 'blue-blue' || this.analysisType === 'red-blue') {
return Array.from({ length: 16 }, (_, i) => i + 1) // 1-16
}
return Array.from({ length: 33 }, (_, i) => i + 1) // 1-33
},
isMasterRed() {
return this.analysisType === 'red-red' || this.analysisType === 'red-blue'
},
isSlaveRed() {
return this.analysisType === 'red-red' || this.analysisType === 'blue-red'
},
selectMasterBall(num) {
this.masterBall = num
this.result = null
this.error = null
},
selectSlaveBall(num) {
this.slaveBall = num
this.result = null
this.error = null
},
async performAnalysis() {
if (!this.canAnalyze) return
this.loading = true
this.error = null
this.result = null
try {
let response
switch (this.analysisType) {
case 'red-red':
response = await lotteryApi.redRedPersistenceAnalysis(this.masterBall, this.slaveBall)
break
case 'blue-blue':
response = await lotteryApi.blueBluePersistenceAnalysis(this.masterBall, this.slaveBall)
break
case 'red-blue':
response = await lotteryApi.redBluePersistenceAnalysis(this.masterBall, this.slaveBall)
break
case 'blue-red':
response = await lotteryApi.blueRedPersistenceAnalysis(this.masterBall, this.slaveBall)
break
}
if (response.success) {
this.result = response.data
} else {
this.error = response.message || '分析失败'
}
} catch (error) {
console.error('接续系数分析失败:', error)
this.error = '网络请求失败,请重试'
} finally {
this.loading = false
}
},
clearError() {
this.error = null
},
resetAnalysis() {
this.masterBall = null
this.slaveBall = null
this.result = null
this.error = null
}
}
}
</script>
<style scoped>
.line-analysis {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
position: relative;
}
.back-button-container {
position: absolute;
left: 20px;
top: 0;
}
.header h2 {
color: #2c3e50;
font-size: 28px;
margin-bottom: 10px;
font-weight: 700;
}
.header p {
color: #7f8c8d;
font-size: 16px;
}
.analysis-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
.analysis-card {
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
}
.analysis-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.15);
border-color: #3498db;
}
.analysis-card.active {
background-color: #eaf5ff;
border-color: #3498db;
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.2);
}
.analysis-card.active :deep(.el-card__body) {
background: transparent;
color: #303133;
}
.btn-icon {
display: flex;
gap: 5px;
margin-bottom: 10px;
}
.red-ball-icon {
background-color: #e74c3c !important;
}
.blue-ball-icon {
background-color: #3498db !important;
}
.btn-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.analysis-card.active .btn-title {
color: #2980b9;
}
.btn-desc {
font-size: 14px;
opacity: 0.8;
}
.analysis-card.active .btn-desc {
color: #3498db;
}
.analysis-container {
margin-bottom: 30px;
}
.number-selection {
padding: 20px 0;
}
.number-selection h3 {
color: #2c3e50;
margin-bottom: 20px;
font-size: 20px;
text-align: center;
}
.ball-selection-group {
margin-bottom: 25px;
}
.ball-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 15px;
padding: 0 15px;
}
:deep(.el-button.red-ball.is-plain:not(.is-disabled)) {
color: #e74c3c;
border-color: #e74c3c;
background-color: #fff5f5;
}
:deep(.el-button.red-ball:not(.is-plain):not(.is-disabled)) {
background-color: #e74c3c;
border-color: #c0392b;
}
:deep(.el-button.blue-ball.is-plain:not(.is-disabled)) {
color: #3498db;
border-color: #3498db;
background-color: #f0f8ff;
}
:deep(.el-button.blue-ball:not(.is-plain):not(.is-disabled)) {
background-color: #3498db;
border-color: #2980b9;
}
.analyze-section {
text-align: center;
margin-top: 30px;
}
.result-container {
border-top: 2px solid #ecf0f1;
padding: 20px;
background: #f8f9fa;
}
:deep(.el-result) {
padding: 0px;
}
.result-details {
width: 100%;
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.table-wrapper {
width: 100%;
max-width: 500px;
margin: 0 auto 20px;
border-collapse: collapse;
}
.table-title-row {
width: 100%;
text-align: center;
border: 1px solid #000;
}
.table-title-cell {
padding: 8px;
font-weight: normal;
font-size: 15px;
color: #000;
background-color: #fff;
border-bottom: 1px solid #000;
}
.analysis-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #000;
border-top: none;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
.analysis-table tr {
border-bottom: 1px solid #000;
}
.analysis-table tr:last-child {
border-bottom: none;
}
.label-cell {
width: 150px;
padding: 8px;
text-align: center;
font-weight: normal;
background-color: #f5f5f5;
border-right: 1px solid #000;
vertical-align: middle;
}
.value-cell {
padding: 8px;
text-align: center;
vertical-align: middle;
}
.highlight-ball {
color: #e74c3c;
font-weight: normal;
margin-right: 8px;
}
.coefficient-value {
font-weight: normal;
color: #333;
display: inline-block;
}
.recommendation {
font-size: 13px;
line-height: 1.5;
color: #333;
text-align: left;
padding: 5px;
white-space: normal;
font-weight: normal;
}
@media (max-width: 768px) {
.surface-analysis {
padding: 15px;
}
.back-button-container {
position: static;
text-align: left;
margin-bottom: 15px;
}
.header {
margin-bottom: 20px;
}
.analysis-buttons {
grid-template-columns: 1fr;
}
.ball-grid {
gap: 8px;
padding: 0;
}
.table-wrapper {
max-width: 100%;
}
.table-title-cell {
font-size: 14px;
padding: 6px;
}
.label-cell, .value-cell {
padding: 6px;
font-size: 13px;
}
.recommendation {
font-size: 12px;
line-height: 1.4;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,823 @@
<template>
<div class="lottery-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="page-title">
<h1>开奖信息查询</h1>
<p class="subtitle">实时开奖结果历史数据查询</p>
</div>
</div>
<!-- 彩票种类选择区域 -->
<el-card class="lottery-types" shadow="never">
<h2 class="section-title">选择彩票种类</h2>
<div class="lottery-options">
<!-- 中国福彩 -->
<div class="lottery-category">
<h3 class="category-title">中国福彩</h3>
<div class="lottery-category-options">
<!-- 双色球 -->
<div
class="lottery-option"
:class="{ active: currentLotteryType === 'ssq' }"
@click="switchLotteryType('ssq')"
>
<div class="lottery-option-image">
<img src="@/assets/shuangseqiu.png" alt="双色球" />
</div>
<div class="lottery-option-name">双色球</div>
</div>
<!-- 福彩3D -->
<div
class="lottery-option"
@click="switchLotteryType('3d')"
>
<div class="lottery-option-image">
<img src="@/assets/3D.png" alt="福彩3D" />
</div>
<div class="lottery-option-name">福彩3D</div>
</div>
<!-- 七乐彩 -->
<div
class="lottery-option"
@click="switchLotteryType('qlc')"
>
<div class="lottery-option-image">
<img src="@/assets/7lecai.png" alt="七乐彩" />
</div>
<div class="lottery-option-name">七乐彩</div>
</div>
<!-- 快乐8 -->
<div
class="lottery-option"
@click="switchLotteryType('kl8')"
>
<div class="lottery-option-image">
<img src="@/assets/kuaile8.png" alt="快乐8" />
</div>
<div class="lottery-option-name">快乐8</div>
</div>
</div>
</div>
<!-- 中国体彩 -->
<div class="lottery-category">
<h3 class="category-title">中国体彩</h3>
<div class="lottery-category-options">
<!-- 大乐透 -->
<div
class="lottery-option"
@click="switchLotteryType('dlt')"
>
<div class="lottery-option-image">
<img src="@/assets/daletou.png" alt="大乐透" />
</div>
<div class="lottery-option-name">大乐透</div>
</div>
<!-- 排列3 -->
<div
class="lottery-option"
@click="switchLotteryType('pl3')"
>
<div class="lottery-option-image">
<img src="@/assets/pailie3.png" alt="排列3" />
</div>
<div class="lottery-option-name">排列3</div>
</div>
<!-- 排列5 -->
<div
class="lottery-option"
@click="switchLotteryType('pl5')"
>
<div class="lottery-option-image">
<img src="@/assets/pailie5.png" alt="排列5" />
</div>
<div class="lottery-option-name">排列5</div>
</div>
<!-- 七星彩 -->
<div
class="lottery-option"
@click="switchLotteryType('qxc')"
>
<div class="lottery-option-image">
<img src="@/assets/7星彩.png" alt="七星彩" />
</div>
<div class="lottery-option-name">七星彩</div>
</div>
</div>
</div>
</div>
</el-card>
<!-- 搜索区域 - 只有在选择彩票种类后才显示 -->
<el-card v-if="currentLotteryType" class="search-section" shadow="never">
<el-tabs v-model="searchType" type="border-card">
<el-tab-pane label="期号查询" name="period">
<div class="search-form">
<el-input
v-model="searchPeriod"
placeholder="请输入期号例如2025056"
clearable
@keyup.enter="searchByPeriod"
style="max-width: 400px;"
>
<template #append>
<el-button
type="primary"
@click="searchByPeriod"
:loading="loading"
:disabled="!searchPeriod"
>
查询
</el-button>
</template>
</el-input>
</div>
</el-tab-pane>
<el-tab-pane label="日期查询" name="date">
<div class="search-form">
<el-date-picker
v-model="searchDateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="margin-right: 10px;"
/>
<el-button
type="primary"
@click="searchByDate"
:loading="loading"
>
查询
</el-button>
</div>
</el-tab-pane>
<el-tab-pane label="表相查询" name="table">
<div class="search-form">
<div class="table-analysis-info">
<p>表相分析提供基于最新100期开奖数据的详细表格分析</p>
<el-button
type="primary"
@click="goToTableAnalysis"
>
进入表相分析
</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
<el-alert
v-if="errorMessage"
:title="errorMessage"
type="error"
show-icon
@close="clearError"
style="margin-top: 15px;"
/>
</el-card>
<!-- 搜索结果 -->
<el-card v-if="searchResults.length > 0" class="results-section" shadow="never">
<template #header>
<div class="card-header">
<span>查询结果</span>
</div>
</template>
<div class="lottery-records">
<div v-for="item in searchResults" :key="item.drawId" class="lottery-record-item">
<div class="lottery-record-header">
<span class="lottery-period">{{ item.drawId }}</span>
<span class="lottery-date">{{ formatDate(item.drawDate) }}</span>
</div>
<div class="lottery-balls-container">
<div class="red-balls-container">
<span v-for="ball in getRedBalls(item)" :key="ball" class="ball-red">
{{ String(ball).padStart(2, '0') }}
</span>
</div>
<span class="ball-blue">{{ String(item.blueBall).padStart(2, '0') }}</span>
</div>
</div>
</div>
</el-card>
<!-- 近期开奖记录 -->
<el-card class="recent-section" shadow="never" v-loading="loadingRecent" element-loading-text="加载中...">
<template #header>
<div class="card-header">
<span>近期开奖记录</span>
</div>
</template>
<el-alert
v-if="recentError"
:title="recentError"
type="error"
show-icon
style="margin-bottom: 15px;"
/>
<div v-if="!recentError" class="lottery-records">
<div v-for="item in recentDraws" :key="item.drawId" class="lottery-record-item">
<div class="lottery-record-header">
<span class="lottery-period">{{ item.drawId }}</span>
<span class="lottery-date">{{ formatDate(item.drawDate) }}</span>
</div>
<div class="lottery-balls-container">
<div class="red-balls-container">
<span v-for="ball in getRedBalls(item)" :key="ball" class="ball-red">
{{ String(ball).padStart(2, '0') }}
</span>
</div>
<span class="ball-blue">{{ String(item.blueBall).padStart(2, '0') }}</span>
</div>
</div>
</div>
</el-card>
<!-- 开发中提示 -->
<div v-if="showTip" class="tip-overlay" @click="hideTip">
<div class="tip-content" @click.stop>
<div class="tip-icon">🚧</div>
<h3>功能开发中</h3>
<p>该彩票种类查询功能正在开发中目前仅支持双色球查询请稍后再试或选择双色球进行查询</p>
<button class="tip-button" @click="hideTip">我知道了</button>
</div>
</div>
</div>
</template>
<script>
import { lotteryApi } from '../api'
import { ElCard, ElTabs, ElTabPane, ElInput, ElButton, ElDatePicker, ElAlert, ElTable, ElTableColumn, ElLoading } from 'element-plus'
export default {
name: 'LotteryInfo',
components: {
ElCard,
ElTabs,
ElTabPane,
ElInput,
ElButton,
ElDatePicker,
ElAlert,
ElTable,
ElTableColumn
},
directives: {
loading: ElLoading.directive,
},
data() {
return {
searchType: 'period', // 'period' 或 'date'
loading: false,
loadingRecent: false,
currentLotteryType: '', // 默认为空,不选择任何彩票
showTip: false, // 提示框显示状态
// 搜索相关
searchPeriod: '',
searchDateRange: [], // [startDate, endDate]
searchResults: [],
// 近期开奖
recentDraws: [],
// 错误信息
errorMessage: '',
recentError: ''
}
},
computed: {
searchStartDate() {
return this.searchDateRange ? this.searchDateRange[0] : ''
},
searchEndDate() {
return this.searchDateRange ? this.searchDateRange[1] : ''
}
},
mounted() {
// 不再自动加载近期开奖记录,需要用户先选择彩票种类
},
methods: {
// 加载近期开奖记录
async loadRecentDraws() {
// 如果未选择彩票种类,不加载数据
if (!this.currentLotteryType) {
this.recentDraws = [];
return;
}
this.loadingRecent = true;
this.recentError = '';
try {
// 根据不同彩票类型调用不同的API
// 这里假设API支持传递彩票类型参数
const response = await lotteryApi.getRecentDraws(10, this.currentLotteryType);
if (response.success === true) {
this.recentDraws = response.data;
} else {
this.recentError = response.message || '获取近期开奖信息失败';
console.error('获取近期开奖信息失败:', response.message);
}
} catch (error) {
this.recentError = error?.response?.data?.message || error?.message || '网络连接失败,请稍后重试';
console.error('获取近期开奖信息失败:', error);
} finally {
this.loadingRecent = false;
}
},
// 按期号搜索
async searchByPeriod() {
if (!this.searchPeriod || !this.currentLotteryType) return;
this.loading = true;
this.clearError();
this.searchResults = [];
try {
// 传递彩票类型
const response = await lotteryApi.getDrawById(parseInt(this.searchPeriod), this.currentLotteryType);
if (response.success === true && response.data) {
this.searchResults = [response.data];
} else {
this.showError(response.message || `未找到期号为 ${this.searchPeriod} 的记录`);
this.searchResults = [];
}
} catch (error) {
console.error('期号查询失败:', error);
this.showError(error?.response?.data?.message || error?.message || '网络连接失败,请检查网络连接');
this.searchResults = [];
} finally {
this.loading = false;
}
},
// 按日期范围搜索
async searchByDate() {
if (!this.searchStartDate || !this.searchEndDate || !this.currentLotteryType) {
this.showError('请选择开始和结束日期');
return;
}
this.loading = true;
this.clearError();
this.searchResults = [];
try {
// 传递彩票类型
const response = await lotteryApi.queryDraws(
this.searchStartDate,
this.searchEndDate,
this.currentLotteryType
);
if (response.success === true) {
this.searchResults = response.data;
if (this.searchResults.length === 0) {
this.showError('该日期范围内未找到开奖记录');
}
} else {
this.showError(response.message || '查询失败,请重试');
this.searchResults = [];
}
} catch (error) {
console.error('日期查询失败:', error);
this.showError(error?.response?.data?.message || error?.message || '网络连接失败,请检查网络连接');
this.searchResults = [];
} finally {
this.loading = false;
}
},
// 解析红球号码字符串
parseRedBalls(redBallsStr) {
if (!redBallsStr) return []
return redBallsStr.split(',').map(ball => parseInt(ball.trim()))
},
// 获取红球号码数组适配API返回的数据结构
getRedBalls(item) {
// 如果是新的API结构redBall1-6字段
if (item.redBall1 !== undefined) {
return [
item.redBall1,
item.redBall2,
item.redBall3,
item.redBall4,
item.redBall5,
item.redBall6
].filter(ball => ball !== undefined && ball !== null)
}
// 如果是旧的API结构redBalls字符串
if (item.redBalls) {
return this.parseRedBalls(item.redBalls)
}
return []
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
},
// 显示错误信息
showError(message) {
this.errorMessage = message
},
// 清除错误信息
clearError() {
this.errorMessage = ''
},
// 添加彩票类型切换方法
switchLotteryType(type) {
// 只有双色球功能可用,其他彩票类型显示开发中提示
if (type !== 'ssq') {
this.showDevelopingTip();
return;
}
this.currentLotteryType = type;
// 清除之前的搜索结果
this.searchResults = [];
this.searchPeriod = '';
this.searchDateRange = [];
this.clearError();
// 加载新选择的彩票类型的数据
this.loadRecentDraws();
},
// 显示开发中提示
showDevelopingTip() {
this.showTip = true;
},
// 隐藏提示
hideTip() {
this.showTip = false;
},
// 跳转到表相分析页面
goToTableAnalysis() {
this.$router.push({ name: 'TableAnalysis' });
}
}
}
</script>
<style scoped>
.lottery-container {
padding: 20px 20px 0px 20px;
background-color: #f0f2f5;
}
.search-section, .results-section, .recent-section, .lottery-types {
margin-bottom: 20px;
border-radius: 8px; /* 添加圆角,与第一个卡片保持一致 */
overflow: hidden; /* 确保内容不超出圆角范围 */
}
.search-form {
display: flex;
align-items: center;
padding: 20px 10px;
}
.card-header {
font-weight: bold;
}
.red-balls-container {
display: flex;
flex-wrap: nowrap;
}
.ball-red, .ball-blue {
display: inline-block;
width: 28px;
height: 28px;
border-radius: 50%;
text-align: center;
line-height: 28px;
color: white;
font-weight: bold;
margin: 0 3px;
}
.ball-red {
background-color: #e53e3e;
}
.ball-blue {
background-color: #3b82f6;
}
.page-header {
background: url('@/assets/banner/backend1.png') center/cover no-repeat, linear-gradient(135deg, #ff6b6b, #ee5a52);
color: white;
padding: 45px 20px 25px;
text-align: center;
position: relative;
margin-bottom: 20px;
border-radius: 8px;
}
.page-title {
margin: 0;
text-align: center;
}
.page-title h1 {
font-size: 38px;
margin: 0 auto 10px;
font-weight: 700;
color: white;
text-shadow: 0 2px 6px rgba(0,0,0,0.7), 0 0 15px rgba(0,0,0,0.4);
letter-spacing: 2px;
text-align: center;
width: 100%;
}
.page-title p {
font-size: 22px;
opacity: 0.9;
color: white;
text-shadow: 0 2px 6px rgba(0,0,0,0.7), 0 0 15px rgba(0,0,0,0.4);
text-align: center;
width: 100%;
margin: 0 auto;
font-weight: 500;
}
/* 彩票类型选择区域 */
.section-title {
text-align: center;
color: #333;
font-size: 20px;
margin-bottom: 25px;
font-weight: 600;
}
.lottery-options {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
}
.lottery-category {
margin-bottom: 25px;
}
.category-title {
text-align: left;
color: #333;
font-size: 18px;
margin-bottom: 15px;
font-weight: 600;
padding-bottom: 5px;
border-bottom: 1px solid #f0f0f0;
}
.lottery-category-options {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: flex-start;
}
.lottery-option {
width: 100px;
padding: 15px 10px;
border-radius: 12px;
text-align: center;
background: white;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
cursor: pointer;
}
.lottery-option.active {
border-color: #4caf50;
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.2);
transform: translateY(-2px);
background-color: #f1f8e9;
}
.lottery-option:hover:not(.active) {
transform: translateY(-3px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
border-color: #e0e0e0;
}
.lottery-option-image {
width: 50px;
height: 50px;
margin: 0 auto 10px;
display: flex;
align-items: center;
justify-content: center;
}
.lottery-option-image img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.lottery-option-name {
font-size: 14px;
font-weight: 600;
color: #333;
}
/* 开发中提示框 */
.tip-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.tip-content {
background: white;
padding: 30px;
border-radius: 16px;
text-align: center;
max-width: 320px;
width: 90%;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
}
.tip-icon {
font-size: 40px;
margin-bottom: 15px;
}
.tip-content h3 {
font-size: 20px;
margin-bottom: 10px;
}
.tip-content p {
color: #666;
margin-bottom: 20px;
}
.tip-button {
background: #e53e3e;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
.tip-button:hover {
background: #c53030;
}
/* 响应式设计 */
@media (max-width: 768px) {
.lottery-container {
padding: 10px 10px 0px 10px;
}
.search-form {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.lottery-options {
gap: 12px;
}
.lottery-option {
width: 100px;
padding: 12px;
}
.page-title h1 {
font-size: 28px;
}
.page-title p {
font-size: 16px;
}
.lottery-category-options {
justify-content: center;
gap: 10px;
}
.lottery-option {
width: calc(25% - 10px);
min-width: 80px;
padding: 10px 5px;
}
.lottery-option-image {
width: 40px;
height: 40px;
}
.lottery-option-name {
font-size: 12px;
}
.category-title {
text-align: center;
font-size: 16px;
}
.table-analysis-info {
padding: 5px 0;
}
.table-analysis-info p {
font-size: 14px;
}
}
@media (max-width: 480px) {
.lottery-option {
width: calc(33% - 10px);
}
}
.lottery-records {
display: flex;
flex-direction: column;
gap: 15px;
}
.lottery-record-item {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #f0f0f0;
}
.lottery-record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.lottery-period {
font-size: 16px;
font-weight: bold;
color: #333;
}
.lottery-date {
color: #666;
font-size: 14px;
}
.lottery-balls-container {
display: flex;
align-items: center;
gap: 15px;
}
.table-analysis-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
padding: 10px 0;
}
.table-analysis-info p {
color: #666;
margin: 0;
text-align: center;
}
</style>

View File

@@ -0,0 +1,610 @@
<template>
<div class="lottery-selection">
<!-- 顶部标题区域 -->
<div class="header">
<div class="header-content">
<div class="page-title">
<h1>彩票智能猪手</h1>
<p class="subtitle">专业算法智能推荐提升体验</p>
</div>
</div>
</div>
<!-- 彩票种类选择区域 -->
<div class="lottery-types">
<h2 class="section-title">选择彩票种类</h2>
<div class="lottery-container">
<!-- 左侧中国福彩 -->
<div class="lottery-section">
<h3 class="section-subtitle">中国福彩</h3>
<div class="lottery-column">
<!-- 双色球 -->
<div class="lottery-item available" @click="goToShuangseqiu">
<div class="lottery-image">
<img src="@/assets/shuangseqiu.png" alt="双色球" />
</div>
<div class="lottery-name">双色球</div>
<div class="status-tag available-tag">可用</div>
</div>
<!-- 快乐8 -->
<div class="lottery-item disabled" @click="showDevelopingTip">
<div class="lottery-image">
<img src="@/assets/kuaile8.png" alt="快乐8" />
</div>
<div class="lottery-name">快乐8</div>
<div class="status-tag developing-tag">即将开放</div>
</div>
<!-- 福彩3D -->
<div class="lottery-item disabled" @click="showDevelopingTip">
<div class="lottery-image">
<img src="@/assets/3D.png" alt="福彩3D" />
</div>
<div class="lottery-name">福彩3D</div>
<div class="status-tag developing-tag">即将开放</div>
</div>
<!-- 七乐彩 -->
<div class="lottery-item disabled" @click="showDevelopingTip">
<div class="lottery-image">
<img src="@/assets/7lecai.png" alt="七乐彩" />
</div>
<div class="lottery-name">七乐彩</div>
<div class="status-tag developing-tag">即将开放</div>
</div>
</div>
</div>
<!-- 分隔线 -->
<div class="divider"></div>
<!-- 右侧中国体彩 -->
<div class="lottery-section">
<h3 class="section-subtitle">中国体彩</h3>
<div class="lottery-column">
<!-- 超级大乐透 -->
<div class="lottery-item disabled" @click="showDevelopingTip">
<div class="lottery-image">
<img src="@/assets/daletou.png" alt="超级大乐透" />
</div>
<div class="lottery-name">超级大乐透</div>
<div class="status-tag developing-tag">即将开放</div>
</div>
<!-- 排列3 -->
<div class="lottery-item disabled" @click="showDevelopingTip">
<div class="lottery-image">
<img src="@/assets/pailie3.png" alt="排列3" />
</div>
<div class="lottery-name">排列3</div>
<div class="status-tag developing-tag">即将开放</div>
</div>
<!-- 排列5 -->
<div class="lottery-item disabled" @click="showDevelopingTip">
<div class="lottery-image">
<img src="@/assets/pailie5.png" alt="排列5" />
</div>
<div class="lottery-name">排列5</div>
<div class="status-tag developing-tag">即将开放</div>
</div>
<!-- 七星彩 -->
<div class="lottery-item disabled" @click="showDevelopingTip">
<div class="lottery-image">
<img src="@/assets/7星彩.png" alt="七星彩" />
</div>
<div class="lottery-name">七星彩</div>
<div class="status-tag developing-tag">即将开放</div>
</div>
</div>
</div>
</div>
</div>
<!-- 开发中提示框 -->
<div v-if="showTip" class="tip-overlay" @click="hideTip">
<div class="tip-content" @click.stop>
<div class="tip-icon">🚧</div>
<h3>功能开发中</h3>
<p>该彩票类型的智能分析功能正在开发中敬请期待</p>
<button class="tip-button" @click="hideTip">我知道了</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'LotterySelection',
data() {
return {
showTip: false
}
},
methods: {
goToShuangseqiu() {
this.$router.push('/shuangseqiu')
},
showDevelopingTip() {
this.showTip = true
},
hideTip() {
this.showTip = false
}
}
}
</script>
<style scoped>
.lottery-selection {
min-height: 100vh;
background: #f0f2f5;
padding: 20px;
padding-bottom: 30px; /* 增加底部边距 */
}
.header {
background: url('@/assets/banner/backend1.png') center/cover no-repeat, linear-gradient(135deg, #ff6b6b, #ee5a52);
color: white;
padding: 45px 20px 25px;
text-align: center;
position: relative;
margin-bottom: 20px;
border-radius: 8px;
}
.header-content {
max-width: 900px;
margin: 0 auto;
}
.page-title {
margin: 0;
text-align: center;
}
.page-title h1 {
font-size: 38px;
margin: 0 auto 10px;
font-weight: 700;
color: white;
text-shadow: 0 2px 6px rgba(0,0,0,0.7), 0 0 15px rgba(0,0,0,0.4);
letter-spacing: 2px;
text-align: center;
width: 100%;
}
.page-title p {
font-size: 22px;
opacity: 0.9;
color: white;
text-shadow: 0 2px 6px rgba(0,0,0,0.7), 0 0 15px rgba(0,0,0,0.4);
text-align: center;
width: 100%;
margin: 0 auto;
font-weight: 500;
}
.subtitle {
font-size: 16px;
margin: 0;
opacity: 1;
text-shadow: 0 2px 6px rgba(0,0,0,0.7), 0 0 15px rgba(0,0,0,0.4);
color: white;
font-weight: 500;
}
.lottery-types {
padding: 30px 20px 20px; /* 减少底部padding从40px到20px */
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 8px; /* 添加圆角 */
box-shadow: 0 2px 8px rgba(0,0,0,0.05); /* 添加轻微阴影效果 */
border: 1px solid #f0f0f0; /* 添加边框 */
}
.section-title {
text-align: center;
color: #333;
font-size: 20px;
margin-bottom: 25px;
font-weight: 600;
}
.lottery-container {
display: flex;
justify-content: space-between;
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
border: 1px solid #f0f0f0;
overflow: hidden;
}
.lottery-section {
flex: 1; /* 使类别占据相等宽度 */
padding: 20px 15px;
transition: all 0.3s ease;
position: relative;
display: flex;
flex-direction: column;
}
.section-subtitle {
text-align: center;
color: #333;
font-size: 18px;
margin-bottom: 20px;
font-weight: 600;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.lottery-column {
display: flex;
flex-direction: column;
gap: 20px; /* 在列中添加间距 */
flex: 1;
}
.lottery-item {
background: white;
border-radius: 12px;
padding: 25px 15px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
transition: all 0.3s ease;
position: relative;
cursor: pointer;
width: 280px;
min-height: 180px;
border: 1px solid #f0f0f0;
margin: 0 auto;
}
.lottery-item.available:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0,0,0,0.12);
}
.lottery-item.disabled {
opacity: 0.7;
}
.lottery-item.disabled:hover {
transform: translateY(-1px);
}
.lottery-image {
width: 90px;
height: 90px;
margin: 0 auto 15px;
border-radius: 12px;
overflow: visible;
display: flex;
align-items: center;
justify-content: center;
}
.lottery-image img {
width: 100%;
height: auto;
max-height: 100%;
object-fit: contain;
}
.placeholder-icon {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 18px;
border-radius: 12px;
}
.lottery-name {
font-size: 17px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.status-tag {
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
font-weight: 500;
}
.available-tag {
background: #e8f5e8;
color: #2e7d32;
}
.developing-tag {
background: #fff3e0;
color: #f57c00;
}
.divider {
width: 1px;
align-self: stretch;
background-color: #f0f0f0;
}
.tip-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.tip-content {
background: white;
border-radius: 20px;
padding: 40px 30px;
text-align: center;
max-width: 320px;
width: 100%;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.tip-icon {
font-size: 48px;
margin-bottom: 20px;
}
.tip-content h3 {
font-size: 22px;
color: #333;
margin: 0 0 15px 0;
font-weight: 600;
}
.tip-content p {
color: #666;
font-size: 16px;
line-height: 1.5;
margin: 0 0 25px 0;
}
.tip-button {
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.tip-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(255, 107, 107, 0.3);
}
.tip-button:active {
transform: translateY(0);
}
/* 响应式设计 */
@media (max-width: 768px) {
.lottery-selection {
padding: 10px;
padding-bottom: 20px; /* 保持底部边距 */
}
.header {
padding: 45px 15px 35px;
}
.page-title {
margin: 0 0 8px 0;
}
.page-title h1 {
font-size: 32px;
margin-bottom: 5px;
}
.page-title p {
font-size: 20px;
}
.subtitle {
font-size: 14px;
}
.lottery-container {
flex-direction: column; /* 在移动端堆叠类别 */
gap: 0; /* 类别之间的间距 */
}
.lottery-section {
width: 100%;
max-width: none; /* 移除最大宽度限制 */
padding: 15px;
}
.section-subtitle {
font-size: 16px;
margin-bottom: 15px;
padding-bottom: 8px;
}
.lottery-column {
gap: 15px; /* 列中的间距 */
}
.lottery-item {
width: 100%;
max-width: none; /* 移除最大宽度限制 */
min-height: auto; /* 自适应高度 */
padding: 12px;
display: flex;
align-items: center;
text-align: left;
margin: 0;
}
.lottery-image {
width: 60px;
height: 60px;
margin: 0 12px 0 0;
overflow: visible;
}
.lottery-name {
font-size: 15px;
margin-bottom: 4px;
}
.status-tag {
font-size: 11px;
padding: 3px 8px;
}
.divider {
width: 100%;
height: 1px;
margin: 0;
}
}
@media (max-width: 480px) {
.header {
padding: 40px 12px 30px;
}
.page-title {
margin: 0 0 6px 0;
}
.page-title h1 {
font-size: 28px;
margin-bottom: 3px;
}
.page-title p {
font-size: 18px;
}
.subtitle {
font-size: 13px;
}
.lottery-types {
padding: 15px 12px 20px; /* 减少左右padding */
}
.section-title {
font-size: 16px;
margin-bottom: 15px;
}
.lottery-container {
flex-direction: column; /* 在移动端堆叠类别 */
gap: 0; /* 无间距,使用分隔线 */
}
.lottery-section {
width: 100%;
max-width: none; /* 移除最大宽度限制 */
padding: 12px 10px;
}
.section-subtitle {
font-size: 15px;
margin-bottom: 12px;
padding-bottom: 6px;
}
.lottery-column {
gap: 10px; /* 减少列中的间距 */
}
.lottery-item {
width: 100%;
min-height: auto; /* 自适应高度 */
padding: 10px 8px;
margin: 0;
display: flex;
align-items: center;
text-align: left;
}
.lottery-image {
width: 50px;
height: 50px;
margin: 0 10px 0 0;
overflow: visible;
}
.lottery-name {
font-size: 14px;
margin-bottom: 2px;
font-weight: 600;
}
.status-tag {
font-size: 10px;
padding: 2px 6px;
}
.divider {
width: 100%;
height: 1px;
margin: 0;
}
.tip-overlay {
padding: 15px;
}
.tip-content {
padding: 25px 15px;
}
.tip-content h3 {
font-size: 18px;
}
.tip-content p {
font-size: 13px;
}
}
/* 桌面端样式 */
@media (min-width: 1024px) {
.header {
padding: 40px 20px 30px;
}
.page-title h1 {
font-size: 42px;
margin-bottom: 10px;
}
.page-title p {
font-size: 24px;
}
}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<div class="not-found">
<div class="not-found-content">
<div class="error-code">404</div>
<h1>页面未找到</h1>
<p>抱歉您访问的页面不存在或已被移除</p>
<div class="actions">
<el-button type="primary" @click="goHome">
<el-icon><House /></el-icon>
返回首页
</el-button>
<el-button @click="goBack">
<el-icon><Back /></el-icon>
返回上页
</el-button>
</div>
</div>
</div>
</template>
<script>
import { useRouter } from 'vue-router'
import { House, Back } from '@element-plus/icons-vue'
export default {
name: 'NotFound',
components: {
House,
Back
},
setup() {
const router = useRouter()
const goHome = () => {
router.push('/')
}
const goBack = () => {
router.go(-1)
}
return {
goHome,
goBack
}
}
}
</script>
<style scoped>
.not-found {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.not-found-content {
text-align: center;
color: white;
max-width: 500px;
}
.error-code {
font-size: 120px;
font-weight: 700;
line-height: 1;
margin-bottom: 20px;
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.not-found-content h1 {
font-size: 32px;
font-weight: 600;
margin-bottom: 16px;
}
.not-found-content p {
font-size: 16px;
opacity: 0.9;
margin-bottom: 32px;
line-height: 1.5;
}
.actions {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
.actions .el-button {
min-width: 120px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.error-code {
font-size: 80px;
}
.not-found-content h1 {
font-size: 24px;
}
.not-found-content p {
font-size: 14px;
}
.actions {
flex-direction: column;
align-items: center;
}
.actions .el-button {
width: 200px;
}
}
</style>

View File

@@ -0,0 +1,383 @@
<template>
<div class="predict-records-page">
<!-- 页面头部 -->
<el-page-header @back="goBack" class="page-header">
<template #title>
返回
</template>
<template #content>
<div class="header-content">
<el-icon :size="24" style="margin-right: 8px;"><Tickets /></el-icon>
<span class="header-title">推测记录</span>
</div>
</template>
</el-page-header>
<!-- 状态筛选 -->
<div class="filter-container">
<el-radio-group v-model="currentFilter" @change="handleFilterChange">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="待开奖">待开奖</el-radio-button>
<el-radio-button label="已中奖">已中奖</el-radio-button>
<el-radio-button label="未中奖">未中奖</el-radio-button>
</el-radio-group>
</div>
<!-- 主要内容 -->
<div class="records-container" v-loading="loading" element-loading-text="正在加载记录...">
<!-- 空状态 -->
<el-empty v-if="!loading && records.length === 0" :description="getEmptyStateMessage()">
<el-button type="primary" @click="$router.push('/')">开始推测</el-button>
</el-empty>
<!-- 记录列表 -->
<div v-else class="records-grid">
<el-card
v-for="record in records"
:key="record.id"
class="record-card"
shadow="hover"
>
<template #header>
<div class="record-card-header">
<span class="draw-info">{{ record.drawId }}</span>
<el-tag :type="getStatusClass(record.predictStatus)" effect="dark" size="small">
{{ record.predictStatus }}
</el-tag>
</div>
</template>
<div class="record-body">
<div class="predicted-numbers">
<div class="balls-display">
<div class="red-balls">
<span class="ball ball-red">{{ formatNumber(record.redBall1) }}</span>
<span class="ball ball-red">{{ formatNumber(record.redBall2) }}</span>
<span class="ball ball-red">{{ formatNumber(record.redBall3) }}</span>
<span class="ball ball-red">{{ formatNumber(record.redBall4) }}</span>
<span class="ball ball-red">{{ formatNumber(record.redBall5) }}</span>
<span class="ball ball-red">{{ formatNumber(record.redBall6) }}</span>
</div>
<div class="separator">+</div>
<div class="blue-ball">
<span class="ball ball-blue">{{ formatNumber(record.blueBall) }}</span>
</div>
</div>
</div>
<el-descriptions :column="1" border size="small" class="record-details">
<el-descriptions-item label="推测时间">{{ formatDateTime(record.predictTime) }}</el-descriptions-item>
<el-descriptions-item label="推测结果">
<span class="result-text" :class="getResultClass(record.predictResult)">
{{ record.predictResult }}
</span>
</el-descriptions-item>
<el-descriptions-item v-if="record.predictStatus !== '待开奖'" label="奖金">
<span class="bonus-amount" :class="{ 'zero-bonus': record.bonus === 0 }">
{{ isHighPrize(record.predictResult) ? '约' : '' }}¥{{ record.bonus || 0 }}
</span>
</el-descriptions-item>
</el-descriptions>
<div v-if="isHighPrize(record.predictResult)" class="prize-note">
二等奖奖金随机浮动请以官方为准
</div>
</div>
</el-card>
</div>
<!-- 分页组件 -->
<div v-if="totalPages > 1" class="pagination-container">
<el-pagination
background
layout="prev, pager, next"
:total="total"
:page-size="pageSize"
v-model:current-page="currentPage"
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</template>
<script>
import { lotteryApi } from '../api/index.js'
import { userStore } from '../store/user.js'
import {
ElPageHeader,
ElCard,
ElButton,
ElIcon,
ElEmpty,
ElTag,
ElRadioGroup,
ElRadioButton,
ElPagination,
ElDescriptions,
ElDescriptionsItem
} from 'element-plus'
import { Tickets } from '@element-plus/icons-vue'
export default {
name: 'PredictRecords',
components: {
ElPageHeader,
ElCard,
ElButton,
ElIcon,
ElEmpty,
ElTag,
ElRadioGroup,
ElRadioButton,
ElPagination,
ElDescriptions,
ElDescriptionsItem,
Tickets
},
data() {
return {
loading: false,
records: [],
currentPage: 1,
totalPages: 0,
total: 0,
pageSize: 10,
hasNext: false,
hasPrevious: false,
currentFilter: 'all'
}
},
async mounted() {
await this.loadPredictRecords()
},
methods: {
async loadPredictRecords(page = 1) {
this.loading = true
try {
const userId = userStore.user?.id
if (!userId) {
this.$router.push('/login')
return
}
let response
if (this.currentFilter === 'all') {
response = await lotteryApi.getPredictRecordsByUserId(Number(userId), page, this.pageSize)
} else {
response = await lotteryApi.queryPredictRecords(Number(userId), this.currentFilter, page, this.pageSize)
}
if (response.success === true) {
const pageData = response.data
this.records = pageData.records || []
this.currentPage = pageData.page || 1
this.totalPages = pageData.totalPages || 0
this.total = pageData.total || 0
this.pageSize = pageData.size || this.pageSize
this.hasNext = pageData.hasNext || false
this.hasPrevious = pageData.hasPrevious || false
} else {
this.$message.error('获取推测记录失败:' + response.message)
}
} catch (error) {
this.$message.error('获取推测记录失败,请重试')
} finally {
this.loading = false
}
},
formatDate(dateString) {
if (!dateString) return ''
return new Date(dateString).toLocaleDateString('zh-CN')
},
formatDateTime(dateString) {
if (!dateString) return ''
return new Date(dateString).toLocaleString('zh-CN')
},
formatNumber(num) {
return String(num).padStart(2, '0')
},
getStatusClass(status) {
switch (status) {
case '待开奖': return 'warning'
case '已中奖': return 'success'
case '未中奖': return 'danger'
default: return 'info'
}
},
getResultClass(result) {
if (result && result.includes('中奖')) {
return 'result-won'
} else if (result === '待开奖') {
return 'result-pending'
} else {
return 'result-lost'
}
},
async handlePageChange(page) {
await this.loadPredictRecords(page)
},
isHighPrize(result) {
return result && (result.includes('一等奖') || result.includes('二等奖'));
},
getEmptyStateMessage() {
const filterMap = {
'待开奖': '待开奖',
'已中奖': '已中奖',
'未中奖': '未中奖'
}
const state = filterMap[this.currentFilter]
return state ? `您还没有${state}的推测记录` : '您还没有创建任何推测记录'
},
handleFilterChange() {
this.currentPage = 1
this.loadPredictRecords()
},
goBack() {
this.$router.go(-1);
}
}
}
</script>
<style scoped>
.predict-records-page {
background-color: #f0f2f5;
padding: 20px;
min-height: calc(100vh - 70px);
}
.page-header {
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
padding: 30px 24px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
:deep(.el-page-header__back .el-icon),
:deep(.el-page-header__back .el-page-header__title) {
color: white;
}
.header-content {
display: flex;
align-items: center;
color: white;
}
.header-title {
font-size: 20px;
font-weight: 600;
}
.filter-container {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.records-container {
max-width: 800px;
margin: 0 auto;
}
.records-grid {
display: grid;
gap: 16px;
}
.record-card {
border-radius: 8px;
}
.record-card-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.draw-info {
font-weight: 600;
}
.predicted-numbers {
margin-bottom: 16px;
}
.balls-display {
display: flex;
align-items: center;
gap: 10px;
}
.red-balls {
display: flex;
gap: 5px;
}
.ball {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
color: white;
}
.ball-red {
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
}
.ball-blue {
background: linear-gradient(135deg, #2196f3, #64b5f6);
}
.separator {
color: #999;
font-weight: bold;
font-size: 16px;
}
.result-text {
font-weight: 600;
}
.result-won { color: #67C23A; }
.result-pending { color: #E6A23C; }
.result-lost { color: #F56C6C; }
.bonus-amount {
font-weight: 600;
color: #F56C6C;
}
.bonus-amount.zero-bonus {
color: #909399;
}
.prize-note {
margin-top: 8px;
font-size: 12px;
color: #E6A23C;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 24px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,875 @@
<template>
<div class="register-page-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="page-title">
<h1 class="main-title">彩票猪手</h1>
<p class="subtitle">您的专属彩票数据助理</p>
</div>
</div>
<!-- 注册表单 -->
<el-card class="register-form-container" shadow="never">
<form @submit.prevent="handleRegister" class="register-form">
<!-- 账号 -->
<div class="form-group">
<el-input
v-model="formData.username"
placeholder="请输入账号"
:error="errors.username"
prefix-icon="User"
size="large"
@blur="validateUsername"
clearable
/>
<div v-if="errors.username" class="error-text">{{ errors.username }}</div>
</div>
<!-- 昵称 -->
<div class="form-group">
<el-input
v-model="formData.nickname"
placeholder="请输入昵称"
:error="errors.nickname"
prefix-icon="Avatar"
size="large"
@blur="validateNickname"
clearable
/>
<div v-if="errors.nickname" class="error-text">{{ errors.nickname }}</div>
</div>
<!-- 手机号 -->
<div class="form-group">
<el-input
v-model="formData.phone"
type="tel"
placeholder="请输入手机号"
:error="errors.phone"
prefix-icon="Iphone"
size="large"
maxlength="11"
@input="validatePhoneInput"
@blur="validatePhoneOnBlur"
clearable
/>
<div v-if="errors.phone" class="error-text">{{ errors.phone }}</div>
<div v-else-if="formData.phone && formData.phone.length > 0 && formData.phone.length < 11" class="tip-text">请输入11位手机号码</div>
</div>
<!-- 验证码 -->
<div class="form-group">
<el-input
v-model="formData.code"
placeholder="请输入验证码"
prefix-icon="Key"
size="large"
@blur="validateCode"
>
<template #append>
<el-button
type="primary"
:disabled="codeBtnDisabled"
@click="sendVerificationCode"
>
{{ codeButtonText }}
</el-button>
</template>
</el-input>
<div v-if="errors.code" class="error-text">{{ errors.code }}</div>
</div>
<!-- 密码 -->
<div class="form-group">
<el-input
v-model="formData.password"
placeholder="请输入密码"
:error="errors.password"
prefix-icon="Lock"
size="large"
:type="showPassword ? 'text' : 'password'"
:show-password="true"
autocomplete="new-password"
@blur="validatePassword"
/>
<div v-if="errors.password" class="error-text">{{ errors.password }}</div>
</div>
<!-- 确认密码 -->
<div class="form-group">
<el-input
v-model="formData.confirmPassword"
placeholder="请确认密码"
:error="errors.confirmPassword"
prefix-icon="Lock"
size="large"
:type="showConfirmPassword ? 'text' : 'password'"
:show-password="true"
autocomplete="new-password"
@blur="validateConfirmPassword"
/>
<div v-if="errors.confirmPassword" class="error-text">{{ errors.confirmPassword }}</div>
</div>
<!-- 服务条款 -->
<div class="form-group">
<el-checkbox v-model="formData.agreeTerms" class="checkbox-custom">
我已阅读并同意
<a href="#" class="terms-link" @click.prevent="showTerms">用户服务协议</a>
</el-checkbox>
<div v-if="errors.agreeTerms" class="error-text">{{ errors.agreeTerms }}</div>
</div>
<!-- 注册按钮 -->
<el-button
type="primary"
native-type="submit"
:loading="loading"
class="register-btn"
size="large"
>
{{ loading ? '注册中...' : '立即注册' }}
</el-button>
<!-- 登录链接 -->
<div class="login-link">
<span>已有账号</span>
<router-link to="/login" class="link">立即登录</router-link>
</div>
</form>
</el-card>
</div>
</template>
<script>
import { userStore } from '../store/user'
import { lotteryApi } from '../api/index.js'
import { useToast } from 'vue-toastification'
import { useRouter } from 'vue-router'
import { ElCard, ElInput, ElButton, ElCheckbox } from 'element-plus'
import { User, Lock, Iphone, Key, Avatar } from '@element-plus/icons-vue'
export default {
name: 'Register',
components: {
ElCard,
ElInput,
ElButton,
ElCheckbox,
User,
Lock,
Iphone,
Key,
Avatar
},
setup() {
const toast = useToast()
const router = useRouter()
return { toast, router }
},
data() {
return {
showPassword: false,
showConfirmPassword: false,
loading: false,
codeCountdown: 0,
timer: null,
showPhoneError: false,
phoneValid: false,
formData: {
username: '',
nickname: '',
password: '',
confirmPassword: '',
phone: '',
code: '',
agreeTerms: false
},
errors: {}
}
},
computed: {
codeButtonText() {
return this.codeCountdown > 0 ? `${this.codeCountdown}秒后重试` : '获取验证码';
},
codeBtnDisabled() {
return this.codeCountdown > 0 || !this.isValidPhone(this.formData.phone);
}
},
methods: {
// 手机号格式验证
isValidPhone(phone) {
return /^1[3-9]\d{9}$/.test(phone);
},
// 手机号输入时验证
validatePhoneInput() {
// 清除之前的错误
this.errors.phone = '';
this.showPhoneError = false;
this.phoneValid = false;
const phone = this.formData.phone;
// 如果输入不是数字,替换非数字字符
if (!/^\d*$/.test(phone)) {
this.formData.phone = phone.replace(/\D/g, '');
}
// 如果长度达到11位验证格式
if (phone.length === 11) {
if (this.isValidPhone(phone)) {
this.phoneValid = true;
} else {
this.errors.phone = '手机号格式不正确';
this.showPhoneError = true;
}
}
},
// 手机号失焦时验证
validatePhoneOnBlur() {
const phone = this.formData.phone;
if (phone && phone.length > 0) {
if (phone.length !== 11) {
this.errors.phone = '手机号应为11位数字';
this.showPhoneError = true;
this.phoneValid = false;
} else if (!this.isValidPhone(phone)) {
this.errors.phone = '请输入正确的手机号码';
this.showPhoneError = true;
this.phoneValid = false;
} else {
this.phoneValid = true;
}
}
},
// 发送验证码
async sendVerificationCode() {
if (!this.formData.phone) {
this.errors.phone = '请输入手机号';
this.showPhoneError = true;
this.phoneValid = false;
return;
}
if (!this.isValidPhone(this.formData.phone)) {
this.errors.phone = '请输入正确的手机号码';
this.showPhoneError = true;
this.phoneValid = false;
return;
}
this.phoneValid = true;
try {
// 开始倒计时 (先启动倒计时避免API延迟导致用户体验不佳)
this.codeCountdown = 60;
this.startCodeCountdown();
const response = await lotteryApi.sendSmsCode(this.formData.phone);
if (response.success) {
this.toast.success('验证码已发送,请注意查收');
} else {
// 如果发送失败,停止倒计时
this.codeCountdown = 0;
clearInterval(this.timer);
this.toast.error(response.message || '发送验证码失败,请稍后重试');
}
} catch (error) {
// 如果发送出错,停止倒计时
this.codeCountdown = 0;
clearInterval(this.timer);
console.error('发送验证码失败:', error);
this.toast.error('发送验证码失败,请稍后重试');
}
},
// 开始倒计时
startCodeCountdown() {
if (this.timer) {
clearInterval(this.timer);
}
this.timer = setInterval(() => {
if (this.codeCountdown > 0) {
this.codeCountdown--;
} else {
clearInterval(this.timer);
this.timer = null;
}
}, 1000);
},
// 验证账号
validateUsername() {
if (!this.formData.username) {
this.errors.username = '请输入账号';
} else if (this.formData.username.length < 3 || this.formData.username.length > 20) {
this.errors.username = '账号长度为3-20个字符';
} else if (!/^[a-zA-Z0-9_]+$/.test(this.formData.username)) {
this.errors.username = '账号只能包含字母、数字和下划线';
} else {
delete this.errors.username;
}
},
// 验证昵称
validateNickname() {
if (!this.formData.nickname) {
this.errors.nickname = '请输入昵称';
} else if (this.formData.nickname.length < 2 || this.formData.nickname.length > 20) {
this.errors.nickname = '昵称长度为2-20个字符';
} else {
delete this.errors.nickname;
}
},
// 验证验证码
validateCode() {
if (!this.formData.code) {
this.errors.code = '请输入验证码';
} else if (this.formData.code.length < 4 || this.formData.code.length > 6) {
this.errors.code = '验证码格式不正确';
} else {
delete this.errors.code;
}
},
// 验证密码
validatePassword() {
if (!this.formData.password) {
this.errors.password = '请输入密码';
} else if (this.formData.password.length < 8 || this.formData.password.length > 20) {
this.errors.password = '密码长度不小于8位';
} else {
delete this.errors.password;
}
// 如果确认密码有值,则重新校验
if (this.formData.confirmPassword) {
this.validateConfirmPassword();
}
},
// 验证确认密码
validateConfirmPassword() {
if (!this.formData.confirmPassword) {
this.errors.confirmPassword = '请确认密码';
} else if (this.formData.password !== this.formData.confirmPassword) {
this.errors.confirmPassword = '两次输入的密码不一致';
} else {
delete this.errors.confirmPassword;
}
},
// 表单验证
validateForm() {
this.errors = {};
this.validateUsername();
this.validateNickname();
this.validatePhoneOnBlur();
this.validateCode();
this.validatePassword();
this.validateConfirmPassword();
// 服务条款验证
if (!this.formData.agreeTerms) {
this.errors.agreeTerms = '请同意用户服务协议';
}
return Object.keys(this.errors).length === 0;
},
// 处理注册
async handleRegister() {
if (!this.validateForm()) {
return;
}
this.loading = true;
try {
// 调用手机号注册接口
const response = await lotteryApi.userPhoneRegister(
this.formData.username,
this.formData.password,
this.formData.confirmPassword,
this.formData.phone,
this.formData.code,
this.formData.nickname
);
if (response.success === true) {
// 注册成功
this.toast.success('注册成功!请登录您的账号');
this.router.push('/login');
} else {
// 注册失败
this.toast.error(response.message || '注册失败,请稍后重试');
}
} catch (error) {
console.error('注册失败:', error);
if (error.response && error.response.data) {
this.toast.error(error.response.data.message || '注册失败,请稍后重试');
} else {
this.toast.error('网络错误,请重试');
}
} finally {
this.loading = false;
}
},
// 显示服务条款
showTerms() {
this.$router.push('/user-agreement');
}
},
// 组件销毁时清除定时器
beforeUnmount() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
}
</script>
<style scoped>
/* 注册页面容器 */
.register-page-container {
min-height: calc(100vh - 70px);
background: #f0f2f5;
padding: 20px 20px 0px 20px;
}
/* 页面头部 */
.page-header {
background: url('@/assets/banner/backend1.png') center/cover no-repeat, linear-gradient(135deg, #ff6b6b, #ee5a52);
color: white;
padding: 45px 20px 25px;
text-align: center;
position: relative;
margin-bottom: 20px;
border-radius: 8px;
}
.page-title {
margin: 0;
text-align: center;
}
.main-title {
font-size: 38px;
margin: 0 auto 10px;
font-weight: 700;
color: white;
text-shadow: 0 2px 6px rgba(0,0,0,0.7), 0 0 15px rgba(0,0,0,0.4);
letter-spacing: 2px;
text-align: center;
width: 100%;
}
.subtitle {
font-size: 22px;
margin: 0;
color: white;
opacity: 0.9;
text-shadow: 0 2px 6px rgba(0,0,0,0.7), 0 0 15px rgba(0,0,0,0.4);
text-align: center;
width: 100%;
font-weight: 500;
}
/* 桌面端样式 */
@media (min-width: 1024px) {
.page-header {
padding: 40px 20px 30px;
}
}
.register-form-container {
padding: 0;
background: white;
margin: 0 0 20px 0;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.register-form {
background: white;
border-radius: 0;
padding: 30px 25px;
box-shadow: none;
}
/* 表单组 */
.form-group {
margin-bottom: 24px;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
border: 2px solid #e9ecef;
border-radius: 6px;
background: #f8f9fa;
transition: all 0.3s ease;
min-height: 56px;
overflow: hidden;
}
.input-wrapper:focus-within {
border-color: #e9ecef;
background: white;
box-shadow: none;
outline: none;
}
.input-wrapper.error {
border-color: #dc3545;
background: #fff5f5;
}
.input-wrapper.success {
border-color: #4caf50;
background: #f8fff8;
}
.validation-icon {
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
}
.input-icon {
padding: 12px 25px;
color: #6c757d;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
}
.icon-img {
width: 22px;
height: 22px;
object-fit: contain;
}
.form-input {
flex: 1;
padding: 16px 8px;
border: none;
outline: none;
font-size: 16px;
background: transparent;
color: #212529;
box-shadow: none;
-webkit-appearance: none;
}
.form-input:focus {
outline: none;
border: none;
box-shadow: none;
}
/* 控制浏览器自动填充的样式 */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px white inset !important;
-webkit-text-fill-color: #212529 !important;
transition: background-color 5000s ease-in-out 0s;
border: none !important;
outline: none !important;
}
/* Element Plus验证码按钮已在组件中实现移除原来的样式以避免冲突 */
/* 隐藏浏览器自带的密码控件 */
input::-ms-reveal,
input::-ms-clear {
display: none;
}
input::-webkit-credentials-auto-fill-button {
visibility: hidden;
position: absolute;
right: 0;
}
.form-input::placeholder {
color: #9ca3af;
}
.password-toggle {
padding: 0 15px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.toggle-icon-img {
width: 22px;
height: 22px;
object-fit: contain;
}
.error-text {
color: #ff4444;
font-size: 12px;
margin-top: 5px;
margin-left: 8px;
}
.tip-text {
color: #888888;
font-size: 12px;
margin-top: 5px;
margin-left: 8px;
}
/* 复选框 */
.checkbox-wrapper {
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
color: #666;
}
.checkbox-wrapper input {
display: none;
}
.checkmark {
width: 16px;
height: 16px;
border: 1px solid #ddd;
border-radius: 3px;
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: transparent;
transition: all 0.3s;
flex-shrink: 0;
}
.checkbox-wrapper input:checked + .checkmark {
background: #e53e3e;
border-color: #e53e3e;
color: white;
}
.terms-link {
color: #e53e3e;
text-decoration: none;
margin-left: 4px;
}
.terms-link:hover {
text-decoration: underline;
}
/* 注册按钮 */
.register-btn {
width: 100%;
margin: 30px 0 25px 0;
padding: 12px;
font-size: 18px;
height: auto;
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
border: none;
box-shadow: 0 4px 15px rgba(229, 62, 62, 0.3);
}
.register-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(229, 62, 62, 0.4);
background: linear-gradient(135deg, #d43030, #ff5a5a);
border: none;
}
/* Element UI 组件自定义样式 */
:deep(.el-input__wrapper) {
padding: 4px 11px;
box-shadow: none !important;
background-color: #f8f9fa;
border: 2px solid #e9ecef;
}
:deep(.el-input__wrapper.is-focus) {
background-color: #fff;
border-color: #e53e3e;
}
:deep(.el-input__prefix) {
margin-right: 8px;
}
:deep(.el-input__inner) {
height: 40px;
font-size: 16px;
}
:deep(.el-checkbox__label) {
font-size: 14px;
color: #666;
}
:deep(.el-checkbox__inner) {
border-color: #ddd;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #e53e3e;
border-color: #e53e3e;
}
:deep(.el-input-group__append) {
padding: 0;
border: none;
}
:deep(.el-input-group__append .el-button) {
background-color: #ECF5FF;
border: 1px solid #DCDFE6;
margin: 0;
height: 100%;
border-radius: 0 4px 4px 0;
padding: 0 15px;
font-size: 14px;
font-weight: normal;
color: #e53e3e;
min-width: 105px;
box-shadow: none;
}
:deep(.el-input-group__append .el-button:hover) {
background-color: #e6f1ff;
color: #e53e3e;
}
:deep(.el-input-group__append .el-button.is-disabled) {
background-color: #F5F7FA;
color: #C0C4CC;
border-color: #E4E7ED;
}
:deep(.el-button.is-disabled) {
background: #cccccc;
border-color: #cccccc;
}
/* 处理用户协议链接 */
.checkbox-custom :deep(.el-checkbox__label) {
display: flex;
align-items: center;
}
/* 登录链接 */
.login-link {
text-align: center;
font-size: 14px;
color: #666;
}
.login-link .link {
color: #e53e3e;
text-decoration: none;
margin-left: 5px;
}
.login-link .link:hover {
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 768px) {
.register-page-container {
padding: 10px;
}
.page-header {
padding: 45px 15px 35px;
}
.page-title {
margin: 0 0 8px 0;
}
.send-code-btn {
font-size: 12px;
min-width: 90px;
padding: 0 10px;
}
}
@media (max-width: 480px) {
.page-header {
padding: 40px 12px 30px;
}
.page-title {
margin: 0 0 6px 0;
}
.main-title {
font-size: 28px;
margin-bottom: 3px;
}
.subtitle {
font-size: 18px;
}
.register-form {
padding: 20px 15px;
}
.input-icon {
padding: 12px 25px;
}
.icon-img {
width: 22px;
height: 22px;
}
.toggle-icon-img {
width: 20px;
height: 20px;
}
.password-toggle {
padding: 0 12px;
}
.send-code-btn {
font-size: 12px;
min-width: 85px;
padding: 0 8px;
}
}
/* 桌面端样式 - 这部分已在上面定义,这里移除重复定义 */
</style>

View File

@@ -0,0 +1,848 @@
<template>
<div class="reset-password-page-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="page-title">
<h1>找回密码</h1>
</div>
</div>
<!-- 找回密码表单 -->
<div class="reset-password-form-container">
<form @submit.prevent="handleResetPassword" class="reset-password-form">
<!-- 手机号 -->
<div class="form-group">
<div class="input-wrapper" :class="{ error: errors.phone }">
<div class="input-icon">
<img src="@/assets/icon/login/shouji.png" alt="手机号" class="icon-img" />
</div>
<input
v-model="formData.phone"
type="tel"
class="form-input"
placeholder="请输入手机号"
maxlength="11"
@input="validatePhoneInput"
@blur="validatePhoneOnBlur"
/>
</div>
<div v-if="errors.phone" class="error-text">{{ errors.phone }}</div>
<div v-else-if="formData.phone && formData.phone.length > 0 && formData.phone.length < 11" class="tip-text">请输入11位手机号码</div>
</div>
<!-- 验证码 -->
<div class="form-group">
<div class="input-wrapper code-input-wrapper" :class="{ error: errors.code }">
<div class="input-icon">
<img src="@/assets/icon/login/mima.png" alt="验证码" class="icon-img" />
</div>
<input
v-model="formData.code"
type="text"
class="form-input"
placeholder="请输入验证码"
/>
<button
type="button"
class="send-code-btn"
@click="sendVerificationCode"
:disabled="codeBtnDisabled"
>
{{ codeButtonText }}
</button>
</div>
<div v-if="errors.code" class="error-text">{{ errors.code }}</div>
</div>
<!-- 新密码 -->
<div class="form-group">
<div class="input-wrapper" :class="{ error: errors.newPassword }">
<div class="input-icon">
<img src="@/assets/icon/login/mima.png" alt="新密码" class="icon-img" />
</div>
<input
v-model="formData.newPassword"
:type="showNewPassword ? 'text' : 'password'"
class="form-input"
placeholder="请输入新密码"
autocomplete="new-password"
/>
<div class="password-toggle" @click="showNewPassword = !showNewPassword">
<img
v-if="showNewPassword"
src="@/assets/icon/login/zhanshi.png"
alt="隐藏密码"
class="toggle-icon-img"
/>
<img
v-else
src="@/assets/icon/login/yingcang.png"
alt="显示密码"
class="toggle-icon-img"
/>
</div>
</div>
<div v-if="errors.newPassword" class="error-text">{{ errors.newPassword }}</div>
</div>
<!-- 确认新密码 -->
<div class="form-group">
<div class="input-wrapper" :class="{ error: errors.confirmPassword }">
<div class="input-icon">
<img src="@/assets/icon/login/mima.png" alt="确认新密码" class="icon-img" />
</div>
<input
v-model="formData.confirmPassword"
:type="showConfirmPassword ? 'text' : 'password'"
class="form-input"
placeholder="请再次输入新密码"
autocomplete="new-password"
/>
<div class="password-toggle" @click="showConfirmPassword = !showConfirmPassword">
<img
v-if="showConfirmPassword"
src="@/assets/icon/login/zhanshi.png"
alt="隐藏密码"
class="toggle-icon-img"
/>
<img
v-else
src="@/assets/icon/login/yingcang.png"
alt="显示密码"
class="toggle-icon-img"
/>
</div>
</div>
<div v-if="errors.confirmPassword" class="error-text">{{ errors.confirmPassword }}</div>
</div>
<!-- 重置密码按钮 -->
<button
type="submit"
class="reset-password-btn"
:disabled="loading"
>
{{ loading ? '重置中...' : '重置密码' }}
</button>
<!-- 返回登录链接 -->
<div class="login-link">
<span><router-link to="/login" class="link">返回登录</router-link></span>
</div>
</form>
</div>
<!-- 成功/错误弹窗 -->
<div v-if="showModal" class="modal-overlay" @click="closeModal">
<div class="modal-content" :class="{ 'success-modal': isSuccess, 'error-modal': !isSuccess }" @click.stop>
<div v-if="!isSuccess" class="error-icon"></div>
<h3>{{ modalTitle }}</h3>
<p>{{ modalMessage }}</p>
<button class="modal-btn" :class="{ 'error-btn': !isSuccess }" @click="closeModal">确定</button>
</div>
</div>
</div>
</template>
<script>
import { lotteryApi } from '../api/index.js'
import { useToast } from 'vue-toastification'
import { useRouter } from 'vue-router'
export default {
name: 'ResetPassword',
setup() {
const toast = useToast()
const router = useRouter()
return { toast, router }
},
data() {
return {
loading: false,
showNewPassword: false,
showConfirmPassword: false,
codeCountdown: 0,
timer: null,
phoneValid: false,
showModal: false,
isSuccess: false,
modalTitle: '',
modalMessage: '',
formData: {
phone: '',
code: '',
newPassword: '',
confirmPassword: '',
},
errors: {}
}
},
computed: {
codeButtonText() {
return this.codeCountdown > 0 ? `${this.codeCountdown}秒后重试` : '获取验证码';
},
codeBtnDisabled() {
return this.codeCountdown > 0 || !this.isValidPhone(this.formData.phone);
}
},
methods: {
// 手机号格式验证
isValidPhone(phone) {
return /^1[3-9]\d{9}$/.test(phone);
},
// 手机号输入时验证
validatePhoneInput() {
this.errors.phone = '';
this.phoneValid = false;
const phone = this.formData.phone;
if (!/^\d*$/.test(phone)) {
this.formData.phone = phone.replace(/\D/g, '');
}
if (phone.length === 11) {
if (this.isValidPhone(phone)) {
this.phoneValid = true;
} else {
this.errors.phone = '手机号格式不正确';
}
}
},
// 手机号失焦时验证
validatePhoneOnBlur() {
const phone = this.formData.phone;
if (phone && phone.length > 0) {
if (phone.length !== 11) {
this.errors.phone = '手机号应为11位数字';
this.phoneValid = false;
} else if (!this.isValidPhone(phone)) {
this.errors.phone = '请输入正确的手机号码';
this.phoneValid = false;
} else {
this.phoneValid = true;
}
}
},
// 发送验证码
async sendVerificationCode() {
if (!this.formData.phone) {
this.errors.phone = '请输入手机号';
return;
}
if (!this.isValidPhone(this.formData.phone)) {
this.errors.phone = '请输入正确的手机号码';
return;
}
this.phoneValid = true;
try {
this.codeCountdown = 60;
this.startCodeCountdown();
const response = await lotteryApi.sendSmsCode(this.formData.phone);
if (response.success) {
this.toast.success('验证码已发送,请注意查收');
} else {
this.codeCountdown = 0;
clearInterval(this.timer);
this.toast.error(response.message || '发送验证码失败,请稍后重试');
}
} catch (error) {
this.codeCountdown = 0;
clearInterval(this.timer);
console.error('发送验证码失败:', error);
this.toast.error('发送验证码失败,请稍后重试');
}
},
// 开始倒计时
startCodeCountdown() {
if (this.timer) {
clearInterval(this.timer);
}
this.timer = setInterval(() => {
if (this.codeCountdown > 0) {
this.codeCountdown--;
} else {
clearInterval(this.timer);
this.timer = null;
}
}, 1000);
},
// 表单验证
validateForm() {
this.errors = {};
if (!this.formData.phone) {
this.errors.phone = '请输入手机号';
} else if (!this.isValidPhone(this.formData.phone)) {
this.errors.phone = '请输入正确的手机号码';
}
if (!this.formData.code) {
this.errors.code = '请输入验证码';
} else if (this.formData.code.length !== 6 && this.formData.code.length !== 4) {
this.errors.code = '验证码格式不正确';
}
if (!this.formData.newPassword) {
this.errors.newPassword = '请输入新密码';
} else if (this.formData.newPassword.length < 6 || this.formData.newPassword.length > 20) {
this.errors.newPassword = '密码长度在 6 到 20 个字符';
}
if (!this.formData.confirmPassword) {
this.errors.confirmPassword = '请再次输入新密码';
} else if (this.formData.newPassword !== this.formData.confirmPassword) {
this.errors.confirmPassword = '两次输入的密码不一致';
}
return Object.keys(this.errors).length === 0;
},
// 处理重置密码
async handleResetPassword() {
if (!this.validateForm()) {
return;
}
this.loading = true;
try {
const response = await lotteryApi.resetPassword(
this.formData.phone,
this.formData.code,
this.formData.newPassword,
this.formData.confirmPassword
);
if (response.success === true) {
this.showModal = true;
this.isSuccess = true;
this.modalTitle = '密码重置成功';
this.modalMessage = '您的密码已成功重置,请返回登录页面。';
// 重置表单
this.formData.phone = '';
this.formData.code = '';
this.formData.newPassword = '';
this.formData.confirmPassword = '';
this.errors = {};
} else {
this.showModal = true;
this.isSuccess = false;
this.modalTitle = '密码重置失败';
this.modalMessage = response.message || '重置密码失败,请重试。';
}
} catch (error) {
console.error('重置密码失败:', error);
this.showModal = true;
this.isSuccess = false;
this.modalTitle = '操作失败';
this.modalMessage = error.response?.data?.message || '网络错误或服务器异常,请稍后重试。';
} finally {
this.loading = false;
}
},
// 关闭弹窗
closeModal() {
this.showModal = false;
if (this.isSuccess) {
this.router.push('/login');
}
this.modalTitle = '';
this.modalMessage = '';
}
},
// 组件销毁时清除定时器
beforeUnmount() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
}
</script>
<style scoped>
/* 页面容器 */
.reset-password-page-container {
min-height: calc(100vh - 70px);
background: #f8f9fa;
padding: 0;
}
/* 页面头部 */
.page-header {
background: url('@/assets/banner/backend1.png') center/cover no-repeat, linear-gradient(135deg, #ff6b6b, #ee5a52);
color: white;
padding: 50px 20px 40px;
text-align: center;
position: relative;
}
.page-title {
margin: 0 0 8px 0;
text-align: center;
}
.page-title h1 {
font-size: 36px;
margin: 0;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.reset-password-form-container {
padding: 0;
background: white;
margin: 0 20px;
}
.reset-password-form {
background: white;
border-radius: 0;
padding: 30px 25px;
box-shadow: none;
min-height: calc(100vh - 320px);
}
/* 表单组 */
.form-group {
margin-bottom: 24px;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
border: 2px solid #e9ecef;
border-radius: 6px;
background: #f8f9fa;
transition: all 0.3s ease;
min-height: 56px;
overflow: hidden;
}
.input-wrapper:focus-within {
border-color: #e9ecef;
background: white;
box-shadow: none;
outline: none;
}
input:focus {
outline: none;
box-shadow: none;
}
.input-wrapper.error {
border-color: #dc3545;
background: #fff5f5;
}
.input-wrapper.success {
border-color: #4caf50;
background: #f8fff8;
}
.input-icon {
padding: 12px 25px;
color: #6c757d;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
}
.icon-img {
width: 22px;
height: 22px;
object-fit: contain;
}
.form-input {
flex: 1;
padding: 16px 8px;
border: none;
outline: none;
font-size: 16px;
background: transparent;
color: #212529;
box-shadow: none;
-webkit-appearance: none;
}
.form-input:focus {
outline: none;
border: none;
box-shadow: none;
}
/* 控制浏览器自动填充的样式 */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px white inset !important;
-webkit-text-fill-color: #212529 !important;
transition: background-color 5000s ease-in-out 0s;
border: none !important;
outline: none !important;
}
/* 发送验证码按钮 */
.send-code-btn {
height: 32px;
margin-right: 5px;
padding: 0 10px;
border: none;
color: white;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border-radius: 4px;
white-space: nowrap;
min-width: 85px;
transition: all 0.3s;
background: #e53e3e;
align-self: center;
}
.code-input-wrapper {
padding-right: 5px;
border: 2px solid #e9ecef;
border-radius: 6px;
overflow: hidden;
}
.code-input-wrapper:focus-within {
border-color: #e9ecef;
box-shadow: none;
}
.send-code-btn:disabled {
background: #cccccc;
cursor: not-allowed;
}
.send-code-btn:hover:not(:disabled) {
background: #d43030;
}
/* 提示文本 */
.error-text {
color: #ff4444;
font-size: 12px;
margin-top: 5px;
margin-left: 8px;
}
.tip-text {
color: #888888;
font-size: 12px;
margin-top: 5px;
margin-left: 8px;
}
/* 隐藏浏览器自带的密码控件 */
input::-ms-reveal,
input::-ms-clear {
display: none;
}
input::-webkit-credentials-auto-fill-button {
visibility: hidden;
position: absolute;
right: 0;
}
.form-input::placeholder {
color: #9ca3af;
}
.password-toggle {
padding: 0 15px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.toggle-icon-img {
width: 22px;
height: 22px;
object-fit: contain;
}
/* 重置密码按钮 */
.reset-password-btn {
width: 100%;
padding: 18px;
background: linear-gradient(135deg, #e53e3e, #ff6b6b);
color: white;
border: none;
border-radius: 6px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin: 30px 0 25px 0;
position: relative;
overflow: hidden;
box-shadow: 0 4px 15px rgba(229, 62, 62, 0.3);
}
.reset-password-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.reset-password-btn:hover:not(:disabled)::before {
left: 100%;
}
.reset-password-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(229, 62, 62, 0.4);
}
.reset-password-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
box-shadow: 0 4px 15px rgba(229, 62, 62, 0.2);
}
/* 返回登录链接 */
.login-link {
text-align: center;
font-size: 14px;
color: #666;
}
.login-link .link {
color: #e53e3e;
text-decoration: none;
margin-left: 0px;
}
.login-link .link:hover {
text-decoration: underline;
}
/* 模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 20px;
padding: 40px 30px;
text-align: center;
max-width: 350px;
width: 100%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-30px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.success-modal h3 {
margin-bottom: 15px;
color: #4caf50;
font-size: 22px;
font-weight: bold;
}
.success-modal p {
margin-bottom: 25px;
color: #666;
line-height: 1.5;
font-size: 16px;
}
.modal-btn {
background: linear-gradient(135deg, #4caf50, #66bb6a);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
min-width: 120px;
}
.modal-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3);
}
.modal-btn:active {
transform: translateY(0);
}
.error-icon {
font-size: 48px;
margin-bottom: 20px;
animation: shake 0.6s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.error-modal h3 {
margin-bottom: 15px;
color: #f44336;
font-size: 22px;
font-weight: bold;
}
.error-modal p {
margin-bottom: 25px;
color: #666;
line-height: 1.5;
font-size: 16px;
}
.error-btn {
background: linear-gradient(135deg, #f44336, #e57373) !important;
}
.error-btn:hover {
box-shadow: 0 8px 25px rgba(244, 67, 54, 0.3) !important;
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-header {
padding: 45px 15px 35px;
}
.page-title {
margin: 0 0 8px 0;
}
.page-title h1 {
font-size: 30px;
}
.reset-password-form {
padding: 25px 20px;
}
.send-code-btn {
font-size: 12px;
min-width: 90px;
padding: 0 10px;
}
}
@media (max-width: 480px) {
.page-header {
padding: 40px 12px 30px;
}
.page-title {
margin: 0 0 6px 0;
}
.page-title h1 {
font-size: 26px;
}
.reset-password-form {
padding: 20px 15px;
}
.input-icon {
padding: 12px 25px;
}
.icon-img {
width: 22px;
height: 22px;
}
.toggle-icon-img {
width: 20px;
height: 20px;
}
.password-toggle {
padding: 0 12px;
}
.send-code-btn {
font-size: 12px;
min-width: 85px;
padding: 0 8px;
}
}
/* 桌面端样式 */
@media (min-width: 1024px) {
.page-header {
padding: 45px 20px 35px;
}
.page-title h1 {
font-size: 40px;
}
.page-subtitle {
font-size: 14px;
}
.send-code-btn {
height: 40px;
font-size: 14px;
min-width: 120px;
padding: 0 15px;
}
}
</style>

View File

@@ -0,0 +1,847 @@
<template>
<div class="surface-analysis">
<div class="header">
<!-- 返回按钮 -->
<div class="back-button-container">
<el-button @click="$router.back()" icon="ArrowLeft" size="medium">
返回
</el-button>
</div>
<h2>组合性分析</h2>
<p>球号组合分析计算组合性系数值</p>
</div>
<!-- 分析类型选择 -->
<div class="analysis-buttons">
<el-card
class="analysis-card"
:class="{ active: analysisType === 'red-red' }"
@click="selectAnalysisType('red-red')"
shadow="hover"
>
<div class="btn-icon">
<el-avatar class="red-ball-icon"></el-avatar>
<el-avatar class="red-ball-icon"></el-avatar>
</div>
<div class="btn-text">
<div class="btn-title">红球与红球</div>
<div class="btn-desc">红球组合分析</div>
</div>
</el-card>
<el-card
class="analysis-card"
:class="{ active: analysisType === 'red-blue' }"
@click="selectAnalysisType('red-blue')"
shadow="hover"
>
<div class="btn-icon">
<el-avatar class="red-ball-icon"></el-avatar>
<el-avatar class="blue-ball-icon"></el-avatar>
</div>
<div class="btn-text">
<div class="btn-title">红球与蓝球</div>
<div class="btn-desc">红蓝组合分析</div>
</div>
</el-card>
<el-card
class="analysis-card"
:class="{ active: analysisType === 'blue-red' }"
@click="selectAnalysisType('blue-red')"
shadow="hover"
>
<div class="btn-icon">
<el-avatar class="blue-ball-icon"></el-avatar>
<el-avatar class="red-ball-icon"></el-avatar>
</div>
<div class="btn-text">
<div class="btn-title">蓝球与红球</div>
<div class="btn-desc">蓝红组合分析</div>
</div>
</el-card>
</div>
<!-- 号码选择和分析区域 -->
<el-card v-if="analysisType" class="analysis-container" shadow="never">
<div class="number-selection">
<h3>{{ getSelectionTitle() }}</h3>
<!-- 主球选择 -->
<div class="ball-selection-group">
<el-divider>{{ getMasterBallLabel() }}</el-divider>
<div class="ball-grid">
<el-button
v-for="num in getMasterBallRange()"
:key="'master-' + num"
:class="{
active: masterBall === num,
'red-ball': isMasterRed(),
'blue-ball': !isMasterRed()
}"
:type="masterBall === num ? 'primary' : 'default'"
:plain="masterBall !== num"
circle
@click="selectMasterBall(num)"
>
{{ num }}
</el-button>
</div>
</div>
<!-- 随球选择 -->
<div class="ball-selection-group">
<el-divider>{{ getSlaveBallLabel() }}</el-divider>
<div class="ball-grid">
<el-button
v-for="num in getSlaveBallRange()"
:key="'slave-' + num"
:class="{
active: slaveBall === num,
'red-ball': isSlaveRed(),
'blue-ball': !isSlaveRed()
}"
:type="slaveBall === num ? 'primary' : 'default'"
:plain="slaveBall !== num"
circle
@click="selectSlaveBall(num)"
>
{{ num }}
</el-button>
</div>
</div>
<!-- 分析按钮 -->
<div class="analyze-section">
<el-button
type="success"
size="large"
round
:disabled="!canAnalyze || loading"
:loading="loading"
@click="performAnalysis"
>
{{ loading ? '分析中...' : '开始分析' }}
</el-button>
</div>
</div>
<!-- 分析结果 -->
<div v-if="result !== null || error" class="result-container">
<el-alert
v-if="error"
:title="error"
type="error"
show-icon
:closable="false"
style="margin-bottom: 20px;"
>
<template #default>
<el-button @click="clearError" type="primary" size="small">重试</el-button>
</template>
</el-alert>
<el-result
v-else-if="result !== null"
icon="success"
title="组合性分析结果"
>
<template #extra>
<div class="result-details">
<div class="table-wrapper">
<div class="table-title-row">
<div class="table-title-cell">
<span class="highlight-ball">{{masterBall}}</span>{{isMasterRed() ? '红' : '蓝'}}球与<span class="highlight-ball">{{slaveBall}}</span>{{isSlaveRed() ? '红' : '蓝'}}球组合性分析报告
</div>
</div>
<table class="analysis-table">
<tbody>
<tr>
<td class="label-cell">引用数据截至</td>
<td class="value-cell">{{resultData[0]?.latestDrawId || '-'}}</td>
</tr>
<tr>
<td class="label-cell">两号组合性系数</td>
<td class="value-cell">{{resultData[0]?.faceCoefficient || '-'}}</td>
</tr>
<tr>
<td class="label-cell">同号组最高系数球号及系数</td>
<td class="value-cell">
<span class="highlight-ball">{{resultData[0]?.highestBall || '--'}}</span>
<span class="coefficient-value">{{resultData[0]?.highestCoefficient || '-'}}</span>
</td>
</tr>
<tr>
<td class="label-cell">同号组最低系数球号及系数</td>
<td class="value-cell">
<span class="highlight-ball">{{resultData[0]?.lowestBall || '--'}}</span>
<span class="coefficient-value">{{resultData[0]?.lowestCoefficient || '-'}}</span>
</td>
</tr>
<tr>
<td class="label-cell">同号组平均系数</td>
<td class="value-cell">{{resultData[0]?.averageCoefficient || '-'}}</td>
</tr>
<tr>
<td class="label-cell">建议</td>
<td class="value-cell recommendation">
系数越高表示该此组合概率较高高于平均系数的组合更值得关注同时可尝试寻找关联组合与交叉组合的共性规律
</td>
</tr>
</tbody>
</table>
</div>
<el-button type="primary" @click="resetAnalysis">重新分析</el-button>
</div>
</template>
</el-result>
</div>
</el-card>
<!-- 使用说明 -->
<el-card v-if="!analysisType" class="instruction-container" shadow="hover">
<template #header>
<div class="card-header">
<span><el-icon><InfoFilled /></el-icon> 组合性分析说明</span>
</div>
</template>
<el-collapse accordion>
<el-collapse-item title="红球与红球分析" name="red-red">
<div class="collapse-content">
<p>分析两个红球号码之间的组合关系计算红球配对的组合系数</p>
</div>
</el-collapse-item>
<el-collapse-item title="红球与蓝球分析" name="red-blue">
<div class="collapse-content">
<p>分析红球与蓝球号码的组合关系计算红蓝配对的组合系数</p>
</div>
</el-collapse-item>
<el-collapse-item title="蓝球与红球分析" name="blue-red">
<div class="collapse-content">
<p>分析蓝球与红球号码的组合关系计算蓝红配对的组合系数</p>
</div>
</el-collapse-item>
</el-collapse>
<el-alert
type="info"
show-icon
:closable="false"
style="margin-top: 20px;"
>
<template #title>
<span>选择分析类型后依次选择主球和随球号码点击"开始分析"获取组合系数值</span>
</template>
</el-alert>
</el-card>
</div>
</template>
<script>
import { lotteryApi } from '@/api'
import {
ElButton,
ElCard,
ElAvatar,
ElDivider,
ElAlert,
ElResult,
ElTag,
ElStatistic,
ElTooltip,
ElIcon,
ElCollapse,
ElCollapseItem,
ElTable,
ElTableColumn
} from 'element-plus'
import { ArrowLeft, InfoFilled } from '@element-plus/icons-vue'
export default {
name: 'SurfaceAnalysis',
components: {
ElButton,
ElCard,
ElAvatar,
ElDivider,
ElAlert,
ElResult,
ElTag,
ElStatistic,
ElTooltip,
ElIcon,
ElCollapse,
ElCollapseItem,
ElTable,
ElTableColumn,
InfoFilled
},
data() {
return {
analysisType: '', // 'red-red', 'red-blue', 'blue-red'
masterBall: null,
slaveBall: null,
loading: false,
result: null,
error: null
}
},
computed: {
canAnalyze() {
return this.masterBall !== null && this.slaveBall !== null && this.analysisType
},
resultData() {
if (!this.result) return [];
try {
const data = typeof this.result === 'string' ? JSON.parse(this.result) : this.result;
return [{
latestDrawId: data.latestDrawId || '',
faceCoefficient: data.faceCoefficient || 0,
highestCoefficient: data.highestCoefficient || 0,
lowestCoefficient: data.lowestCoefficient || 0,
averageCoefficient: data.averageCoefficient || 0,
highestBall: data.highestBall || '-',
lowestBall: data.lowestBall || '-'
}];
} catch (e) {
console.error('解析结果数据失败', e);
return [{
latestDrawId: '',
faceCoefficient: 0,
highestCoefficient: 0,
lowestCoefficient: 0,
averageCoefficient: 0,
highestBall: '-',
lowestBall: '-'
}];
}
}
},
methods: {
selectAnalysisType(type) {
this.analysisType = type
this.masterBall = null
this.slaveBall = null
this.result = null
this.error = null
},
getSelectionTitle() {
const titles = {
'red-red': '红球与红球组合分析(不能重复)',
'red-blue': '红球与蓝球组合分析',
'blue-red': '蓝球与红球组合分析'
}
return titles[this.analysisType] || ''
},
getMasterBallLabel() {
const labels = {
'red-red': '主球(红球)',
'red-blue': '主球(红球)',
'blue-red': '主球(蓝球)'
}
return labels[this.analysisType] || ''
},
getSlaveBallLabel() {
const labels = {
'red-red': '随球(红球)',
'red-blue': '随球(蓝球)',
'blue-red': '随球(红球)'
}
return labels[this.analysisType] || ''
},
getMasterBallRange() {
if (this.analysisType === 'blue-red') {
return Array.from({ length: 16 }, (_, i) => i + 1) // 1-16
}
return Array.from({ length: 33 }, (_, i) => i + 1) // 1-33
},
getSlaveBallRange() {
if (this.analysisType === 'red-blue') {
return Array.from({ length: 16 }, (_, i) => i + 1) // 1-16
}
return Array.from({ length: 33 }, (_, i) => i + 1) // 1-33
},
isMasterRed() {
return this.analysisType === 'red-red' || this.analysisType === 'red-blue'
},
isSlaveRed() {
return this.analysisType === 'red-red' || this.analysisType === 'blue-red'
},
selectMasterBall(num) {
this.masterBall = num
this.result = null
this.error = null
},
selectSlaveBall(num) {
this.slaveBall = num
this.result = null
this.error = null
},
async performAnalysis() {
if (!this.canAnalyze) return
this.loading = true
this.error = null
this.result = null
try {
let response
switch (this.analysisType) {
case 'red-red':
response = await lotteryApi.redBallCombinationAnalysis(this.masterBall, this.slaveBall)
break
case 'red-blue':
response = await lotteryApi.redBlueCombinationAnalysis(this.masterBall, this.slaveBall)
break
case 'blue-red':
response = await lotteryApi.blueRedCombinationAnalysis(this.masterBall, this.slaveBall)
break
}
if (response.success) {
this.result = response.data
} else {
this.error = response.message || '分析失败'
}
} catch (error) {
console.error('组合系数分析失败:', error)
this.error = '网络请求失败,请重试'
} finally {
this.loading = false
}
},
clearError() {
this.error = null
},
resetAnalysis() {
this.masterBall = null
this.slaveBall = null
this.result = null
this.error = null
},
getAnalysisTitle() {
const titles = {
'red-red': '红',
'red-blue': '红',
'blue-red': '蓝'
}
return titles[this.analysisType] || ''
}
}
}
</script>
<style scoped>
.surface-analysis {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
position: relative;
}
/* 返回按钮 */
.back-button-container {
position: absolute;
left: 20px;
top: 0;
}
.header h2 {
color: #2c3e50;
font-size: 28px;
margin-bottom: 10px;
font-weight: 700;
}
.header p {
color: #7f8c8d;
font-size: 16px;
}
/* 分析类型卡片 */
.analysis-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.analysis-card {
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
}
.analysis-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.15);
border-color: #3498db;
}
.analysis-card.active {
background-color: #eaf5ff;
border-color: #3498db;
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.2);
}
.analysis-card.active :deep(.el-card__body) {
background: transparent;
color: #303133;
}
.btn-icon {
display: flex;
gap: 5px;
margin-bottom: 10px;
}
.red-ball-icon {
background-color: #e74c3c !important;
}
.blue-ball-icon {
background-color: #3498db !important;
}
.btn-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.btn-desc {
font-size: 14px;
opacity: 0.8;
}
.analysis-card.active .btn-title {
color: #2980b9;
}
.analysis-card.active .btn-desc {
color: #3498db;
}
/* 分析容器 */
.analysis-container {
margin-bottom: 30px;
}
.number-selection {
padding: 20px 0;
}
.number-selection h3 {
color: #2c3e50;
margin-bottom: 20px;
font-size: 20px;
text-align: center;
}
.ball-selection-group {
margin-bottom: 25px;
}
/* 球号网格 */
.ball-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 15px;
padding: 0 15px;
}
/* 红球和蓝球按钮样式 */
:deep(.el-button.red-ball.is-plain:not(.is-disabled)) {
color: #e74c3c;
border-color: #e74c3c;
background-color: #fff5f5;
}
:deep(.el-button.red-ball:not(.is-plain):not(.is-disabled)) {
background-color: #e74c3c;
border-color: #c0392b;
}
:deep(.el-button.blue-ball.is-plain:not(.is-disabled)) {
color: #3498db;
border-color: #3498db;
background-color: #f0f8ff;
}
:deep(.el-button.blue-ball:not(.is-plain):not(.is-disabled)) {
background-color: #3498db;
border-color: #2980b9;
}
/* 分析按钮 */
.analyze-section {
text-align: center;
margin-top: 30px;
}
/* 结果容器 */
.result-container {
border-top: 2px solid #ecf0f1;
padding: 20px;
background: #f8f9fa;
}
:deep(.el-result) {
padding: 0px;
}
.result-details {
width: 100%;
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.ball-combo {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 15px 0;
}
.combo-label {
font-size: 16px;
color: #2c3e50;
font-weight: 600;
}
.combo-separator {
font-size: 20px;
color: #7f8c8d;
font-weight: bold;
}
.ball-tag {
padding: 8px 12px;
font-weight: 700;
font-size: 16px;
border-radius: 50%;
min-width: 40px;
}
.coefficient-result {
margin: 20px 0;
}
:deep(.el-statistic__head) {
margin-bottom: 10px;
font-size: 16px;
}
:deep(.el-statistic__content) {
font-size: 24px !important;
color: #e74c3c;
}
/* 使用说明 */
.instruction-container {
max-width: 800px;
margin: 0 auto;
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
}
.collapse-content {
padding: 10px;
color: #7f8c8d;
}
.table-wrapper {
width: 100%;
max-width: 500px;
margin: 0 auto 20px;
border-collapse: collapse;
}
.table-title-row {
width: 100%;
text-align: center;
border: 1px solid #000;
}
.table-title-cell {
padding: 8px;
font-weight: normal;
font-size: 15px;
color: #000;
background-color: #fff;
border-bottom: 1px solid #000;
}
.analysis-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #000;
border-top: none;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
.analysis-table tr {
border-bottom: 1px solid #000;
}
.analysis-table tr:last-child {
border-bottom: none;
}
.label-cell {
width: 150px;
padding: 8px;
text-align: center;
font-weight: normal;
background-color: #f5f5f5;
border-right: 1px solid #000;
vertical-align: middle;
}
.value-cell {
padding: 8px;
text-align: center;
vertical-align: middle;
}
.value-cell .ball-number {
display: inline-block;
min-width: 30px;
text-align: right;
}
.value-cell .highlight-ball {
display: inline-block;
min-width: 30px;
text-align: left;
color: #e74c3c; /* 红色文字 */
font-weight: bold;
margin-right: 8px;
}
.highlight-ball {
color: #e74c3c; /* 红色文字 */
font-weight: normal;
}
.coefficient-value {
font-weight: normal;
color: #333;
}
.value-cell .coefficient-value {
text-align: left;
display: inline-block;
}
.recommendation {
font-size: 13px;
line-height: 1.5;
color: #333;
text-align: left;
padding: 5px;
white-space: normal;
font-weight: normal;
}
.report-title {
font-size: 18px;
font-weight: bold;
color: #303133;
margin: 20px 0 15px;
text-align: center;
padding-bottom: 10px;
width: 100%;
max-width: 600px;
display: none; /* 隐藏原来的标题,使用表格自身的标题 */
}
/* 响应式设计 */
@media (max-width: 768px) {
.surface-analysis {
padding: 15px;
}
.back-button-container {
position: static;
text-align: left;
margin-bottom: 15px;
}
.header {
margin-bottom: 20px;
}
.analysis-buttons {
grid-template-columns: 1fr;
}
.ball-grid {
gap: 8px;
padding: 0;
}
.table-wrapper {
max-width: 100%;
}
.table-title-cell {
font-size: 14px;
padding: 6px;
}
.label-cell, .value-cell {
padding: 6px;
font-size: 13px;
}
.recommendation {
font-size: 12px;
line-height: 1.4;
}
}
</style>

View File

@@ -0,0 +1,420 @@
<template>
<div class="table-analysis-container">
<!-- 工具栏 -->
<div class="toolbar">
<el-button @click="goBack" type="default" size="medium" icon="ArrowLeft">
返回
</el-button>
</div>
<div class="header">
<h1>表相性分析</h1>
<p>基于最新100期开奖数据的表相性分析</p>
</div>
<!-- 加载状态 -->
<el-card v-if="loading" class="loading-container" shadow="never">
<el-skeleton :rows="10" animated />
</el-card>
<!-- 错误状态 -->
<el-card v-else-if="error" class="error-container" shadow="never">
<el-alert
:title="error"
type="error"
show-icon
:closable="false"
style="margin-bottom: 20px;"
/>
<el-button @click="fetchData" type="primary">重新加载</el-button>
</el-card>
<!-- 数据表格 -->
<el-card v-else-if="drawsData.length > 0" class="table-container" shadow="never">
<div class="table-wrapper">
<el-table
:data="drawsData"
border
style="width: 100%"
:highlight-current-row="true"
@current-change="handleCurrentChange"
height="calc(100vh - 150px)"
>
<el-table-column
prop="drawId"
label="期号"
width="80"
fixed="left"
align="center"
/>
<!-- 红球列 -->
<el-table-column label="红球" align="center">
<el-table-column
v-for="num in 33"
:key="`red-${num}`"
:label="formatNumberLabel(num)"
width="40"
align="center"
>
<template #default="scope">
<div v-if="isRedBallHit(scope.row, num)" class="ball red-ball" :class="{'double-digits': num >= 10}">
{{ num }}
</div>
</template>
</el-table-column>
</el-table-column>
<!-- 蓝球列 -->
<el-table-column label="蓝球" align="center">
<el-table-column
v-for="num in 16"
:key="`blue-${num}`"
:label="formatNumberLabel(num)"
width="40"
align="center"
>
<template #default="scope">
<div v-if="isBlueBallHit(scope.row, num)" class="ball blue-ball" :class="{'double-digits': num >= 10}">
{{ num }}
</div>
</template>
</el-table-column>
</el-table-column>
</el-table>
</div>
</el-card>
<!-- 无数据状态 -->
<el-card v-else class="no-data-container" shadow="never">
<el-empty description="暂无开奖数据" />
</el-card>
</div>
</template>
<script>
import { lotteryApi } from '../api/index.js'
import {
ElButton,
ElCard,
ElSkeleton,
ElAlert,
ElTable,
ElTableColumn,
ElEmpty
} from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
export default {
name: 'TableAnalysis',
components: {
ElButton,
ElCard,
ElSkeleton,
ElAlert,
ElTable,
ElTableColumn,
ElEmpty
},
data() {
return {
drawsData: [],
loading: false,
error: null,
selectedRowIndex: null // 选中的行索引
}
},
async mounted() {
await this.fetchData()
},
methods: {
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 选中行
handleCurrentChange(row) {
this.selectedRowIndex = row ? this.drawsData.indexOf(row) : null
},
// 获取数据
async fetchData() {
this.loading = true
this.error = null
try {
console.log('开始获取最新100期开奖数据')
const response = await lotteryApi.getRecent100Draws()
if (response && response.success && response.data) {
this.drawsData = response.data
console.log('获取开奖数据成功,共', this.drawsData.length, '期')
console.log('第一条数据示例:', this.drawsData[0])
if (this.drawsData.length > 0) {
console.log('红球数据:', this.getRedBalls(this.drawsData[0]))
console.log('蓝球数据:', this.drawsData[0].blueBall)
}
} else {
this.error = response?.message || '获取数据失败'
console.error('获取数据失败:', response)
}
} catch (error) {
console.error('获取开奖数据出错:', error)
this.error = error?.response?.data?.message || '网络错误,请稍后重试'
// 添加测试数据以便调试
this.drawsData = [
{
id: 1,
drawId: '2025101',
drawPeriod: '2025101',
drawDate: '2025-01-01',
redBall1: 1,
redBall2: 2,
redBall3: 3,
redBall4: 4,
redBall5: 5,
redBall6: 6,
blueBall: 7
},
{
id: 2,
drawId: '2025100',
drawPeriod: '2025100',
drawDate: '2025-01-02',
redBall1: 6,
redBall2: 9,
redBall3: 10,
redBall4: 13,
redBall5: 30,
redBall6: 33,
blueBall: 7
}
]
console.log('使用测试数据:', this.drawsData)
} finally {
this.loading = false
}
},
// 获取红球数组
getRedBalls(draw) {
const balls = []
if (draw.redBall1) balls.push(parseInt(draw.redBall1))
if (draw.redBall2) balls.push(parseInt(draw.redBall2))
if (draw.redBall3) balls.push(parseInt(draw.redBall3))
if (draw.redBall4) balls.push(parseInt(draw.redBall4))
if (draw.redBall5) balls.push(parseInt(draw.redBall5))
if (draw.redBall6) balls.push(parseInt(draw.redBall6))
return balls
},
// 判断红球是否命中
isRedBallHit(draw, num) {
const redBalls = this.getRedBalls(draw)
return redBalls.includes(num)
},
// 判断蓝球是否命中
isBlueBallHit(draw, num) {
return parseInt(draw.blueBall) === num
},
// 格式化数字标签,确保两位数显示在一行
formatNumberLabel(num) {
// 直接返回数字CSS会处理双位数的显示
return num;
}
}
}
</script>
<style scoped>
.table-analysis-container {
min-height: calc(100vh - 70px);
background: #f8f9fa;
padding: 10px;
}
/* 工具栏样式 */
.toolbar {
margin-bottom: 15px;
}
.header {
text-align: center;
margin-bottom: 15px;
}
.header h1 {
color: #333;
font-size: 24px;
margin-bottom: 5px;
}
.header p {
color: #666;
font-size: 14px;
}
/* 加载状态 */
.loading-container {
margin-bottom: 20px;
}
/* 错误状态 */
.error-container {
margin-bottom: 20px;
text-align: center;
padding: 20px;
}
/* 表格容器 */
.table-container {
margin-bottom: 20px;
}
.table-wrapper {
overflow-x: auto;
}
/* 自定义表格样式 */
:deep(.el-table) {
--el-table-header-bg-color: #f5f7fa;
--el-table-header-text-color: #333;
--el-table-row-hover-bg-color: #ecf5ff;
}
:deep(.el-table__header) th {
padding: 2px 0;
height: 32px;
font-size: 12px; /* 减小表头字体大小 */
}
:deep(.el-table__header .cell) {
padding: 0 !important; /* 移除单元格内边距 */
white-space: nowrap;
line-height: 1;
}
/* 对两位数的表头标签应用特殊样式 */
:deep(.el-table__header th[class*="is-leaf"]:nth-child(n+11)) .cell {
font-size: 11px; /* 缩小两位数字体 */
letter-spacing: -0.5px; /* 减少字母间距 */
}
:deep(.el-table__body) td {
padding: 4px 0;
height: 34px;
}
:deep(.el-table--border .el-table__cell) {
border-right: 1px solid #ebeef5;
}
:deep(.el-table__fixed-right) {
height: 100%;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.08);
}
/* 无数据状态 */
.no-data-container {
margin-bottom: 20px;
padding: 40px;
text-align: center;
}
/* 球体样式 */
.ball {
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
font-size: 10px;
margin: 0 auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
/* 两位数的球使用更小字体 */
.double-digits {
font-size: 9px;
letter-spacing: -0.5px;
}
/* 红球样式 */
.red-ball {
background: linear-gradient(135deg, #ff5252 0%, #d32f2f 50%, #b71c1c 100%);
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
border: 1px solid #b71c1c;
}
.red-ball::before {
content: '';
position: absolute;
top: 2px;
left: 3px;
width: 6px;
height: 6px;
background: rgba(255, 255, 255, 0.4);
border-radius: 50%;
opacity: 0.8;
}
/* 蓝球样式 */
.blue-ball {
background: linear-gradient(135deg, #42a5f5 0%, #1976d2 50%, #0d47a1 100%);
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
border: 1px solid #0d47a1;
}
.blue-ball::before {
content: '';
position: absolute;
top: 2px;
left: 3px;
width: 6px;
height: 6px;
background: rgba(255, 255, 255, 0.4);
border-radius: 50%;
opacity: 0.8;
}
/* 响应式设计 */
@media (max-width: 768px) {
.table-analysis-container {
padding: 5px;
}
.header h1 {
font-size: 20px;
}
.header p {
font-size: 12px;
}
.toolbar {
margin-bottom: 10px;
}
/* 在移动端调整球体大小 */
.ball {
width: 20px;
height: 20px;
font-size: 9px;
}
.ball::before {
width: 4px;
height: 4px;
}
}
</style>

View File

@@ -0,0 +1,666 @@
<template>
<div class="trend-analysis">
<div class="header">
<!-- 返回按钮 -->
<div class="back-button-container">
<el-button @click="$router.back()" icon="ArrowLeft" size="medium">
返回
</el-button>
</div>
<h2>活跃性分析</h2>
<p>查看红球和蓝球历史数据统计</p>
<!-- 球类选择切换 -->
<div class="ball-type-tabs">
<el-radio-group v-model="ballType" @change="switchBallType">
<el-radio-button label="red">🔴 红球</el-radio-button>
<el-radio-button label="blue">🔵 蓝球</el-radio-button>
</el-radio-group>
</div>
</div>
<!-- 功能按钮区域 -->
<div class="analysis-buttons">
<el-card
class="analysis-card"
:class="{ active: currentView === 'all' }"
@click="loadData('all')"
shadow="hover"
>
<div class="btn-icon">📊</div>
<div class="btn-text">
<div class="btn-title">历史数据</div>
<div class="btn-desc">查看所有历史数据</div>
</div>
</el-card>
<el-card
class="analysis-card"
:class="{ active: currentView === 'top' }"
@click="loadData('top')"
shadow="hover"
>
<div class="btn-icon">🏆</div>
<div class="btn-text">
<div class="btn-title">历史排行</div>
<div class="btn-desc">历史数据排行榜</div>
</div>
</el-card>
<el-card
class="analysis-card"
:class="{ active: currentView === '100' }"
@click="loadData('100')"
shadow="hover"
>
<div class="btn-icon">📈</div>
<div class="btn-text">
<div class="btn-title">百期数据</div>
<div class="btn-desc">最近100期数据</div>
</div>
</el-card>
<el-card
class="analysis-card"
:class="{ active: currentView === 'top100' }"
@click="loadData('top100')"
shadow="hover"
>
<div class="btn-icon">🎯</div>
<div class="btn-text">
<div class="btn-title">百期排行</div>
<div class="btn-desc">最近100期数据排行</div>
</div>
</el-card>
</div>
<!-- 数据显示区域 -->
<el-card class="data-container" shadow="never">
<div v-if="loading" class="loading">
<el-skeleton :rows="10" animated />
</div>
<el-alert
v-else-if="error"
:title="error"
type="error"
show-icon
:closable="false"
style="margin-bottom: 20px;"
>
<template #default>
<el-button @click="retryLoad" type="primary" size="small">重试</el-button>
</template>
</el-alert>
<div v-else-if="tableData.length > 0" class="data-table-container">
<div class="table-header">
<h3>{{ getTableTitle() }}</h3>
<div class="table-info">
<span> {{ tableData.length }} 条记录</span>
</div>
</div>
<el-table :data="paginatedData" stripe style="width: 100%">
<el-table-column
v-for="column in getTableColumns()"
:key="column.key"
:prop="column.key"
:label="column.title"
>
<template #default="scope">
<span :class="getColumnClass(column.key, scope.row[column.key])">
{{ formatValue(column.key, scope.row[column.key], scope.$index) }}
</span>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination" v-if="totalPages > 1">
<el-pagination
background
layout="prev, pager, next"
:total="tableData.length"
:page-size="pageSize"
v-model:current-page="currentPage"
/>
</div>
</div>
<el-empty v-else-if="!loading" description="暂无数据" />
</el-card>
</div>
</template>
<script>
import { lotteryApi } from '@/api'
import {
ElButton,
ElCard,
ElRadioGroup,
ElRadioButton,
ElSkeleton,
ElAlert,
ElTable,
ElTableColumn,
ElPagination,
ElEmpty
} from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
export default {
name: 'TrendAnalysis',
components: {
ElButton,
ElCard,
ElRadioGroup,
ElRadioButton,
ElSkeleton,
ElAlert,
ElTable,
ElTableColumn,
ElPagination,
ElEmpty
},
data() {
return {
loading: false,
error: null,
currentView: '',
ballType: 'red', // 'red' 或 'blue'
tableData: [],
currentPage: 1,
pageSize: 20
}
},
computed: {
totalPages() {
return Math.ceil(this.tableData.length / this.pageSize)
},
paginatedData() {
const start = (this.currentPage - 1) * this.pageSize
return this.tableData.slice(start, start + this.pageSize)
}
},
methods: {
async loadHistoryAll() {
this.currentView = 'all'
this.currentPage = 1
this.loading = true
this.error = null
try {
const response = await lotteryApi.getHistoryAll()
if (response.success) {
this.tableData = response.data || []
} else {
this.error = response.message || '获取数据失败'
}
} catch (error) {
console.error('获取历史全部记录失败:', error)
this.error = '网络请求失败,请重试'
} finally {
this.loading = false
}
},
async loadHistory100() {
this.currentView = '100'
this.currentPage = 1
this.loading = true
this.error = null
try {
const response = await lotteryApi.getHistory100()
if (response.success) {
this.tableData = response.data || []
} else {
this.error = response.message || '获取数据失败'
}
} catch (error) {
console.error('获取最近100期记录失败:', error)
this.error = '网络请求失败,请重试'
} finally {
this.loading = false
}
},
async loadHistoryTop() {
this.currentView = 'top'
this.currentPage = 1
this.loading = true
this.error = null
try {
const response = await lotteryApi.getHistoryTop()
if (response.success) {
this.tableData = response.data || []
} else {
this.error = response.message || '获取数据失败'
}
} catch (error) {
console.error('获取历史排行记录失败:', error)
this.error = '网络请求失败,请重试'
} finally {
this.loading = false
}
},
async loadHistoryTop100() {
this.currentView = 'top100'
this.currentPage = 1
this.loading = true
this.error = null
try {
const response = await lotteryApi.getHistoryTop100()
if (response.success) {
this.tableData = response.data || []
} else {
this.error = response.message || '获取数据失败'
}
} catch (error) {
console.error('获取100期排行记录失败:', error)
this.error = '网络请求失败,请重试'
} finally {
this.loading = false
}
},
// 蓝球数据获取方法
async loadBlueHistoryAll() {
this.currentView = 'all'
this.currentPage = 1
this.loading = true
this.error = null
try {
const response = await lotteryApi.getBlueHistoryAll()
if (response.success) {
this.tableData = response.data || []
} else {
this.error = response.message || '获取数据失败'
}
} catch (error) {
console.error('获取蓝球历史全部记录失败:', error)
this.error = '网络请求失败,请重试'
} finally {
this.loading = false
}
},
async loadBlueHistory100() {
this.currentView = '100'
this.currentPage = 1
this.loading = true
this.error = null
try {
const response = await lotteryApi.getBlueHistory100()
if (response.success) {
this.tableData = response.data || []
} else {
this.error = response.message || '获取数据失败'
}
} catch (error) {
console.error('获取蓝球最近100期记录失败:', error)
this.error = '网络请求失败,请重试'
} finally {
this.loading = false
}
},
async loadBlueHistoryTop() {
this.currentView = 'top'
this.currentPage = 1
this.loading = true
this.error = null
try {
const response = await lotteryApi.getBlueHistoryTop()
if (response.success) {
this.tableData = response.data || []
} else {
this.error = response.message || '获取数据失败'
}
} catch (error) {
console.error('获取蓝球历史排行记录失败:', error)
this.error = '网络请求失败,请重试'
} finally {
this.loading = false
}
},
async loadBlueHistoryTop100() {
this.currentView = 'top100'
this.currentPage = 1
this.loading = true
this.error = null
try {
const response = await lotteryApi.getBlueHistoryTop100()
if (response.success) {
this.tableData = response.data || []
} else {
this.error = response.message || '获取数据失败'
}
} catch (error) {
console.error('获取蓝球100期排行记录失败:', error)
this.error = '网络请求失败,请重试'
} finally {
this.loading = false
}
},
// 切换球类型
switchBallType() {
this.currentView = ''
this.tableData = []
this.currentPage = 1
this.error = null
},
// 统一的数据加载方法
loadData(viewType) {
if (this.ballType === 'red') {
switch (viewType) {
case 'all':
this.loadHistoryAll()
break
case '100':
this.loadHistory100()
break
case 'top':
this.loadHistoryTop()
break
case 'top100':
this.loadHistoryTop100()
break
}
} else {
switch (viewType) {
case 'all':
this.loadBlueHistoryAll()
break
case '100':
this.loadBlueHistory100()
break
case 'top':
this.loadBlueHistoryTop()
break
case 'top100':
this.loadBlueHistoryTop100()
break
}
}
},
getTableTitle() {
const ballTypeText = this.ballType === 'red' ? '红球' : '蓝球'
const titles = {
'all': `${ballTypeText}历史全部记录`,
'100': `${ballTypeText}最近100期数据`,
'top': `${ballTypeText}历史数据排行`,
'top100': `${ballTypeText}100期数据排行`
}
return titles[this.currentView] || '数据列表'
},
getTableColumns() {
// 根据不同的视图返回不同的列配置
if (this.currentView === 'all') {
// 历史全部记录 - 显示完整数据
return [
{ key: 'ballNumber', title: '球号' },
{ key: 'frequencyCount', title: '出现次数' },
{ key: 'frequencyPercentage', title: '出现频率(%)' },
{ key: 'averageInterval', title: '平均隐现期' },
{ key: 'maxHiddenInterval', title: '最长隐现期' },
{ key: 'maxConsecutiveCount', title: '最大连出期' },
{ key: 'pointCoefficient', title: '活跃系数' }
]
} else if (this.currentView === '100') {
// 最近100期 - 只显示有数据的列
return [
{ key: 'ballNumber', title: '球号' },
{ key: 'frequencyCount', title: '出现次数' },
{ key: 'averageInterval', title: '平均隐现期' },
{ key: 'maxConsecutiveCount', title: '最大连出期' },
{ key: 'pointCoefficient', title: '活跃系数' }
]
} else if (this.currentView === 'top' || this.currentView === 'top100') {
return [
{ key: 'no', title: '排名' },
{ key: 'ballNumber', title: '球号' },
{ key: 'pointCoefficient', title: '活跃系数' }
]
}
return []
},
getColumnClass(key, value) {
if (key === 'ballNumber') {
return this.ballType === 'red' ? 'ball-number red-ball' : 'ball-number blue-ball'
} else if (key === 'pointCoefficient') {
return 'point-coefficient'
} else if (key === 'no') {
return 'ranking'
}
return ''
},
formatValue(key, value, index) {
if (key === 'no') {
// 排名显示为连续数字(当前页面索引 + 全局偏移)
return (this.currentPage - 1) * this.pageSize + index + 1
} else if (key === 'frequencyPercentage' && typeof value === 'number') {
return value.toFixed(2)
}
return value
},
retryLoad() {
if (this.currentView) {
this.loadData(this.currentView)
}
},
prevPage() {
if (this.currentPage > 1) {
this.currentPage--
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
}
}
}
}
</script>
<style scoped>
.trend-analysis {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
position: relative;
}
.back-button-container {
position: absolute;
left: 20px;
top: 0;
}
.header h2 {
color: #2c3e50;
font-size: 28px;
margin-bottom: 10px;
font-weight: 700;
}
.header p {
color: #7f8c8d;
font-size: 16px;
}
.ball-type-tabs {
margin-top: 20px;
}
.analysis-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
.analysis-card {
cursor: pointer;
transition: all 0.3s ease;
}
.analysis-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.15);
border-color: #3498db;
}
.analysis-card.active {
background-color: #eaf5ff;
border-color: #3498db;
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.2);
}
.analysis-card.active :deep(.el-card__body) {
background: transparent;
color: #303133;
}
.btn-icon {
font-size: 32px;
min-width: 40px;
text-align: center;
margin-bottom: 10px;
}
.btn-text {
text-align: center;
}
.btn-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.analysis-card.active .btn-title {
color: #2980b9;
}
.btn-desc {
font-size: 14px;
opacity: 0.8;
}
.analysis-card.active .btn-desc {
color: #3498db;
}
.data-container {
overflow: hidden;
}
.loading {
padding: 40px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #ebeef5;
}
.table-header h3 {
margin: 0;
font-size: 18px;
}
.table-info {
font-size: 14px;
color: #909399;
}
.pagination {
display: flex;
justify-content: center;
padding: 20px;
}
.ball-number {
color: white;
padding: 4px 8px;
border-radius: 50%;
font-weight: 700;
font-size: 14px;
min-width: 30px;
text-align: center;
display: inline-block;
}
.red-ball {
background: #e74c3c;
}
.blue-ball {
background: #3498db;
}
.point-coefficient {
color: #e74c3c;
font-weight: 700;
}
.ranking {
background: #f39c12;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-weight: 700;
display: inline-block;
}
@media (max-width: 768px) {
.trend-analysis {
padding: 15px;
}
.back-button-container {
position: static;
text-align: left;
margin-bottom: 15px;
}
.header {
margin-bottom: 20px;
}
.analysis-buttons {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div class="user-agreement-page">
<el-page-header @back="goBack" content="用户协议">
<template #title>
<span>返回</span>
</template>
</el-page-header>
<el-card class="agreement-content-card">
<div class="agreement-content">
<h4>欢迎使用彩票猪手数据服务</h4>
<p>&emsp;&emsp;在开始使用我们的服务之前请您用户仔细阅读并充分理解本用户服务协议以下简称 "本协议"的全部内容本协议是您用户与西安精彩数据服务社之间关于使用本服务的法律协议一旦您用户使用本服务即表示您用户已同意接受本协议的约束如果您用户不同意本协议的任何条款请不要使用本服务</p>
<h5>定义</h5>
<p>1本服务指我们通过网站应用程序或其他相关平台向您提供的彩票猪手数据服务包括但不限于信息发布数据存储在线交流等</p>
<p>2用户指承认本协议接受本服务的自然人法人或其他组织具体包含付费账号用户和体验账号用户</p>
<p>3个人信息以电子或者其他方式记录的与已识别或者可识别的自然人有关的各种信息不包括匿名化处理后的信息</p>
<p>4知识产权包括但不限于著作权专利权商标权商业秘密等</p>
<h5>服务内容及门槛</h5>
<p>1我们将尽力为您用户提供稳定高效的服务服务内容包括但不限于</p>
<p class="indent">1.1既定彩票数据查询浏览</p>
<p class="indent">1.2既定彩票数据分析报告</p>
<p class="indent">1.3个性化开奖号码辅助推导推测</p>
<p class="indent">1.4推测记录统计备份</p>
<p class="indent">1.5彩票猪手AI互动交流服务</p>
<p>2我们会根据实际情况对服务内容进行调整更新或终止如有重大变更我们将通过企业公众号通知您用户需要您用户及时接收如您用户在服务内容发生变更后继续使用本服务视为您用户接受变更后的协议内容</p>
<p>3用户理解并同意使用本服务可能需要您用户具备相应的设备软件网络环境和一定的专业知识相关费用由您用户自行承担</p>
<h5>用户账号</h5>
<p>1用户可以通过注册或使用第三方账号登录的方式获取本服务账号在注册过程中用户需提供真实准确完整的信息并确保信息的及时更新</p>
<p>2用户应妥善保管账号及密码不得将账号出借转让赠与或共享给他人使用因您用户自身原因导致账号泄露或被他人使用的后果由您用户自行承担</p>
<p>3用户您发现账号存在异常或安全问题请立即通知我们我们将尽力协助处理但对于非因我们原因导致的损失我们不承担责任</p>
<p>4用户在使用账号过程中应遵守法律法规及本协议约定不得利用账号从事违法违规或损害他人权益的行为否则我们有权暂停或终止您用户的账号使用由此造成的损失您用户自行承担</p>
<h5>用户权利与义务</h5>
<p>1权利</p>
<p class="indent">1.1有权在遵守本协议的前提下按照我们提供的方式使用服务</p>
<p class="indent">1.2对我们提供的服务质量有权提出合理的意见和建议</p>
<p>2义务</p>
<p class="indent">2.1遵守国家法律法规及互联网相关规定不得利用本服务从事违法犯罪活动</p>
<p class="indent">2.2不得干扰破坏本服务的正常运行不得对服务进行反向工程反编译反汇编等行为</p>
<p class="indent">2.3不得发布传播任何侵犯他人知识产权隐私或其他合法权益的信息</p>
<p class="indent">2.4不得恶意注册账号发送垃圾信息或进行其他滥用服务的行为</p>
<p class="indent">2.5如因您用户的行为导致我们或第三方遭受损失用户应承担相应的赔偿责任</p>
<h5>隐私政策</h5>
<p>1我们十分重视对您用户个人信息的保护将谨慎收集安全存储妥善使用您用户的个人信息</p>
<p>2用户同意我们为提供服务改进服务包括遵守法律法规的需要对您用户的个人信息进行合理的调用过程中我们将采取合理措施确保信息安全</p>
<h5>知识产权</h5>
<p>我们对本服务及相关内容包括但不限于软件文字图片音频视频等享有知识产权未经我们书面许可用户不得擅自复制改编或创造基于本服务的衍生品</p>
<h5>责任限制与免责</h5>
<p>1本服务所提供的数据分析报告均为根据彩票历史数据统计学和数学原理通过计算技术进行研究和建模而得出我们可以对数据的及时性客观性承担责任但对任何经过主观干预之后而产生的数据结果无法承当相应的后果</p>
<p>2用户应当理解并知晓彩票娱乐本身就是概率游戏彩票开奖也是最为典型的随机事件本服务仅具备参考功能会助您用户有限地缩小关注重点用户必须对自己的选择和决策承担最终责任</p>
<p>3我们将尽力确保服务的正常运行但由于互联网的复杂性和不确定性可能会出现服务中断延迟错误等情况对于因不可抗力技术故障网络攻击等不可预见不可避免的原因导致的服务问题我们不承担责任</p>
<p>4我们对您用户通过本服务获取的第三方信息的准确性完整性可靠性不承担保证责任您应自行判断并承担使用风险</p>
<p>5在任何情况下我们对您用户因使用本服务而产生的直接间接偶然特殊或后果性损失包括但不限于数据丢失业务中断利润损失等无论基于合同侵权或其他理论均不承担超过您用户实际支付的服务费用的赔偿责任</p>
<h5>收费及其规则</h5>
<p>1本服务采用会员制运营模式所有用户分为会员与非会员两种类型</p>
<p>2本服务对非会员用户提供1次为期连续10天的免费体验服务免费时限结束后未转入会员的用户将无法继续获得本服务而且以后该非会员用户将再无免费体验服务的机会</p>
<p>3本服务对会员用户实行付费服务付费标准分为包月付费和包年付费两种</p>
<p>4包月付费每月10元不足一个月时按一个月计算包年付费每年100元期间任何一方提出终止协议和退费要求时所退款项均按包月付费标准进行折算四舍五入</p>
<p>5出现业务退款情形时所退款项只限原路退回付款账号</p>
<p>6所有用户在服务有效期内使用本服务的时段和频次均不受限</p>
<p>7用户可以根据自己的实际需要随时选择成为会员或非会员</p>
<h5>协议变更与终止</h5>
<p>1我们有权根据法律法规变化业务发展需要等对本协议进行变更变更后的协议将在相关平台公布公布后即视为已通知您用户若您用户在协议变更后继续使用服务视为您用户接受变更后的协议若您用户不同意变更有权停止使用本服务</p>
<p>2出现以下情况下我们有权终止本协议及停止您用户继续使用服务</p>
<p class="indent">2.1用户严重违反本协议约定</p>
<p class="indent">2.2法律法规要求我们终止服务</p>
<p class="indent">2.3因不可抗力等不可预见不可避免的原因导致服务无法继续提供</p>
<p>2.4协议终止后我们有权根据法律法规要求对您用户的相关信息进行处理</p>
<h5>争议解决</h5>
<p>1本协议的签订履行解释及争议解决均适用 中华人民共和国民法典</p>
<p>2如双方在本协议履行过程中发生争议应首先通过友好协商解决协商不成的任何一方均有权向有管辖权的人民法院提起诉讼</p>
<h5>十一其他条款</h5>
<p>1本协议构成您用户与我们之间关于本服务的完整协议未经我们书面同意用户不得转让本协议项下的任何权利利益和义务</p>
<p>2本协议各条款的标题仅为方便阅读而设不影响条款的具体含义及解释</p>
<p>3若本协议任何条款被认定为无效或不可执行不影响其他条款的效力及执行</p>
<p>4我们未行使或执行本协议任何权利或条款不构成对该权利或条款的放弃</p>
<p>5本协议自您用户成功注册彩票猪手服务相关账号之日即刻生效</p>
<p>6任何有关本协议项下服务的问题用户可通过本企业微信号进行咨询</p>
</div>
</el-card>
</div>
</template>
<script>
import { ElPageHeader, ElCard } from 'element-plus'
export default {
name: 'UserAgreement',
components: {
ElPageHeader,
ElCard
},
methods: {
goBack() {
this.$router.go(-1)
}
}
}
</script>
<style scoped>
.user-agreement-page {
padding: 20px;
background-color: #f0f2f5;
}
/* 自定义返回按钮样式 */
:deep(.el-page-header__header) {
background: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
:deep(.el-page-header__back) {
color: #409EFF !important;
font-weight: 600;
font-size: 16px;
}
:deep(.el-page-header__back:hover) {
color: #66b1ff !important;
}
:deep(.el-page-header__content) {
color: #333 !important;
font-weight: 600;
font-size: 18px;
}
.agreement-content-card {
margin-top: 20px;
}
.agreement-content-card h4 {
text-align: center;
margin-bottom: 20px;
}
.agreement-content-card h5 {
margin-top: 20px;
margin-bottom: 10px;
}
.agreement-content {
max-width: 100%;
margin: 0 auto;
}
.agreement-content h4 {
text-align: center;
margin-bottom: 20px;
color: #333;
font-size: 18px;
font-weight: 600;
}
.agreement-content h5 {
margin-top: 25px;
margin-bottom: 15px;
color: #333;
font-size: 16px;
font-weight: 600;
border-left: 3px solid #409EFF;
padding-left: 12px;
}
.agreement-content p {
line-height: 1.8;
color: #666;
margin-bottom: 12px;
font-size: 14px;
text-align: justify;
}
.agreement-content p.indent {
text-indent: 2em;
margin-left: 20px;
margin-bottom: 8px;
}
.agreement-content p:last-child {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,11 @@
<template>
</template>
<script>
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,422 @@
<template>
<div class="admin-login">
<div class="login-container">
<!-- 左侧装饰区域 -->
<div class="login-banner">
<div class="banner-content">
<div class="logo">
<img src="/favicon.ico" alt="Logo" />
<h1>彩票推测系统</h1>
</div>
<div class="banner-text">
<h2>后台管理系统</h2>
<p>专业的数据管理与用户服务</p>
</div>
<div class="banner-features">
<div class="feature-item">
<el-icon><User /></el-icon>
<span>用户管理</span>
</div>
<div class="feature-item">
<el-icon><Key /></el-icon>
<span>会员码管理</span>
</div>
<div class="feature-item">
<el-icon><Document /></el-icon>
<span>数据导入</span>
</div>
</div>
</div>
</div>
<!-- 右侧登录表单 -->
<div class="login-form">
<div class="form-container">
<div class="form-header">
<h2>管理员登录</h2>
<p>请输入您的管理员账号和密码</p>
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form-content"
@keyup.enter="handleLogin"
>
<el-form-item prop="userAccount">
<el-input
v-model="loginForm.userAccount"
placeholder="请输入管理员账号"
size="large"
clearable
>
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="userPassword">
<el-input
v-model="loginForm.userPassword"
type="password"
placeholder="请输入密码"
size="large"
show-password
clearable
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
class="login-button"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<div class="login-tips">
<el-alert
v-if="errorMessage"
:title="errorMessage"
type="error"
:closable="true"
@close="errorMessage = ''"
/>
</div>
<div class="form-footer">
<p>© 2024 彩票推测系统 - 后台管理</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, Lock, Key, Document } from '@element-plus/icons-vue'
import { lotteryApi } from '../../api/index.js'
import { userStore } from '../../store/user.js'
export default {
name: 'AdminLogin',
components: {
User,
Lock,
Key,
Document
},
setup() {
const router = useRouter()
const loginFormRef = ref()
const loading = ref(false)
const errorMessage = ref('')
const loginForm = reactive({
userAccount: '',
userPassword: ''
})
const loginRules = {
userAccount: [
{ required: true, message: '请输入管理员账号', trigger: 'blur' },
{ min: 3, max: 20, message: '账号长度在 3 到 20 个字符', trigger: 'blur' }
],
userPassword: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
]
}
const handleLogin = async () => {
try {
await loginFormRef.value.validate()
loading.value = true
errorMessage.value = ''
// 使用真实的登录接口
const response = await lotteryApi.userLogin(loginForm.userAccount, loginForm.userPassword)
if (response && response.success) {
// 保存用户信息到session存储
userStore.setUserInfo(response.data)
// 获取完整用户信息,检查角色
const userResponse = await lotteryApi.getLoginUser()
if (userResponse && userResponse.success && userResponse.data) {
const userRole = userResponse.data.userRole
// 检查用户角色是否有权限访问后台
if (userRole && userRole !== 'user') {
// 确保将用户角色信息保存到session中
const userData = userResponse.data
userData.userRole = userRole
userStore.setUserInfo(userData)
ElMessage({
type: 'success',
message: '登录成功,欢迎使用后台管理系统'
})
// 跳转到后台管理首页
router.push('/admin/dashboard')
} else {
// 无权限访问
errorMessage.value = '您的账号无权限访问后台管理系统'
userStore.adminLogout() // 使用专门的管理员登出方法
}
} else {
errorMessage.value = userResponse?.message || '获取用户信息失败'
userStore.adminLogout() // 使用专门的管理员登出方法
}
} else {
errorMessage.value = response?.message || '登录失败,请检查账号密码'
}
} catch (error) {
console.error('登录失败:', error)
errorMessage.value = error?.response?.data?.message || '登录失败,请重试'
} finally {
loading.value = false
}
}
return {
loginFormRef,
loginForm,
loginRules,
loading,
errorMessage,
handleLogin
}
}
}
</script>
<style scoped>
.admin-login {
min-height: 100vh;
height: 100vh;
width: 100vw;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
}
.login-container {
width: 100%;
max-width: 1200px;
height: 600px;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
display: flex;
overflow: hidden;
}
/* 左侧装饰区域 */
.login-banner {
flex: 1;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
position: relative;
overflow: hidden;
}
.login-banner::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="white" opacity="0.1"/><circle cx="75" cy="75" r="1" fill="white" opacity="0.1"/><circle cx="50" cy="10" r="0.5" fill="white" opacity="0.1"/><circle cx="10" cy="60" r="0.5" fill="white" opacity="0.1"/><circle cx="90" cy="40" r="0.5" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.banner-content {
text-align: center;
color: white;
position: relative;
z-index: 1;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
}
.logo img {
width: 48px;
height: 48px;
margin-right: 15px;
}
.logo h1 {
font-size: 28px;
font-weight: 600;
margin: 0;
}
.banner-text h2 {
font-size: 32px;
font-weight: 700;
margin-bottom: 10px;
}
.banner-text p {
font-size: 16px;
opacity: 0.9;
margin-bottom: 40px;
}
.banner-features {
display: flex;
flex-direction: column;
gap: 20px;
}
.feature-item {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 16px;
opacity: 0.9;
}
.feature-item .el-icon {
font-size: 20px;
}
/* 右侧登录表单 */
.login-form {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
}
.form-container {
width: 100%;
max-width: 400px;
}
.form-header {
text-align: center;
margin-bottom: 40px;
}
.form-header h2 {
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.form-header p {
color: #666;
font-size: 14px;
}
.login-form-content {
margin-bottom: 30px;
}
.login-button {
width: 100%;
height: 48px;
font-size: 16px;
font-weight: 600;
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
}
.login-button:hover {
background: linear-gradient(135deg, #5a6fd8, #6a4190);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
}
.login-tips {
margin-bottom: 20px;
}
.login-tips p {
margin: 5px 0;
font-size: 14px;
}
.form-footer {
text-align: center;
color: #999;
font-size: 12px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.login-container {
flex-direction: column;
height: auto;
max-width: 400px;
}
.login-banner {
padding: 30px 20px;
}
.banner-text h2 {
font-size: 24px;
}
.login-form {
padding: 30px 20px;
}
}
@media (max-width: 480px) {
.admin-login {
padding: 10px;
}
.login-container {
border-radius: 15px;
}
.logo h1 {
font-size: 20px;
}
.banner-text h2 {
font-size: 20px;
}
}
</style>

View File

@@ -0,0 +1,675 @@
<template>
<div class="admin-dashboard">
<!-- 页面标题 -->
<div class="page-header">
<h1>控制面板</h1>
<p>欢迎使用后台管理系统</p>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<el-row :gutter="20">
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon users">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.totalUsers }}</div>
<div class="stat-label">总用户数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon vip">
<el-icon><Key /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.vipUsers }}</div>
<div class="stat-label">VIP用户</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon codes">
<el-icon><Ticket /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.totalCodes }}</div>
<div class="stat-label">会员码总数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon predictions">
<el-icon><TrendCharts /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.totalPredictions }}</div>
<div class="stat-label">推测记录</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 快捷操作 -->
<div class="quick-actions">
<el-card>
<template #header>
<div class="card-header">
<el-icon><Operation /></el-icon>
<span>快捷操作</span>
</div>
</template>
<div class="actions-grid">
<div class="action-item" @click="$router.push('/admin/vip-code')">
<div class="action-icon">
<el-icon><Key /></el-icon>
</div>
<div class="action-text">
<h3>会员码管理</h3>
<p>生成和管理会员码</p>
</div>
</div>
<div class="action-item" @click="$router.push('/admin/excel-import')">
<div class="action-icon">
<el-icon><Document /></el-icon>
</div>
<div class="action-text">
<h3>数据导入</h3>
<p>导入Excel数据</p>
</div>
</div>
<div class="action-item" @click="$router.push('/admin/user-list')">
<div class="action-icon">
<el-icon><User /></el-icon>
</div>
<div class="action-text">
<h3>用户管理</h3>
<p>管理用户信息</p>
</div>
</div>
</div>
</el-card>
</div>
<!-- 系统信息 -->
<div class="system-info">
<el-row>
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<el-icon><Clock /></el-icon>
<span>最近操作</span>
<div class="header-actions">
<el-button type="text" @click="$router.push('/admin/operation-history')">
更多
<el-icon class="el-icon--right"><ArrowRight /></el-icon>
</el-button>
</div>
</div>
</template>
<div class="recent-actions">
<el-table
:data="historyList"
v-loading="historyLoading"
stripe
style="width: 100%"
>
<el-table-column prop="operationTime" label="操作时间" width="180">
<template #default="{ row }">
{{ formatDate(row.operationTime) }}
</template>
</el-table-column>
<el-table-column prop="operationModule" label="操作模块" width="120">
<template #default="{ row }">
<el-tag>{{ getModuleText(row.operationModule) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationType" label="操作类型" width="150">
<template #default="{ row }">
<el-tag :type="getOperationType(row.operationType)">
{{ row.operationType }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="userName" label="操作人" width="120">
<template #default="{ row }">
{{ row.userName || '-' }}
</template>
</el-table-column>
<el-table-column prop="operationResult" label="操作结果" width="100">
<template #default="{ row }">
<el-tag :type="row.operationResult === '成功' ? 'success' : 'danger'">
{{ row.operationResult }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationDetail" label="详细信息" min-width="200">
<template #default="{ row }">
{{ row.operationDetail || row.resultMessage || '-' }}
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import {
User,
Key,
Ticket,
TrendCharts,
Operation,
Document,
Setting,
InfoFilled,
Clock,
ArrowRight
} from '@element-plus/icons-vue'
import { userStore } from '../../store/user.js'
import { lotteryApi } from '../../api/index.js'
import { useRouter } from 'vue-router'
export default {
name: 'AdminDashboard',
components: {
User,
Key,
Ticket,
TrendCharts,
Operation,
Document,
Setting,
InfoFilled,
Clock,
ArrowRight
},
setup() {
const router = useRouter()
// 统计数据
const stats = reactive({
totalUsers: 0,
vipUsers: 0,
totalCodes: 0,
totalPredictions: 0
})
// 管理员信息
const adminInfo = reactive({
userName: '管理员'
})
// 当前时间
const currentTime = ref('')
let timeInterval = null
let loginCheckInterval = null // 新增登录检查定时器
// 操作历史
const historyList = ref([])
const historyLoading = ref(false)
// 检查登录状态
const checkLoginStatus = () => {
if (!userStore.isAdminLoggedIn()) {
console.log('定期检查发现管理员登录态已过期,正在跳转到登录页...')
userStore.adminLogout()
router.push('/admin/login')
}
}
// 更新时间
const updateTime = () => {
currentTime.value = new Date().toLocaleString('zh-CN')
}
// 加载统计数据
const loadStats = async () => {
try {
console.log('开始加载统计数据...')
// 首先检查登录状态
if (!userStore.isAdminLoggedIn()) {
console.log('检测到管理员未登录或登录态已过期,正在跳转到登录页...')
userStore.adminLogout() // 清除可能存在的过期会话
router.push('/admin/login')
return
}
// 获取用户统计数据
const userCountResponse = await lotteryApi.getUserCount()
if (userCountResponse && userCountResponse.success) {
stats.totalUsers = userCountResponse.data.totalUserCount || 0
stats.vipUsers = userCountResponse.data.vipUserCount || 0
} else {
console.error('获取用户统计数据失败:', userCountResponse?.message)
// 使用默认值
stats.totalUsers = 0
stats.vipUsers = 0
}
// 获取会员码统计数据
console.log('开始获取会员码统计数据...')
const vipCodeCountResponse = await lotteryApi.getVipCodeCount()
if (vipCodeCountResponse && vipCodeCountResponse.success) {
stats.totalCodes = vipCodeCountResponse.data.totalCount || 0
console.log('设置会员码总数:', stats.totalCodes)
} else {
console.error('获取会员码统计数据失败:', vipCodeCountResponse?.message)
stats.totalCodes = 0
}
// 获取推测记录总数
console.log('开始获取推测记录总数...')
const totalPredictResponse = await lotteryApi.getTotalPredictCount()
console.log('推测记录总数API响应:', totalPredictResponse)
if (totalPredictResponse && totalPredictResponse.success) {
stats.totalPredictions = totalPredictResponse.data.totalCount || 0
console.log('设置推测记录总数:', stats.totalPredictions)
} else {
console.error('获取推测记录总数失败:', totalPredictResponse?.message)
stats.totalPredictions = 0
}
console.log('统计数据加载完成:', stats)
} catch (error) {
console.error('加载统计数据失败:', error)
// 发生错误时使用默认值
stats.totalUsers = 0
stats.vipUsers = 0
stats.totalCodes = 0
stats.totalPredictions = 0
}
}
// 加载操作历史
const loadOperationHistory = async () => {
try {
historyLoading.value = true
// 构建查询参数
const params = {
// 不传任何过滤条件,获取全部历史
}
// 调用统一接口获取操作历史
const response = await lotteryApi.getOperationHistoryList(params)
console.log('操作历史接口响应:', response)
if (response && response.success) {
// 处理响应数据
const data = response.data || []
// 取前10条记录
historyList.value = data.slice(0, 10)
} else {
historyList.value = []
}
} catch (error) {
console.error('加载操作历史失败:', error)
historyList.value = []
} finally {
historyLoading.value = false
}
}
// 获取操作模块文本
const getModuleText = (module) => {
const modules = {
0: '会员码管理',
1: 'Excel导入',
2: '用户管理'
}
return modules[module] || '未知模块'
}
// 获取操作类型标签样式
const getOperationType = (type) => {
const types = {
'完整数据导入': 'primary',
'开奖数据覆盖导入': 'warning',
'开奖数据追加': 'success',
'生成会员码': 'info',
'删除会员码': 'danger'
}
return types[type] || 'info'
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// 获取管理员信息
const loadAdminInfo = () => {
const user = userStore.getUserInfo()
if (user) {
adminInfo.userName = user.userName || user.username || '管理员'
}
}
onMounted(() => {
console.log('Dashboard组件已挂载开始初始化...')
loadStats()
loadOperationHistory()
loadAdminInfo()
// 启动时间更新
updateTime()
timeInterval = setInterval(updateTime, 1000)
// 定期检查登录状态每60秒检查一次
loginCheckInterval = setInterval(checkLoginStatus, 60000)
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
if (loginCheckInterval) {
clearInterval(loginCheckInterval)
}
})
return {
stats,
adminInfo,
currentTime,
historyList,
historyLoading,
getModuleText,
getOperationType,
formatDate
}
}
}
</script>
<style scoped>
.admin-dashboard {
padding: 20px;
}
/* 页面标题 */
.page-header {
margin-bottom: 24px;
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
padding: 30px;
border-radius: 8px;
color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.page-header h1 {
font-size: 28px;
font-weight: 600;
color: white;
margin-bottom: 8px;
text-align: center;
}
.page-header p {
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
margin: 0;
text-align: center;
}
/* 统计卡片 */
.stats-grid {
margin-bottom: 24px;
}
.stat-card {
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
}
.stat-content {
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
}
.stat-icon.users {
background: linear-gradient(135deg, #667eea, #764ba2);
}
.stat-icon.vip {
background: linear-gradient(135deg, #28a745, #20c997);
}
.stat-icon.codes {
background: linear-gradient(135deg, #fd7e14, #ffc107);
}
.stat-icon.predictions {
background: linear-gradient(135deg, #17a2b8, #6f42c1);
}
.stat-info {
flex: 1;
}
.stat-number {
font-size: 24px;
font-weight: 600;
color: #333;
line-height: 1;
}
.stat-label {
font-size: 14px;
color: #666;
margin-top: 4px;
}
/* 快捷操作 */
.quick-actions {
margin-bottom: 24px;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #333;
}
.card-header .el-icon {
font-size: 18px;
color: #409EFF;
}
.header-actions {
margin-left: auto; /* Push content to the right */
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.action-item {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.action-item:hover {
border-color: #409EFF;
background-color: #f0f9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
.action-icon {
width: 48px;
height: 48px;
border-radius: 8px;
background: linear-gradient(135deg, #409EFF, #67c23a);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
.action-text h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 4px 0;
}
.action-text p {
font-size: 14px;
color: #666;
margin: 0;
}
/* 系统信息 */
.system-info {
margin-bottom: 24px;
}
.info-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-weight: 600;
color: #333;
}
.info-value {
color: #666;
}
/* 最近操作 */
.recent-actions {
max-height: 200px;
overflow-y: auto;
}
.empty-actions {
text-align: center;
color: #999;
padding: 40px 20px;
}
.action-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.action-record {
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
border-left: 3px solid #409EFF;
}
.action-time {
font-size: 12px;
color: #999;
margin-bottom: 4px;
}
.action-desc {
font-size: 14px;
color: #333;
line-height: 1.4;
}
/* 响应式设计 */
@media (max-width: 768px) {
.admin-dashboard {
padding: 15px;
}
.stats-grid .el-col {
margin-bottom: 16px;
}
.actions-grid {
grid-template-columns: 1fr;
}
.system-info .el-col {
margin-bottom: 16px;
}
}
</style>

View File

@@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,435 @@
<template>
<div class="operation-history">
<!-- 页面标题 -->
<div class="page-header">
<div class="header-content">
<h1>操作历史管理</h1>
<p>查看和管理系统操作历史记录</p>
</div>
</div>
<!-- 筛选器 -->
<el-card class="filter-card">
<template #header>
<div class="card-header">
<el-icon><Filter /></el-icon>
<span>筛选选项</span>
</div>
</template>
<el-form :inline="true" :model="filterForm" class="filter-form">
<el-form-item label="关键词">
<el-input
v-model="filterForm.keyword"
placeholder="搜索详细信息"
clearable
@input="handleFilter"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="操作模块">
<div class="custom-select-wrapper">
<select
v-model="filterForm.operationModule"
class="custom-select"
@change="handleFilter"
>
<option value="">全部模块</option>
<option value="0">会员码管理</option>
<option value="1">Excel导入</option>
</select>
</div>
</el-form-item>
<el-form-item label="操作结果">
<div class="custom-select-wrapper">
<select
v-model="filterForm.operationResult"
class="custom-select"
@change="handleFilter"
>
<option value="">全部结果</option>
<option value="成功">成功</option>
<option value="失败">失败</option>
</select>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleFilter">搜索</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作历史列表 -->
<el-card class="history-card">
<template #header>
<div class="card-header">
<el-icon><Clock /></el-icon>
<span>操作历史列表</span>
<div class="header-actions">
<el-button @click="refreshHistory">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</template>
<el-table
:data="historyList"
v-loading="loading"
stripe
style="width: 100%"
>
<el-table-column prop="operationTime" label="操作时间" width="180">
<template #default="{ row }">
{{ formatDate(row.operationTime) }}
</template>
</el-table-column>
<el-table-column prop="operationModule" label="操作模块" width="120">
<template #default="{ row }">
<el-tag>{{ getModuleText(row.operationModule) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationType" label="操作类型" width="150">
<template #default="{ row }">
<el-tag :type="getOperationType(row.operationType)">
{{ row.operationType }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="userName" label="操作人" width="120">
<template #default="{ row }">
{{ row.userName || '-' }}
</template>
</el-table-column>
<el-table-column prop="operationResult" label="操作结果" width="100">
<template #default="{ row }">
<el-tag :type="row.operationResult === '成功' ? 'success' : 'danger'">
{{ row.operationResult }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operationDetail" label="详细信息" min-width="200">
<template #default="{ row }">
{{ row.operationDetail || row.resultMessage || '-' }}
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50]"
:total="pagination.total"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
Clock,
Refresh,
Search,
Filter
} from '@element-plus/icons-vue'
import { lotteryApi } from '../../api/index.js'
import { userStore } from '../../store/user.js'
export default {
name: 'OperationHistory',
components: {
Clock,
Refresh,
Search,
Filter
},
setup() {
// 筛选表单
const filterForm = reactive({
operationModule: '',
operationResult: '',
keyword: ''
})
// 操作历史
const historyList = ref([])
const loading = ref(false)
const pagination = reactive({
current: 1,
size: 10,
total: 0
})
// 初始化
onMounted(() => {
loadOperationHistory()
})
// 加载操作历史
const loadOperationHistory = async () => {
try {
loading.value = true
// 构建查询参数
const params = {
operationModule: filterForm.operationModule,
operationResult: filterForm.operationResult,
keyword: filterForm.keyword
}
// 调用统一接口获取操作历史
const response = await lotteryApi.getOperationHistoryList(params)
console.log('操作历史接口响应:', response)
if (response && response.success) {
// 处理响应数据
const data = response.data || []
// 简单的前端分页
const startIndex = (pagination.current - 1) * pagination.size
const endIndex = startIndex + pagination.size
historyList.value = data.slice(startIndex, endIndex)
pagination.total = data.length
} else {
ElMessage.error('获取操作历史失败: ' + (response?.message || '未知错误'))
historyList.value = []
pagination.total = 0
}
} catch (error) {
console.error('加载操作历史失败:', error)
ElMessage.error('加载操作历史失败: ' + (error?.message || '未知错误'))
historyList.value = []
pagination.total = 0
} finally {
loading.value = false
}
}
// 刷新历史
const refreshHistory = () => {
pagination.current = 1
loadOperationHistory()
}
// 筛选处理
const handleFilter = () => {
pagination.current = 1
loadOperationHistory()
}
// 重置筛选
const resetFilter = () => {
filterForm.operationModule = ''
filterForm.operationResult = ''
filterForm.keyword = ''
pagination.current = 1
loadOperationHistory()
}
// 分页处理
const handleSizeChange = (size) => {
pagination.size = size
pagination.current = 1
loadOperationHistory()
}
const handleCurrentChange = (current) => {
pagination.current = current
loadOperationHistory()
}
// 获取操作模块文本
const getModuleText = (module) => {
const modules = {
0: '会员码管理',
1: 'Excel导入',
2: '用户管理'
}
return modules[module] || '未知模块'
}
// 获取操作类型标签样式
const getOperationType = (type) => {
const types = {
'完整数据导入': 'primary',
'开奖数据覆盖导入': 'warning',
'开奖数据追加': 'success',
'生成会员码': 'info',
'删除会员码': 'danger'
}
return types[type] || 'info'
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
return {
filterForm,
historyList,
loading,
pagination,
handleFilter,
resetFilter,
refreshHistory,
handleSizeChange,
handleCurrentChange,
getModuleText,
getOperationType,
formatDate
}
}
}
</script>
<style scoped>
.operation-history {
padding: 20px;
}
/* 页面标题 */
.page-header {
margin-bottom: 24px;
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
padding: 30px;
border-radius: 8px;
color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.header-content h1 {
font-size: 28px;
font-weight: 600;
color: white;
margin-bottom: 8px;
text-align: center;
}
.header-content p {
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
margin: 0;
text-align: center;
}
/* 卡片样式 */
.filter-card, .history-card {
margin-bottom: 24px;
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #333;
}
.card-header .el-icon {
font-size: 18px;
color: #409EFF;
}
.header-actions {
margin-left: auto;
}
/* 筛选表单 */
.filter-form {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
/* 自定义下拉框样式 */
.custom-select-wrapper {
position: relative;
width: 100%;
min-width: 160px;
}
.custom-select {
width: 100%;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: white;
font-size: 14px;
color: #606266;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23606266'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.custom-select:hover {
border-color: #c0c4cc;
}
.custom-select:focus {
outline: none;
border-color: #409eff;
}
/* 分页 */
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: center;
}
/* 响应式设计 */
@media (max-width: 768px) {
.operation-history {
padding: 15px;
}
.filter-form {
flex-direction: column;
}
.filter-form .el-form-item {
margin-right: 0;
width: 100%;
}
.header-actions {
margin-top: 8px;
margin-left: 0;
}
}
</style>

View File

@@ -0,0 +1,979 @@
<template>
<div class="user-list">
<!-- 页面标题 -->
<div class="page-header">
<h1>用户管理</h1>
<p>管理系统用户信息和权限</p>
</div>
<!-- 搜索和操作栏 -->
<el-card class="search-card">
<div class="search-bar">
<el-row :gutter="20">
<el-col :span="4">
<el-input
v-model="searchForm.userAccount"
placeholder="账号"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="4">
<el-input
v-model="searchForm.userName"
placeholder="昵称"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="4">
<el-input
v-model="searchForm.phone"
placeholder="手机号"
clearable
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="3">
<div class="custom-select-wrapper">
<select
v-model="searchForm.userRole"
class="custom-select"
@change="handleSearch"
>
<option value="">全部角色</option>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
<option value="superAdmin">超级管理员</option>
</select>
</div>
</el-col>
<el-col :span="3">
<div class="custom-select-wrapper">
<select
v-model="searchForm.status"
class="custom-select"
@change="handleSearch"
>
<option value="">全部状态</option>
<option :value="0">正常</option>
<option :value="1">禁用</option>
</select>
</div>
</el-col>
<el-col :span="3">
<div class="custom-select-wrapper">
<select
v-model="searchForm.isVip"
class="custom-select"
@change="handleSearch"
>
<option value="">全部会员</option>
<option :value="1"></option>
<option :value="0"></option>
</select>
</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 10px;">
<el-col :span="6">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-col>
<el-col :span="4">
<el-button type="success" @click="showAddDialog">
<el-icon><Plus /></el-icon>
添加用户
</el-button>
</el-col>
</el-row>
</div>
</el-card>
<!-- 用户列表 -->
<el-card class="list-card">
<template #header>
<div class="card-header">
<span>用户列表</span>
<div class="header-actions">
<el-button @click="refreshList">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</template>
<el-table
:data="userList"
v-loading="tableLoading"
stripe
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="120" />
<el-table-column prop="userAccount" label="账号" width="120">
<template #default="{ row }">
{{ row.userAccount || '-' }}
</template>
</el-table-column>
<el-table-column prop="userName" label="昵称" width="120">
<template #default="{ row }">
{{ row.userName || '-' }}
</template>
</el-table-column>
<el-table-column prop="phone" label="手机号" width="130">
<template #default="{ row }">
{{ row.phone || '-' }}
</template>
</el-table-column>
<el-table-column prop="userRole" label="角色" width="120">
<template #default="{ row }">
<el-tag :type="getRoleType(row.userRole)">
{{ getRoleText(row.userRole) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="isVip" label="会员" width="80">
<template #default="{ row }">
<el-tag :type="row.isVip === 1 ? 'warning' : 'info'">
{{ row.isVip === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="vipExpire" label="VIP到期" width="120">
<template #default="{ row }">
<span v-if="row.vipExpire">{{ formatDate(row.vipExpire) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 0 ? 'success' : 'danger'">
{{ row.status === 0 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="注册时间" width="180">
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="editUser(row)">
编辑
</el-button>
<el-button
:type="row.status === 0 ? 'warning' : 'success'"
size="small"
@click="toggleUserStatus(row)"
:loading="row.statusLoading"
:disabled="row.statusLoading"
>
{{ row.status === 0 ? '禁用' : '启用' }}
</el-button>
<el-button type="danger" size="small" @click="deleteUser(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 添加/编辑用户对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
@close="resetForm"
>
<el-form
ref="userFormRef"
:model="userForm"
:rules="userRules"
label-width="100px"
>
<el-form-item label="账号" prop="userAccount">
<el-input v-model="userForm.userAccount" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="昵称" prop="userName">
<el-input v-model="userForm.userName" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="userForm.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="密码" prop="editPassword">
<el-input
v-model="userForm.password"
type="password"
placeholder="请输入密码"
show-password
/>
<div class="form-tip" v-if="userForm.id">
<small>不修改密码请留空</small>
</div>
</el-form-item>
<el-form-item label="用户角色" prop="userRole">
<div class="custom-select-wrapper" style="width: 100%;">
<select
v-model="userForm.userRole"
class="custom-select"
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
<option value="superAdmin">超级管理员</option>
</select>
</div>
</el-form-item>
<el-form-item label="会员过期时间" prop="vipExpire">
<input
v-model="userForm.vipExpire"
type="date"
placeholder="选择会员过期时间"
style="width: 100%; height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 4px; box-sizing: border-box;"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="userForm.status">
<el-radio :label="0">正常</el-radio>
<el-radio :label="1">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading">
{{ submitLoading ? '提交中...' : '确定' }}
</el-button>
</span>
</template>
</el-dialog>
<!-- 删除确认弹窗 -->
<el-dialog
v-model="deleteDialogVisible"
title="确认删除"
width="400px"
:close-on-click-modal="false"
:close-on-press-escape="false"
center
top="360px"
>
<div style="text-align:center;">
<span>确认删除用户 "{{ userToDelete?.userName }}" 此操作不可恢复</span>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancelDeleteUser">取消</el-button>
<el-button type="danger" @click="confirmDeleteUser">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Search,
Refresh,
Plus,
Download,
Edit,
Delete
} from '@element-plus/icons-vue'
import { lotteryApi } from '../../api/index.js'
import { userStore } from '../../store/user.js'
import { useRouter } from 'vue-router'
export default {
name: 'UserList',
components: {
Search,
Refresh,
Plus,
Download,
Edit,
Delete
},
setup() {
const router = useRouter()
// 权限检查
const checkPermission = () => {
const user = userStore.getUserInfo()
if (!user || user.userRole !== 'superAdmin') {
ElMessage({
type: 'error',
message: '无权限访问此页面,仅限超级管理员使用'
})
// 重定向到控制面板
setTimeout(() => {
router.push('/admin/dashboard')
}, 1500)
return false
}
return true
}
// 搜索表单
const searchForm = reactive({
userAccount: '',
userName: '',
phone: '',
userRole: '',
status: '',
isVip: ''
})
// 用户列表
const userList = ref([])
const tableLoading = ref(false)
const selectedUsers = ref([])
// 分页
const pagination = reactive({
current: 1,
size: 20,
total: 0
})
// 对话框
const dialogVisible = ref(false)
const dialogTitle = ref('添加用户')
const submitLoading = ref(false)
// 删除确认弹窗
const deleteDialogVisible = ref(false)
const userToDelete = ref(null)
// 用户表单
const userFormRef = ref()
const userForm = reactive({
id: null,
userAccount: '',
userName: '',
phone: '',
password: '',
userRole: 'user', // 设置默认值为普通用户
vipExpire: null,
status: 0
})
const userRules = {
userAccount: [
{ required: true, message: '请输入账号', trigger: 'blur' },
{ min: 3, max: 20, message: '账号长度在 3 到 20 个字符', trigger: 'blur' }
],
userName: [
{ required: true, message: '请输入昵称', trigger: 'blur' },
{ min: 2, max: 20, message: '昵称长度在 2 到 20 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
editPassword: [
{
validator: (rule, value, callback) => {
// 添加用户时,密码必填
if (!userForm.id && !userForm.password) {
callback(new Error('请输入密码'))
}
// 编辑用户时,如果填写了密码,则验证长度
else if (userForm.password && userForm.password.length < 8) {
callback(new Error('密码长度不小于8位'))
}
else {
callback()
}
},
trigger: 'blur'
}
],
userRole: [
{ required: true, message: '请选择用户角色', trigger: 'change' }
]
}
// Function to format Date object or ISO string to YYYY-MM-DD string for native input type="date"
const formatToDateString = (dateInput) => {
if (!dateInput) return '';
let date;
if (dateInput instanceof Date) {
date = dateInput;
} else {
date = new Date(dateInput);
}
if (isNaN(date.getTime())) {
return '';
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// 加载用户列表
const loadUserList = async () => {
try {
tableLoading.value = true
const params = {
page: pagination.current,
size: pagination.size,
// 直接将搜索表单对象的每个属性展开,确保参数正确传递
...searchForm
}
// 删除空字符串参数,避免发送不必要的参数
Object.keys(params).forEach(key => {
if (params[key] === '') {
delete params[key]
}
})
console.log('发送请求参数:', params)
const response = await lotteryApi.getUserList(params)
console.log('获取用户列表响应:', response)
if (response && response.success) {
// 数据直接在response.data中是一个数组
userList.value = response.data || []
// 设置总数为数组长度,因为后端没有返回总数
pagination.total = userList.value.length
} else {
ElMessage({
type: 'error',
message: response?.message || '获取用户列表失败'
})
}
} catch (error) {
console.error('加载用户列表失败:', error)
ElMessage({
type: 'error',
message: '加载数据失败,请重试'
})
} finally {
tableLoading.value = false
}
}
// 搜索处理
const handleSearch = () => {
pagination.current = 1
loadUserList()
}
// 重置搜索
const resetSearch = () => {
Object.assign(searchForm, {
userAccount: '',
userName: '',
phone: '',
userRole: '',
status: '',
isVip: ''
})
handleSearch()
}
// 分页处理
const handleSizeChange = (size) => {
pagination.size = size
pagination.current = 1
loadUserList()
}
const handleCurrentChange = (current) => {
pagination.current = current
loadUserList()
}
// 选择处理
const handleSelectionChange = (selection) => {
selectedUsers.value = selection
}
// 刷新列表
const refreshList = () => {
loadUserList()
}
// 显示添加对话框
const showAddDialog = () => {
dialogTitle.value = '添加用户'
// 重置表单数据,确保弹窗内容被清空
resetForm()
dialogVisible.value = true
}
// 编辑用户
const editUser = (row) => {
dialogTitle.value = '编辑用户'
Object.assign(userForm, {
id: row.id,
userAccount: row.userAccount || '',
userName: row.userName,
phone: row.phone || '',
password: '', // 编辑时密码留空,由用户决定是否修改
userRole: row.userRole,
vipExpire: row.vipExpire ? formatToDateString(row.vipExpire) : null, // Convert to YYYY-MM-DD string
status: row.status
})
dialogVisible.value = true
}
// 切换用户状态
const toggleUserStatus = async (row) => {
console.log('toggleUserStatus called for row:', row)
try {
const action = row.status === 0 ? '禁用' : '启用'
console.log(`Attempting to ${action} user "${row.userName}"`)
// 暂时移除确认对话框以调试
// await ElMessageBox.confirm(`确认${action}用户 "${row.userName}" 吗?`, '提示', {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// type: 'warning'
// })
// console.log('ElMessageBox.confirm confirmed.')
// 设置临时状态标记,防止重复点击
row.statusLoading = true
// 准备请求参数
const params = {
id: row.id,
status: row.status === 0 ? 1 : 0 // 切换状态0->1 或 1->0
}
console.log('发送更新用户状态请求:', params)
const response = await lotteryApi.updateUserStatus(params)
console.log('更新用户状态响应:', response)
if (response && response.success) {
// 更新本地状态
row.status = row.status === 0 ? 1 : 0
ElMessage({
type: 'success',
message: `${action}成功`,
duration: 2000
})
} else {
ElMessage({
type: 'error',
message: response?.message || `${action}失败,请重试`
})
// 如果是未登录或权限错误,可能需要重新登录
if (response?.code === 40100 || response?.code === 40101) {
ElMessage.error('登录已过期,请重新登录')
setTimeout(() => {
window.location.href = '/admin/login'
}, 1500)
}
}
} catch (error) {
if (error === 'cancel') { // 这里捕获到的cancel是用户点击取消按钮的情况
console.log('User canceled the operation.')
} else {
console.error('切换用户状态失败:', error)
const errorMsg = error?.response?.data?.message || '操作失败,请重试'
ElMessage({
type: 'error',
message: errorMsg
})
// 如果是401错误可能是登录态过期
if (error.response && error.response.status === 401) {
ElMessage.error('登录已过期,请重新登录')
setTimeout(() => {
window.location.href = '/admin/login'
}, 1500)
}
}
} finally {
// 清除临时状态标记
row.statusLoading = false
console.log('toggleUserStatus finished.')
}
}
// 删除用户
const deleteUser = (row) => {
userToDelete.value = row
deleteDialogVisible.value = true
}
// 确认删除
const confirmDeleteUser = async () => {
if (!userToDelete.value) return
try {
const response = await lotteryApi.deleteUser(userToDelete.value.id)
if (response && response.success) {
ElMessage({
type: 'success',
message: '删除成功'
})
loadUserList()
} else {
ElMessage({
type: 'error',
message: response.message || '删除失败,请重试'
})
}
} catch (error) {
console.error('删除用户失败:', error)
ElMessage({
type: 'error',
message: '删除失败,请重试'
})
} finally {
deleteDialogVisible.value = false
userToDelete.value = null
}
}
// 取消删除
const cancelDeleteUser = () => {
deleteDialogVisible.value = false
userToDelete.value = null
}
// 提交表单
const submitForm = async () => {
try {
await userFormRef.value.validate()
submitLoading.value = true
let payload = { ...userForm }; // Create a copy to avoid modifying original reactive object directly
// Convert vipExpire string from native date input to ISO string for backend
if (payload.vipExpire && typeof payload.vipExpire === 'string') {
try {
const date = new Date(payload.vipExpire); // Parses "YYYY-MM-DD" string
payload.vipExpire = date.toISOString(); // Convert to ISO string for backend
} catch (e) {
console.error('Error converting vipExpire:', e);
payload.vipExpire = null; // Set to null if conversion fails
}
} else if (payload.vipExpire === '') { // Handle case where input is cleared
payload.vipExpire = null;
}
let response
if (payload.id) {
response = await lotteryApi.updateUser(payload)
} else {
const { id, ...userData } = payload
response = await lotteryApi.addUser(userData)
}
// 检查后端响应的 success 字段
if (response && response.success) {
ElMessage({
type: 'success',
message: payload.id ? '编辑成功' : '添加成功'
})
dialogVisible.value = false // 成功后关闭弹窗
resetForm()
loadUserList()
} else {
ElMessage({
type: 'error',
message: response.message || (payload.id ? '编辑失败' : '添加失败')
})
}
} catch (error) {
console.error('提交表单失败:', error)
ElMessage({
type: 'error',
message: '操作失败,请重试'
})
} finally {
submitLoading.value = false
}
}
// 重置表单
const resetForm = () => {
Object.assign(userForm, {
id: null,
userAccount: '',
userName: '',
phone: '',
password: '',
userRole: 'user',
vipExpire: null,
status: 0
})
userFormRef.value?.resetFields()
}
// 角色处理
const getRoleType = (role) => {
const types = {
admin: 'danger',
superAdmin: 'danger',
vip: 'warning',
user: 'info'
}
return types[role] || 'info'
}
const getRoleText = (role) => {
const texts = {
admin: '管理员',
superAdmin: '超级管理员',
user: '普通用户'
}
return texts[role] || role
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleString('zh-CN')
}
onMounted(() => {
// 检查权限
if (!checkPermission()) return
loadUserList()
})
return {
searchForm,
userList,
tableLoading,
selectedUsers,
pagination,
dialogVisible,
dialogTitle,
submitLoading,
userFormRef,
userForm,
userRules,
handleSearch,
resetSearch,
handleSizeChange,
handleCurrentChange,
handleSelectionChange,
refreshList,
showAddDialog,
editUser,
toggleUserStatus,
deleteUser,
submitForm,
resetForm,
getRoleType,
getRoleText,
formatDate,
deleteDialogVisible,
userToDelete,
confirmDeleteUser,
cancelDeleteUser
}
}
}
</script>
<style scoped>
.user-list {
padding: 20px;
}
/* 页面标题 */
.page-header {
margin-bottom: 24px;
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
padding: 30px;
border-radius: 8px;
color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 1;
}
.page-header h1 {
font-size: 28px;
font-weight: 600;
color: white;
margin-bottom: 8px;
text-align: center;
}
.page-header p {
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
margin: 0;
text-align: center;
}
/* 搜索卡片 */
.search-card {
margin-bottom: 24px;
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 1;
}
.search-bar {
padding: 16px 0;
}
/* 列表卡片 */
.list-card {
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: #333;
}
.header-actions {
display: flex;
gap: 8px;
}
/* 分页 */
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: center;
}
/* 对话框 */
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.user-list {
padding: 15px;
}
.search-bar .el-col {
margin-bottom: 12px;
}
.header-actions {
flex-direction: column;
gap: 4px;
}
}
/* 确保下拉菜单可以正常显示 */
:deep(.el-select__popper) {
z-index: 3000 !important;
}
:deep(.el-select) {
width: 100%;
}
:deep(.el-select .el-input) {
width: 100%;
}
:deep(.el-popper) {
z-index: 3000 !important;
}
/* 自定义下拉框样式 */
.custom-select-wrapper {
position: relative;
width: 100%;
}
.custom-select {
width: 100%;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: white;
font-size: 14px;
color: #606266;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23606266'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.custom-select:hover {
border-color: #c0c4cc;
}
.custom-select:focus {
outline: none;
border-color: #409eff;
}
/* 表单提示文字 */
.form-tip {
font-size: 12px;
color: #909399;
line-height: 1;
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,11 @@
<template>
</template>
<script>
</script>
<style scoped>
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,536 @@
<template>
<div class="admin-layout">
<el-container class="layout-container">
<!-- 侧边栏 -->
<el-aside :width="isCollapse ? '64px' : '240px'" class="sidebar">
<div class="sidebar-header">
<div class="logo" :class="{ 'collapsed': isCollapse }">
<img src="/favicon.ico" alt="Logo" />
<h2 v-show="!isCollapse">后台管理</h2>
</div>
</div>
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:unique-opened="true"
router
background-color="#001529"
text-color="#fff"
active-text-color="#409EFF"
class="sidebar-menu"
>
<el-menu-item index="/admin/dashboard">
<el-icon><DataBoard /></el-icon>
<template #title>控制面板</template>
</el-menu-item>
<el-menu-item index="/admin/user-list" v-if="userRole === 'superAdmin'">
<el-icon><User /></el-icon>
<template #title>用户管理</template>
</el-menu-item>
<el-menu-item index="/admin/vip-code">
<el-icon><Key /></el-icon>
<template #title>会员码管理</template>
</el-menu-item>
<el-menu-item index="/excel-import">
<el-icon><Document /></el-icon>
<template #title>数据导入</template>
</el-menu-item>
<el-menu-item index="/admin/operation-history">
<el-icon><Clock /></el-icon>
<template #title>操作历史</template>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 主容器 -->
<el-container class="main-container">
<!-- 顶部导航 -->
<el-header class="header">
<div class="header-left">
<el-button
link
@click="toggleSidebar"
class="toggle-button"
>
<el-icon :size="20">
<Fold v-if="!isCollapse" />
<Expand v-else />
</el-icon>
</el-button>
<el-breadcrumb separator="/" class="breadcrumb">
<el-breadcrumb-item :to="{ path: '/admin/dashboard' }">首页</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index">
{{ item }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-right">
<div class="header-actions">
<el-button link @click="refreshPage" class="refresh-button">
<el-icon><Refresh /></el-icon>
</el-button>
<!-- 用户信息 -->
<div class="user-info">
<el-avatar :size="32" :src="userAvatar" />
<span class="username">{{ userName }}</span>
</div>
<!-- 注销按钮 -->
<el-button
type="danger"
@click="directLogout"
class="direct-logout"
size="small"
>
<el-icon :size="16"><SwitchButton /></el-icon>
<span style="font-size: 16px;">注销</span>
</el-button>
</div>
</div>
</el-header>
<!-- 主内容区域 -->
<el-main class="main-content">
<div class="page-container">
<router-view />
</div>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
DataBoard,
User,
Key,
Document,
Fold,
Expand,
ArrowDown,
Refresh,
Setting,
SwitchButton,
Clock
} from '@element-plus/icons-vue'
import { userStore } from '../../../store/user.js'
import { lotteryApi } from '../../../api/index.js'
export default {
name: 'AdminLayout',
components: {
DataBoard,
User,
Key,
Document,
Fold,
Expand,
ArrowDown,
Refresh,
Setting,
SwitchButton,
Clock
},
setup() {
const route = useRoute()
const router = useRouter()
const isCollapse = ref(false)
const userName = ref('管理员')
const userAvatar = ref('https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png')
const userRole = ref('')
// 当前激活的菜单项
const activeMenu = computed(() => route.path)
// 面包屑导航
const breadcrumbs = computed(() => {
const path = route.path.split('/')
if (path[2] === 'dashboard') return []
const map = {
'user-list': ['用户管理'],
'vip-code': ['会员码管理'],
'excel-import': ['数据导入'],
'operation-history': ['操作历史']
}
return map[path[2]] || [path[2]]
})
// 切换侧边栏
const toggleSidebar = () => {
isCollapse.value = !isCollapse.value
}
// 刷新页面
const refreshPage = () => {
window.location.reload()
}
// 处理下拉菜单命令
const handleCommand = (command) => {
console.log('接收到下拉菜单命令:', command)
switch (command) {
case 'profile':
console.log('跳转到个人信息页面')
router.push('/admin/profile')
break
case 'settings':
console.log('跳转到系统设置页面')
router.push('/admin/settings')
break
case 'logout':
console.log('执行注销操作')
handleLogout()
break
default:
console.log('未知命令:', command)
}
}
// 退出登录
const handleLogout = () => {
ElMessageBox.confirm('确认退出后台管理系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
// 调用后端注销接口
const res = await lotteryApi.userLogout()
console.log('注销API响应:', res)
// 无论API响应如何都清除session状态
userStore.adminLogout()
ElMessage({
type: 'success',
message: '已安全退出系统'
})
// 确保跳转到登录页
setTimeout(() => {
router.push('/admin/login')
}, 100)
} catch (error) {
console.error('注销过程中出错:', error)
// 即使出错也清除session状态并跳转
userStore.adminLogout()
ElMessage({
type: 'warning',
message: '注销过程中出现错误,已强制退出'
})
setTimeout(() => {
router.push('/admin/login')
}, 100)
}
}).catch(() => {
// 用户取消操作
})
}
// 直接注销,不使用确认对话框
const directLogout = () => {
try {
// 调用后端注销接口
lotteryApi.userLogout()
.then(() => {
console.log('注销API调用成功')
})
.catch((error) => {
console.error('注销API调用失败:', error)
})
.finally(() => {
// 无论成功失败都清除session状态
userStore.adminLogout()
// 显示成功消息
ElMessage({
type: 'success',
message: '已安全退出系统'
})
// 直接跳转到登录页
window.location.href = '/admin/login'
})
} catch (error) {
console.error('注销过程中出错:', error)
// 强制清除状态并跳转
userStore.adminLogout()
window.location.href = '/admin/login'
}
}
// 获取用户信息
onMounted(() => {
const user = userStore.getUserInfo()
if (user) {
userName.value = user.userName || user.username || '管理员'
userRole.value = user.userRole || 'admin'
if (user.avatar) {
userAvatar.value = user.avatar
}
}
})
return {
isCollapse,
userName,
userAvatar,
userRole,
activeMenu,
breadcrumbs,
toggleSidebar,
refreshPage,
handleCommand,
handleLogout,
directLogout
}
}
}
</script>
<style scoped>
.admin-layout {
height: 100vh;
width: 100vw;
display: flex;
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.layout-container {
height: 100%;
width: 100%;
}
/* 侧边栏 */
.sidebar {
background-color: #001529;
transition: width 0.3s;
overflow-y: auto;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
position: relative;
height: 100%;
}
.sidebar-header {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #002140;
border-bottom: 1px solid #001529;
}
.logo {
display: flex;
align-items: center;
padding: 0 16px;
transition: all 0.3s;
}
.logo.collapsed {
justify-content: center;
}
.logo img {
width: 32px;
height: 32px;
margin-right: 12px;
}
.logo h2 {
color: #fff;
font-size: 18px;
font-weight: 600;
margin: 0;
white-space: nowrap;
}
.sidebar-menu {
border: none;
}
.sidebar-menu .el-menu-item,
.sidebar-menu .el-sub-menu__title {
height: 50px;
line-height: 50px;
}
/* 顶部导航 */
.header {
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 60px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.toggle-button {
font-size: 18px;
color: #666;
padding: 8px;
border-radius: 4px;
transition: all 0.3s;
}
.toggle-button:hover {
background-color: #f5f5f5;
color: #409EFF;
}
.refresh-button {
font-size: 18px;
color: #666;
padding: 8px;
border-radius: 4px;
transition: all 0.3s;
}
.refresh-button:hover {
background-color: #f5f5f5;
color: #409EFF;
}
.breadcrumb {
font-size: 14px;
}
.header-right {
display: flex;
align-items: center;
}
.header-actions {
display: flex;
align-items: center;
gap: 0px;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 6px;
}
.username {
font-size: 14px;
color: #333;
font-weight: 500;
}
.dropdown-item-content {
display: flex;
align-items: center;
gap: 8px;
}
/* 添加直接注销按钮样式 */
.direct-logout {
margin-left: 10px;
margin-right: 20px;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
/* 主内容区域 */
.main-content {
background-color: #f5f5f5;
padding: 20px;
overflow-y: auto;
height: calc(100vh - 60px);
}
.page-container {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-height: calc(100vh - 120px);
}
/* 响应式设计 */
@media (max-width: 768px) {
.sidebar {
position: fixed;
z-index: 1000;
height: 100vh;
transform: translateX(-100%);
transition: transform 0.3s;
}
.sidebar.show {
transform: translateX(0);
}
.header {
padding: 0 15px;
}
.header-left {
gap: 15px;
}
.username {
display: none;
}
.main-content {
padding: 15px;
}
}
/* 滚动条样式 */
.sidebar::-webkit-scrollbar,
.main-content::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track,
.main-content::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb,
.main-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover,
.main-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>

5
lottery-app/start.bat Normal file
View File

@@ -0,0 +1,5 @@
@echo off
echo 正在启动双色球智能推测系统...
echo.
npm run dev
pause

View File

@@ -0,0 +1,20 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
host: '0.0.0.0', // 允许局域网访问
port: 5173
}
})

119
userController Normal file
View File

@@ -0,0 +1,119 @@
package com.xy.xyaicpzs.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xy.xyaicpzs.common.DeleteRequest;
import com.xy.xyaicpzs.common.ErrorCode;
import com.xy.xyaicpzs.common.ResultUtils;
import com.xy.xyaicpzs.common.response.ApiResponse;
import com.xy.xyaicpzs.domain.dto.user.*;
import com.xy.xyaicpzs.domain.entity.User;
import com.xy.xyaicpzs.domain.vo.UserVO;
import com.xy.xyaicpzs.exception.BusinessException;
import com.xy.xyaicpzs.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户接口
*/
@Slf4j
@RestController
@RequestMapping("/user")
@Tag(name = "用户管理", description = "用户管理相关接口")
public class UserController {
@Resource
private UserService userService;
// region 登录相关
/**
* 用户注册
*
* @param userRegisterRequest
* @return
*/
@PostMapping("/register")
@Operation(summary = "用户注册", description = "用户注册接口")
public ApiResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
if (userRegisterRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long result = userService.userRegister(userAccount, userPassword, checkPassword);
return ResultUtils.success(result);
}
/**
* 用户登录
*
* @param userLoginRequest
* @param request
* @return
*/
@PostMapping("/login")
@Operation(summary = "用户登录", description = "用户登录接口")
public ApiResponse<UserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = userService.userLogin(userAccount, userPassword, request);
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
return ResultUtils.success(userVO);
}
/**
* 用户注销
*
* @param request
* @return
*/
@PostMapping("/logout")
@Operation(summary = "用户注销", description = "用户注销接口")
public ApiResponse<Boolean> userLogout(HttpServletRequest request) {
if (request == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean result = userService.userLogout(request);
return ResultUtils.success(result);
}
/**
* 获取当前登录用户
*
* @param request
* @return
*/
@GetMapping("/get/login")
@Operation(summary = "获取当前登录用户", description = "获取当前登录用户信息")
public ApiResponse<UserVO> getLoginUser(HttpServletRequest request) {
User user = userService.getLoginUser(request);
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
return ResultUtils.success(userVO);
}
// endregion
}

715
新建文本文档 (2).txt Normal file
View File

@@ -0,0 +1,715 @@
配置流程
步骤一:发布智能体或 AI 应用
在智能体或 AI 应用的发布页面,选择 Chat SDK并单击发布。发布的详细流程可参考
发布应用为 Chat SDK
发布智能体到 Chat SDK
步骤二:获取安装代码
进入发布页面复制 SDK 安装代码。
步骤三:安装 SDK
你可以直接在页面中通过 script 标签的形式加载 Chat SDK 的 js 代码,将步骤二中复制好的安装代码粘贴到网页的 <body> 区域中即可。
步骤二中复制好的安装代码示例如下:
示例代码中的版本号 1.2.0-beta.15 为示例,请以 Chat SDK 最新的版本号为准版本信息请参见Chat SDK 发布历史。
步骤四:配置聊天框
安装 Chat SDK 后,您现在可以初始化客户端。在页面中通过调用 CozeWebSDK.WebChatClient 来生成聊天框,当前页面中聊天框包括 PC 和移动端两种布局样式。在 PC 端中,聊天框位于页面右下角,移动端聊天框会铺满全屏。
智能体配置
调用 CozeWebSDK.WebChatClient 时,你需要配置以下参数:
config必选参数表示智能体的配置信息。
智能体需要设置的参数如下:
参数
是否必选
数据类型
描述
type
必选
String
Chat SDK 调用的对象。 在调用智能体时,该参数保持默认值 bot。
botId
必选
String
智能体的 ID。在智能体编排页面的 URL 中,查看 bot 关键词之后的字符串就是智能体 ID。例如https://www.coze.cn/space/341****/bot/73428668*****,智能体 ID 为 73428668*****。
isIframe
可选
Boolean
是否使用 iframe方式来打开聊天框默认为 false。
true使用 iframe 打开聊天框。
false推荐非 iframe 方式打开聊天框。通过该方式可以规避小程序中 webview 的域名限制。
Chat SDK 1.2.0-beta.3 及以上版本支持该参数。
botInfo.parameters
可选
Map[String, Any]
给智能体中的自定义参数赋值并传给对话流。
如果在对话流的开始节点设置了自定义输入参数,你可以通过 parameters 参数指定自定义参数的名称和值ChatSDK 会将自定义参数的值传递给对话流。具体用法和示例代码可参考为自定义参数赋值。
仅单 Agent对话流模式的智能体支持该配置。
auth表示鉴权方式。当未配置此参数时表示不鉴权。为了账号安全建议配置鉴权请将 auth.type 设置为 token在 token 参数中指定相应的访问密钥,并通过 onRefreshToken 监听获取新的密钥,当 token 中设置的访问密钥失效时Chat SDK 能够自动重新获取新的密钥。调试场景可以直接使用准备工作中添加的访问密钥,快速体验 Chat SDK 的效果;正式上线时建议通过 SAT 或 OAuth 实现鉴权逻辑,并将获取的 OAuth 访问密钥填写在此处。
示例如下:
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
// 智能体 ID
botId: '740849137970326****',
isIframe: false,
// 给自定义参数赋值并传给对话流
botInfo: {
parameters: {
user_name: 'John'
}
}
},
auth: {
// Authentication methods, the default type is 'unauth', which means no authentication is required; it is recommended to set it to 'token', indicating authentication through PAT (Personal Access Token) or OAuth
type: 'token',
// When the type is set to 'token', it is necessary to configure a PAT (Personal Access Token) or OAuth access token for authentication.
token: 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
// When the access token expires, use a new token and set it as needed.
onRefreshToken: () => 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
}
});
如果要实现不同业务侧用户的会话隔离,即每个用户只能看到自己和智能体的对话历史,你需要将鉴权方式配置为 OAuth JWT 鉴权,通过 session_name 参数实现会话隔离,具体请参见如何实现会话隔离。
扣子应用配置
调用 CozeWebSDK.WebChatClient 时,你需要配置以下参数:
config必选参数表示应用的配置信息。
参数
是否必选
数据类型
描述
type
必选
String
Chat SDK 调用的对象。 集成扣子应用时,应设置为 app表示通过 Chat SDK 调用扣子应用。
isIframe
非必选
Boolean
是否使用 iframe方式来打开聊天框默认为 true。
true使用 iframe 打开聊天框。
false推荐非 iframe 方式打开聊天框。通过该方式可以规避小程序中 webview 的域名限制。
Chat SDK 1.2.0-beta.3 及以上版本支持该参数。
appInfo
必选
String
扣子应用的详细信息。
确保该应用已经发布为 Chat SDK。
appInfo.appId
必选
String
AI 应用的 ID。 在扣子应用中打开工作流或对话流URL 中 project-ide 参数之后的字符串就是 appId。
appInfo.workflowId
必选
String
工作流或对话流的 ID。 在扣子应用中打开工作流或对话流URL 中 workflow 参数之后的字符串就是 workflowId。
appInfo.parameters
可选
Map[String, Any]
给应用中的自定义参数赋值并传给对话流。
如果在对话流的开始节点设置了自定义输入参数,你可以通过 parameters 参数指定自定义参数的名称和值ChatSDK 会将自定义参数的值传递给对话流。具体用法和示例代码可参考为自定义参数赋值。
auth表示鉴权方式。当未配置此参数时表示不鉴权。为了账号安全建议配置鉴权请将 auth.type 设置为 token在 token 参数中指定相应的访问密钥,并通过 onRefreshToken 监听获取新的密钥,当 token 中设置的访问密钥失效时Chat SDK 能够自动重新获取新的密钥。调试场景可以直接使用准备工作中添加的访问密钥,快速体验 Chat SDK 的效果;正式上线时建议通过 SAT 或 OAuth 实现鉴权逻辑,并将获取的 OAuth 访问密钥填写在此处。
示例如下:
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
type: 'app', // 应用类型
isIframe: false, // 是否在iframe中运行
appInfo: { // 应用配置信息
appId: '744189632066042****',
workflowId: '744229754050396****',
parameters: { // 给自定义参数赋值并传给对话流
user_name: 'John'
}
}
},
auth: {
// Authentication methods, the default type is 'unauth', which means no authentication is required; it is recommended to set it to 'token', indicating authentication through PAT (Personal Access Token) or OAuth
type: 'token',
// When the type is set to 'token', it is necessary to configure a PAT (Personal Access Token) or OAuth access token for authentication.
token: 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
// When the access token expires, use a new token and set it as needed.
onRefreshToken: () => 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
}
});
步骤五:配置属性
扣子 Chat SDK 支持多种属性配置,开发者可以按需调整对话框的多种展示效果,例如展示的用户信息、对话框 UI 效果、悬浮球展示、底部文案等。
你可以在 WebChatClient 方法中添加各种属性,实现对应的效果。目前支持的属性如下:
功能
属性
说明
智能体配置
config
指定对话的智能体或扣子应用,支持设置是否使用 iframe方式来打开聊天框
鉴权
auth
指定鉴权方式。默认不鉴权,支持通过 PAT 、SAT 或 OAuth 鉴权。
用户信息
userInfo
用于设置对话框中的显示用户信息,包括对话框中的用户头像、用户昵称、用户的业务 ID。
UI 效果
ui.base
用于添加聊天窗口的整体 UI 效果,包括应用图标、页面展示模式(移动端或 PC 端)、系统语言属性、聊天框页面层级。
悬浮球
ui.asstBtn
控制是否在页面右下角展示悬浮球。默认展示,用户点击悬浮球后将弹出聊天窗口。
底部文案
ui.footer
隐藏底部文案或改为其他文案,支持在文案中设置超链接。
顶部标题栏配置
ui.header
用于控制是否展示顶部的标题栏和关闭按钮,默认展示。
聊天框
ui.chatBot
用于控制聊天框的 UI 和基础能力,包括:
标题、大小、位置等基本属性
限制在聊天框中上传文件、语音输入、显示工具调用信息
步骤六:销毁客户端
你可以添加一个 destroy 方法销毁客户端。
<script>
const client = new CozeWebSDK.WebChatClient(options);
client.destroy();
</script>
属性配置
智能体或应用配置
config 参数用于指定智能体。智能体需要设置的参数如下:
参数
是否必选
数据类型
描述
bot_id
必选
String
智能体 ID。
进入智能体的开发页面,开发页面 URL 中 bot 参数后的数字就是智能体ID。例如https://www.coze.cn/space/341****/bot/73428668*****智能体ID 为 73428668*****。
确保该智能体已经发布为 Chat SDK。
isIframe
非必选
Boolean
是否使用 iframe方式来打开聊天框默认为 true。
true使用 iframe 打开聊天框。
false推荐非 iframe 方式打开聊天框。通过该方式可以规避小程序中 webview 的域名限制。
Chat SDK 1.2.0-beta.3 及以上版本支持该参数。
扣子应用需要设置的参数如下:
参数
是否必选
数据类型
描述
type
必选
String
Chat SDK 调用的对象。
默认为 bot表示通过 Chat SDK 调用智能体。
集成扣子应用时,应设置为 app表示通过 Chat SDK 调用扣子应用。
appInfo
必选
String
扣子应用的详细信息。
确保该应用已经发布为 Chat SDK。
appInfo.appId
必选
String
AI 应用 ID。 在扣子应用中打开工作流或对话流URL 中 project-ide 参数之后的字符串是 appId。
appInfo.workflowId
必选
String
工作流或对话流 ID。 在扣子应用中打开工作流或对话流URL 中 workflow 参数之后的字符串是 workflowId。
isIframe
非必选
Boolean
是否使用 iframe方式来打开聊天框默认为 true。
true使用 iframe 打开聊天框。
false推荐非 iframe 方式打开聊天框。通过该方式可以规避小程序中 webview 的域名限制。
Chat SDK 1.2.0-beta.3 及以上版本支持该参数。
鉴权
auth 属性用于配置鉴权方式。不添加此参数时表示不鉴权,也可以通过此参数指定使用 PAT 或 OAuth 鉴权。配置说明如下:
参数
数据类型
是否必选
描述
type
String
必选
可指定为 token通过访问密钥鉴权支持的访问密钥包括 PAT、SAT 和 OAuth 访问密钥。关于访问密钥的详细说明可参考鉴权方式概述。
token
String
type为 token 时必填
type 为 token 时,指定使用的访问密钥。调试场景可以直接使用准备工作中添加的访问密钥,快速体验 Chat SDK 的效果;正式上线时建议通过 SAT 或 OAuth 实现鉴权逻辑,并将获取的 OAuth 访问密钥填写在此处。
onRefreshToken
Function
type为 token 时必填
token 中设置的访问密钥失效时,使用新密钥。建议按需设置。
以使用 PAT 鉴权为例,配置鉴权的方式如下:
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
botId: '740849137970326****',
isIframe: false,
},
auth: {
type: 'token',
token: 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
onRefreshToken: async () => 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
}
});
以使用 OAuth 鉴权为例,配置鉴权的方式如下:
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
botId: '740849137970326****',
isIframe: false,
},
auth: {
type: 'token',
token: 'czs_RQOhsc7vmUzK4bNgb7hn4wqOgRBYAO6xvpFHNbnl6RiQJX3cSXSguIhFDzgy****',
onRefreshToken: async () => 'czs_RQOhsc7vmUzK4bNgb7hn4wqOgRBYAO6xvpFHNbnl6RiQJX3cSXSguIhFDzgy****',
}
});
用户信息
userInfo 参数用于设置对话框中的显示用户信息,包括对话框中的用户头像和账号。同时,此处指定的用户 ID 也会通过发起对话 API 传递给扣子服务端。
参数
数据类型
是否必选
描述
url
String
必选
用户头像的 URL 地址,必须是一个可公开访问的地址。
nickname
String
必选
用户的昵称。
id
String
可选
用户的 ID也就是用户在你的网站或应用中的账号 ID。未指定 ID 时Chat SDK 会根据用户使用的设备随机分配一个用户 ID。
你可以在智能体的分析 > 消息链路页面查看不同用户 ID 的对话记录。详细说明可参考消息日志。
配置示例如下:
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
botId: '740849137970326****',
isIframe: false,
},
auth: {
type: 'token',
token: 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
onRefreshToken: async () => 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
},
// 用户信息
userInfo: {
id: '12345',
url: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
nickname: 'UserA',
},
});
UI 效果
ui.base 参数用于添加聊天窗口的整体 UI 效果,包括应用图标、页面展示模式、语言属性等。
参数
数据类型
是否必选
描述
icon
String
可选
应用图标,必须是一个可访问的公开 URL 地址。如果不设置,则使用默认扣子图标。
扣子企业版支持将 icon 修改为自定义的品牌 Logo扣子团队版和个人版不支持自定义品牌。
layout
String
可选
聊天框窗口的布局风格,支持设置为:
mobile移动端风格聊天框窗口铺满移动设备全屏。
pcPC 端风格,聊天框窗口位于页面右下角。
未设置此参数时,系统会自动识别设备,设置相应的布局风格。
lang
String
可选
系统语言,例如工具提示信息的语言。
en默认英语
zh-CN中文
zIndex
number
可选
开发者自行控制用于调整聊天框的页面层级。详细信息可参考MDN-zIndex。
示例代码如下:
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
botId: '740849137970326****',
isIframe: false,
},
auth: {
type: 'token',
token: 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
onRefreshToken: async () => 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
},
userInfo: {
id: '123',
url: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
nickname: '3qweqweqwe',
},
ui: {
base: {
icon: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
layout: 'pc',
zIndex: 1000,
}
},
});
悬浮球
asstBtn 参数用于控制是否在页面右下角展示悬浮球。默认展示,用户点击悬浮球后将弹出聊天窗口。
若设置为不展示悬浮球,开发者需要通过以下方法控制聊天框的展示或隐藏效果。
显示聊天框cozeWebSDK.showChatBot()
隐藏聊天框cozeWebSDK.hideChatBot()
参数
数据类型
是否必选
描述
isNeed
boolean
是否在页面右下角展示悬浮球。
true默认展示悬浮球。
false不展示悬浮球。
以不展示悬浮球为例,示例代码如下:
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
botId: '740849137970326****',
isIframe: false,
},
auth: {
type: 'token',
token: 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
onRefreshToken: async () => 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
},
userInfo: {
id: '123',
url: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
nickname: '3qweqweqwe',
},
ui: {
base: {
icon: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
layout: 'pc',
zIndex: 1000,
},
asstBtn: {
isNeed: false,
},
},
});
不展示悬浮球时,你可以通过以下方式显示聊天框或隐藏聊天框。
<body>
<button onClick={() => {
cozeWebSDK.showChatBot();
}}>显示聊天框</button>
<button onClick={() => {
cozeWebSDK.hideChatBot();
}}>隐藏聊天框</button>
</body>
底部文案
聊天框底部会展示对话服务的提供方信息默认为Powered by coze. AI-generated content for reference only.。开发者通过 footer 参数隐藏此文案或改为其他文案,支持在文案中设置超链接。
底部文案默认展示效果如下:
footer 参数配置说明如下:
参数
数据类型
是否必选
描述
isShow
boolean
可选
是否展示底部文案模块。
true展示。此时需要通过 expressionText 和 linkvars 设置具体文案和超链接。
false不展示。expressionText 和 linkvars 设置不生效。
expressionText
String
可选
底部显示的文案信息。支持添加以下格式的内容:
纯文本:直接输入文本信息。
链接:通过双大括号({{***}} )设置链接。双大括号中的字段会被替换 linkvars中的内容替换掉。仅支持设置一个链接。
linkvars
Object
可选
底部文案中的链接文案与链接地址。
替换规则name与"{{***}}"中的字段保持对应关系,同时用 a 标签替换掉该文本text 为 a 标签的文案link 为跳转的链接地址。
配置示例如下:
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
botId: '740849137970326****',
isIframe: false,
},
auth: {
type: 'token',
token: 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
onRefreshToken: async () => 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
},
userInfo: {
id: '123',
url: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
nickname: '3qweqweqwe',
},
ui: {
base: {
icon: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
layout: 'pc',
zIndex: 1000,
},
asstBtn: {
isNeed: true,
},
footer: {
isShow: true,
expressionText: 'Powered by {{name}}&{{name1}}',
linkvars: {
name: {
text: 'A',
link: 'https://www.test1.com'
},
name1: {
text: 'B',
link: 'https://www.test2.com'
}
}
}
}
},
});
此配置的对应的展示效果如下:
顶部标题栏配置
聊天框顶部默认展示智能体名称、icon、及关闭按钮。展示效果类似如下
Chat SDK 1.1.0-beta.3 及以上版本支持该配置。
您可以通过 header 参数配置是否展示顶部标题栏和关闭按钮header 参数配置说明如下:
参数
数据类型
是否必选
描述
isShow
Boolean
可选
是否展示顶部标题栏,默认为 true。
true展示。
false不展示。
isNeedClose
String
可选
是否展示顶部的关闭按钮,默认为 true。
true展示。
false不展示。
配置示例如下:
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
botId: '740849137970326****',
isIframe: false,
},
auth: {
type: 'token',
token: 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
onRefreshToken: async () => 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGA*********',
},
userInfo: {
id: '123',
url: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
nickname: '3qweqweqwe',
},
ui: {
base: {
icon: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
layout: 'pc',
zIndex: 1000,
},
asstBtn: {
isNeed: true,
},
header: {
isShow: true,
isNeedClose: true,
},
},
});
聊天框
chatBot 参数用于控制聊天框的 UI 和基础能力,包括标题、大小、位置等基本属性,还可以指定是否支持在聊天框中上传文件。此参数同时提供多个回调方法,用于同步聊天框显示、隐藏等事件通知。
配置说明如下:
参数
数据类型
是否必选
描述
title
String
可选
聊天框的标题,若未配置,使用默认名称 Coze Bot。
uploadable
Boolean
可选
是否开启聊天框的上传能力。
true默认聊天框支持上传文件。选择此选项时应确认模型具备处理文件或图片的能力例如使用的是多模态模型或添加了多模态插件。
false聊天框不支持上传文件。
width
Number
可选
PC 端窗口的宽度,单位为 px默认为 460。
建议综合考虑各种尺寸的屏幕,设置一个合适的宽度。
此配置仅在 PC 端且未设置 el 参数时生效。
el
Element
可选
设置聊天框放置位置的容器Element
开发者应自行设置聊天框高度、宽度,聊天框会占满整个元素空间。
Chat SDK 会自动控制聊天框的显示隐藏,但是对于宿主的 element 元素不会做控制,开发者按需在 onHide、onShow 回调时机中来控制宿主元素的显示隐藏。
isNeedAudio
Boolean
可选
设置聊天框中是否允许语音输入。
true支持用户通过语音输入。
false仅支持打字输入不支持语音输入。
默认值:
非 Iframe 模式(聊天框集成在主页面中): 默认值为 true。
ifreme 模式(聊天框作为独立窗口嵌入主页面): 默认值为 false。
isNeedFunctionCallMessage
Boolean
可选
设置是否在聊天框中显示插件工具调用的信息。
true默认值聊天框中将显示调用的插件工具。
false聊天框中不显示插件工具调用的信息。
相关回调:
onHide当聊天框隐藏的时候会回调该方法。
onShow: 当聊天框显示的时候,会回调该方法。
onBeforeShow: 聊天框显示前调用,如果返回了 false则不会显示。支持异步函数。
onBeforeHide: 聊天框隐藏前调用,如果返回了 false则不会隐藏。支持异步函数。
在以下示例中,聊天框标题为 Kids' Playmate | Snowy并开启上传文件功能。
对应的代码示例如下:
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
botId: '740849137970326****',
isIframe: false,
},
auth: {
type: 'token',
token: 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
onRefreshToken: async () => 'pat_zxzSAzxawer234zASNElEglZxcmWJ5ouCcq12gsAAsqJGALlq7hcOqMcPFV3wEVDiqjrg****',
},
userInfo: {
id: '123',
url: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
nickname: '3qweqweqwe',
},
ui: {
base: {
icon: 'https://lf-coze-web-cdn.coze.cn/obj/coze-web-cn/obric/coze/favicon.1970.png',
layout: 'pc',
zIndex: 1000,
},
asstBtn: {
isNeed: true,
},
footer: {
isShow: true,
expressionText: 'Powered by {{name}}&{{name1}}', linkvars: {
name: {
text: 'A',
link: 'https://www.test1.com'
},
name1: {
text: 'B',
link: 'https://www.test2.com'
}
}
},
chatBot: {
title: "Kids' Playmate | Snowy",
uploadable: true,
width: 800,
el: undefined,
onHide: () => {
// todo...
},
onShow: () => {
// todo...
},
},
},
});
通过 chatbot 的 el 参数设置组件的示例代码如下:
相关操作
更新 SDK 版本
扣子 Chat SDK 将持续更新迭代,支持丰富的对话能力和展示效果。你可以在 Chat SDK 的 script 标签中指定 Chat SDK 的最新版本号,体验和使用最新的 Chat SDK 对话效果。
在以下代码中,将 {{version}} 部分替换为 Chat SDK 的最新版本号。你可以在Chat SDK 发布历史中查看最新版本号。
解绑 Chat SDK
如果不再需要通过 Chat SDK 使用 AI 应用,可以在发布页面点击解绑按钮。一旦解绑,智能体或应用就无法通过集成的 Web 应用程序使用。 如果您想恢复 Web 应用程序的访问,需要再次将智能体或应用发布为 Chat SDK。
示例
以下是一段完整的 Chat SDK 调用智能体的代码示例。
<!doctype html>
<html lang="en">
<head>
<style>
/* 自定义悬浮入口的位置 */
#position_demo {
position: absolute;
right: 10px;
bottom: 20px;
}
</style>
</head>
<body>
<h1>Hello World</h1>
<div id="position_demo"></div>
<script src="https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/1.2.0-beta.8/libs/cn/index.js"></script>
<script>
const cozeWebSDK = new CozeWebSDK.WebChatClient({
config: {
// 智能体 ID
botId: '742477211246629****',
},
auth: {
//鉴权方式默认type 为unauth表示不鉴权建议设置为 token表示通过PAT或OAuth鉴权
type: 'token',
//type 为 token 时需要设置PAT或OAuth访问密钥
token: 'pat_82GrrdfNWPMnlcY58r98Rzqiev2s5NyrqCR8Ypbh5hOomzZN4ivb325PZAd****',
//访问密钥过期时,使用的新密钥,可以按需设置。
onRefreshToken: () => 'pat_82GrrdfNWPMnlcY58r98Rzqiev2s5NyrqCR8Ypbh5hOomzZN4ivb325PZAdZ****',
},ui:{
chatBot: {
title: "智能客服",
uploadable: true
}}
});
</script>
</body>
</html>
相关文档