彩票助手版本1.0
402
Controller.txt
Normal 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
30
lottery-app/.gitignore
vendored
Normal 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
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
134
lottery-app/README.md
Normal 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+语法
|
||||||
|
- 组件采用SFC(Single 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
@@ -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>
|
||||||
8
lottery-app/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
3662
lottery-app/package-lock.json
generated
Normal file
25
lottery-app/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
lottery-app/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
500
lottery-app/src/App.vue
Normal 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>
|
||||||
663
lottery-app/src/api/index.js
Normal 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
|
||||||
BIN
lottery-app/src/assets/3D.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
lottery-app/src/assets/7lecai.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
lottery-app/src/assets/7星彩.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
lottery-app/src/assets/backend.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
lottery-app/src/assets/banner/backend1.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
lottery-app/src/assets/banner/banner.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
lottery-app/src/assets/banner/home.png
Normal file
|
After Width: | Height: | Size: 207 B |
BIN
lottery-app/src/assets/banner/kaijiang.png
Normal file
|
After Width: | Height: | Size: 257 B |
BIN
lottery-app/src/assets/banner/wode.png
Normal file
|
After Width: | Height: | Size: 218 B |
86
lottery-app/src/assets/base.css
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
'Fira Sans',
|
||||||
|
'Droid Sans',
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
BIN
lottery-app/src/assets/daletou.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
lottery-app/src/assets/font/font1.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
lottery-app/src/assets/font/font2.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
lottery-app/src/assets/font/font3.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
lottery-app/src/assets/font/font4.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
lottery-app/src/assets/font/font5.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
4
lottery-app/src/assets/icon/ai.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg t="1753944963328" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6211" width="32" height="32">
|
||||||
|
<path d="M603.904 244.992c134.4 0 243.3536 108.9536 243.3536 243.3536v202.8032c0 134.4-108.9536 243.3536-243.3536 243.3536H320c-134.4 0-243.3536-108.9536-243.3536-243.3536V488.3456c0-134.4 108.9536-243.3536 243.3536-243.3536h283.904z m0 81.1008H320c-89.6 0-162.2528 72.6528-162.2528 162.2528v202.8032c0 89.6 72.6528 162.2528 162.2528 162.2528h283.904c89.6 0 162.2528-72.6528 162.2528-162.2528V488.3456c0-89.6-72.6528-162.2528-162.2528-162.2528z" fill="#1296db" p-id="6212"></path>
|
||||||
|
<path d="M340.224 508.6208c27.0336 0 40.5504 13.5168 40.5504 40.5504v81.1008c0 27.0336-13.5168 40.5504-40.5504 40.5504-27.0336 0-40.5504-13.5168-40.5504-40.5504v-81.1008c0-27.0336 13.5168-40.5504 40.5504-40.5504zM583.2192 501.3504c15.2576-11.4176 36.864-8.3456 48.2816 6.912a34.4832 34.4832 0 0 1-6.912 48.2816l-44.3904 33.28 44.3904 33.28a34.53952 34.53952 0 0 1 9.472 44.3392l-2.56 3.9424c-11.4176 15.2576-33.024 18.3296-48.2816 6.912l-59.4944-44.5952c-13.7728-10.3424-21.9136-26.5728-21.9136-43.8272s8.0896-33.4848 21.9136-43.8272l59.4944-44.5952zM883.5072 261.12l-19.7632 47.3088c-2.7648 6.656-9.2672 10.9568-16.4864 10.9568s-13.6704-4.3008-16.4864-10.9568L811.008 261.12a44.416 44.416 0 0 0-19.7632-21.9648l-34.8672-19.0976c-5.7344-3.1232-9.2672-9.1136-9.2672-15.6672s3.5328-12.544 9.2672-15.6672l34.8672-19.0464a44.89728 44.89728 0 0 0 19.7632-21.9648l19.7632-47.3088c2.7648-6.656 9.2672-10.9568 16.4864-10.9568s13.6704 4.3008 16.4864 10.9568l19.7632 47.3088a44.416 44.416 0 0 0 19.7632 21.9648l34.8672 19.0976c5.7344 3.1232 9.2672 9.1136 9.2672 15.6672s-3.584 12.544-9.2672 15.6672l-34.8672 19.0464c-8.9088 4.864-15.872 12.6464-19.7632 21.9648z" fill="#1296db" p-id="6213"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
1
lottery-app/src/assets/icon/bzzx.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1753948292582" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18768" width="32" height="32"><path d="M891.033093 98.392071 649.86931 98.392071c-56.248614 0-106.30472 27.178229-137.783303 68.977658-31.478582-41.799429-81.534688-68.977658-137.783303-68.977658L132.966907 98.392071c-38.015118 0-68.977658 30.96254-68.977658 68.977658l0 637.484294c0 38.015118 30.96254 68.977658 68.977658 68.977658l253.376785 0c33.88678 33.026709 78.782463 51.604233 125.742315 51.604233s91.855535-18.577524 125.742315-51.604233L891.033093 873.831682c38.015118 0 68.977658-30.96254 68.977658-68.977658L960.010751 167.36973C960.010751 129.354611 929.048211 98.392071 891.033093 98.392071zM891.033093 804.854023 622.863094 804.854023c-9.976818 0-19.609609 4.300353-26.146145 12.040988-21.501764 25.11406-52.464304 39.563245-84.630942 39.563245-32.166639 0-63.129179-14.449185-84.630942-39.563245-6.536536-7.740635-16.169326-12.040988-26.146145-12.040988l-268.342012 0L132.966907 167.36973l241.163783 0c56.936671 0 103.38048 46.44381 103.38048 103.38048l0 292.94003c0 19.093566 15.48127 34.402822 34.402822 34.402822 19.093566 0 34.402822-15.48127 34.402822-34.402822L546.316815 270.75021c0-56.936671 46.44381-103.38048 103.38048-103.38048L891.033093 167.36973 891.033093 804.854023z" fill="#2c2c2c" p-id="18769"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
lottery-app/src/assets/icon/dhjl.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1753948236353" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17518" width="32" height="32"><path d="M828.61 878.22H195.46c-42.09 0-76.34-34.25-76.34-76.34V699.84c0-25.27 14.05-48.16 36.67-59.74 37.8-19.36 63.2-69.43 63.2-124.59 0-55.01-25.31-105.04-62.99-124.48-22.75-11.74-36.88-34.81-36.88-60.21V228.31c0-42.09 34.25-76.34 76.34-76.34h633.16c42.09 0 76.33 34.24 76.33 76.33v102.78c0 25.3-14.06 48.27-36.7 59.95-37.68 19.44-62.99 69.46-62.99 124.48 0 55 25.3 105.02 62.96 124.47 22.66 11.7 36.73 34.7 36.73 60.01v101.89c-0.01 42.1-34.25 76.34-76.34 76.34zM195.12 705.25v96.63c0 0.19 0.15 0.34 0.34 0.34h633.16c0.18 0 0.33-0.15 0.33-0.33v-96.74c-60.72-33.82-99.69-107.62-99.69-189.63 0-82.02 38.97-155.81 99.69-189.63V228.3c0-0.18-0.15-0.33-0.33-0.33H195.46c-0.19 0-0.34 0.15-0.34 0.34v97.47c60.82 33.78 99.87 107.63 99.87 189.73 0 82.16-39.03 155.96-99.87 189.74z" p-id="17519" fill="#2c2c2c"></path><path d="M643.78 421.79H380.22c-20.99 0-38-17.01-38-38s17.01-38 38-38h263.57c20.99 0 38 17.01 38 38-0.01 20.99-17.02 38-38.01 38zM643.78 616.58H380.22c-20.99 0-38-17.01-38-38s17.01-38 38-38h263.57c20.99 0 38 17.01 38 38-0.01 20.99-17.02 38-38.01 38z" p-id="17520" fill="#2c2c2c"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
lottery-app/src/assets/icon/gywm.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1753948313670" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19844" width="32" height="32"><path d="M513 5C232.9 5 5 232.9 5 513s227.9 508 508 508 508-227.9 508-508S793.1 5 513 5zM217.4 867.4c63.9-145 177.4-234.3 300.3-234.3 121.6 0 232.5 85.3 297.6 228.2-81.1 70.5-186.7 113.5-302.3 113.5-112.4 0-215.4-40.4-295.6-107.4zM513 560.8c-73.6 0-133.6-59.9-133.6-133.6s60-133.6 133.6-133.6 133.6 59.9 133.6 133.6-60 133.6-133.6 133.6z m337.3 266.6c-62.2-127.2-159.4-210.7-269.7-233.8 65.7-26.8 112.2-91.2 112.2-166.5 0-99.1-80.6-179.8-179.8-179.8s-179.8 80.6-179.8 179.8c0 76.6 48.3 142.1 116 167.9-109.9 25.5-207.1 111.6-267.5 239-80.6-83.1-130.5-196.3-130.5-321C51.2 258.4 258.4 51.2 513 51.2S974.8 258.4 974.8 513c0 121.5-47.5 231.9-124.5 314.4z" fill="#2c2c2c" p-id="19845"></path></svg>
|
||||||
|
After Width: | Height: | Size: 840 B |
BIN
lottery-app/src/assets/icon/login/mima.png
Normal file
|
After Width: | Height: | Size: 381 B |
BIN
lottery-app/src/assets/icon/login/nicheng.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
lottery-app/src/assets/icon/login/shouji.png
Normal file
|
After Width: | Height: | Size: 372 B |
BIN
lottery-app/src/assets/icon/login/yingcang.png
Normal file
|
After Width: | Height: | Size: 431 B |
BIN
lottery-app/src/assets/icon/login/zhanghao.png
Normal file
|
After Width: | Height: | Size: 583 B |
BIN
lottery-app/src/assets/icon/login/zhanshi.png
Normal file
|
After Width: | Height: | Size: 775 B |
1
lottery-app/src/assets/icon/sjfx.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1753945117321" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7804" width="32" height="32"><path d="M259.462 675.528H118.034a64.55 64.55 0 0 0-62.502 66.128V957.83a64.55 64.55 0 0 0 62.502 66.127h141.428a64.592 64.592 0 0 0 62.715-66.127V741.656a64.592 64.592 0 0 0-62.715-66.128z m0 304.401H118.034a19.198 19.198 0 0 1-19.199-22.12v-213.53a21.332 21.332 0 0 1 19.199-24.488h141.428a22.12 22.12 0 0 1 20.691 24.488v213.55a20.052 20.052 0 0 1-20.691 22.121z m678.343-668.595H796.377a65.061 65.061 0 0 0-62.501 67.301V956.72a65.04 65.04 0 0 0 62.501 67.28h141.428a65.19 65.19 0 0 0 62.502-67.28V378.635a65.061 65.061 0 0 0-62.502-67.3z m2.133 665.78H796.377a16.425 16.425 0 0 1-17.705-20.33V385.205a19.198 19.198 0 0 1 17.705-23.827h141.428a19.54 19.54 0 0 1 17.919 23.827v571.58c0 14.227-3.627 20.328-15.786 20.328zM440.993 476.866a64.955 64.955 0 0 0-62.714 66.81v413.449a64.933 64.933 0 0 0 62.714 66.81h141.642a64.933 64.933 0 0 0 62.715-66.81V543.677a64.955 64.955 0 0 0-62.715-66.81H440.993zM582.635 977.88H440.993a17.77 17.77 0 0 1-19.198-20.755V543.677a19.05 19.05 0 0 1 19.198-22.376h141.642a19.326 19.326 0 0 1 19.625 22.376v413.449a18.046 18.046 0 0 1-19.625 20.755zM561.73 447.493L904.528 97.272l62.928 65.061L975.99 0.021h-173l63.356 67.045-333.84 334.053-176.411-181.767a41.831 41.831 0 0 0-58.662 0L36.334 488.13a29.694 29.694 0 0 0-3.627 45.842c13.44 13.908 34.13-1.131 49.703-17.215L330.922 264.81l173 179.889a39.271 39.271 0 0 0 28.584 12.052 38.14 38.14 0 0 0 29.224-9.172z" p-id="7805" fill="#2c2c2c"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1
lottery-app/src/assets/icon/tcjl.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1753948072083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9272" width="32" height="32"><path d="M754.346667 212.906667v-81.066667s7.68-58.453333-65.706667-58.453333H126.293333s-48.213333-2.56-48.213333 48.213333v706.56s5.12 58.453333 60.586667 58.453333h144.213333s27.733333 5.12 27.733333 35.413334-27.733333 30.293333-27.733333 30.293333h-179.2S7.253333 934.4 7.253333 846.08V129.28S-2.986667 5.12 128.853333 5.12h582.4s103.68 0 106.24 96.426667 2.56 111.36 2.56 111.36-5.12 30.293333-32.853333 30.293333c-27.733333 2.56-32.853333-25.6-32.853333-30.293333z m0 0" p-id="9273" fill="#2c2c2c"></path><path d="M619.946667 506.453333v136.96s-2.56 15.36 12.8 30.293334l134.4 134.4s22.613333 10.24 40.533333-7.68c20.053333-20.053333-2.56-45.653333-2.56-45.653334l-111.36-116.48s-7.68-5.12-7.68-27.733333v-106.24s-10.24-27.733333-32.853333-27.733333c-23.04 2.133333-33.28 22.186667-33.28 29.866666z m0 0" p-id="9274" fill="#2c2c2c"></path><path d="M686.08 346.88c-184.746667 0-336.64 149.333333-336.64 336.64 0 184.746667 149.333333 336.64 336.64 336.64 184.746667 0 336.64-149.333333 336.64-336.64-2.56-187.306667-151.893333-336.64-336.64-336.64z m0 608c-149.333333 0-270.933333-121.6-270.933333-270.933333 0-149.333333 121.6-270.933333 270.933333-270.933334 149.333333 0 270.933333 121.6 270.933333 270.933334 0 149.333333-121.6 270.933333-270.933333 270.933333zM290.986667 478.72H199.68c-15.36 0-27.733333-12.8-27.733333-27.733333v-15.36c0-15.36 12.8-27.733333 27.733333-27.733334h91.306667c15.36 0 27.733333 12.8 27.733333 27.733334v15.36c0 17.493333-12.8 27.733333-27.733333 27.733333z m339.2-202.666667H199.68c-15.36 0-27.733333-12.8-27.733333-27.733333v-15.36c0-15.36 12.8-27.733333 27.733333-27.733333h427.946667c15.36 0 27.733333 12.8 27.733333 27.733333v15.36c0 14.933333-12.373333 27.733333-25.173333 27.733333z m0 0" p-id="9275" fill="#2c2c2c"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
lottery-app/src/assets/icon/tuichu.png
Normal file
|
After Width: | Height: | Size: 633 B |
BIN
lottery-app/src/assets/icon/zuanshi.png
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
lottery-app/src/assets/kuaile8.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
1
lottery-app/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
24
lottery-app/src/assets/main.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除可能影响布局的媒体查询 */
|
||||||
BIN
lottery-app/src/assets/pailie3.png
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
lottery-app/src/assets/pailie5.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
lottery-app/src/assets/shuangseqiu.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
lottery-app/src/assets/weixin.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
44
lottery-app/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
msg: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="greetings">
|
||||||
|
<h1 class="green">{{ msg }}</h1>
|
||||||
|
<h3>
|
||||||
|
You’ve successfully created a project with
|
||||||
|
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
position: relative;
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
94
lottery-app/src/components/TheWelcome.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<script setup>
|
||||||
|
import WelcomeItem from './WelcomeItem.vue'
|
||||||
|
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||||
|
import ToolingIcon from './icons/IconTooling.vue'
|
||||||
|
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||||
|
import CommunityIcon from './icons/IconCommunity.vue'
|
||||||
|
import SupportIcon from './icons/IconSupport.vue'
|
||||||
|
|
||||||
|
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<DocumentationIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Documentation</template>
|
||||||
|
|
||||||
|
Vue’s
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||||
|
provides you with all information you need to get started.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<ToolingIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Tooling</template>
|
||||||
|
|
||||||
|
This project is served and bundled with
|
||||||
|
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||||
|
recommended IDE setup is
|
||||||
|
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||||
|
+
|
||||||
|
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
|
||||||
|
you need to test your components and web pages, check out
|
||||||
|
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||||
|
and
|
||||||
|
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||||
|
/
|
||||||
|
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
More instructions are available in
|
||||||
|
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||||
|
>.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<EcosystemIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Ecosystem</template>
|
||||||
|
|
||||||
|
Get official tools and libraries for your project:
|
||||||
|
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||||
|
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||||
|
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||||
|
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||||
|
you need more resources, we suggest paying
|
||||||
|
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||||
|
a visit.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<CommunityIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Community</template>
|
||||||
|
|
||||||
|
Got stuck? Ask your question on
|
||||||
|
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||||
|
(our official Discord server), or
|
||||||
|
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||||
|
>StackOverflow</a
|
||||||
|
>. You should also follow the official
|
||||||
|
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||||
|
Bluesky account or the
|
||||||
|
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||||
|
X account for latest news in the Vue world.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<SupportIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Support Vue</template>
|
||||||
|
|
||||||
|
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||||
|
us by
|
||||||
|
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||||
|
</WelcomeItem>
|
||||||
|
</template>
|
||||||
87
lottery-app/src/components/WelcomeItem.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="item">
|
||||||
|
<i>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
</i>
|
||||||
|
<div class="details">
|
||||||
|
<h3>
|
||||||
|
<slot name="heading"></slot>
|
||||||
|
</h3>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
place-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
color: var(--color-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.item {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
top: calc(50% - 25px);
|
||||||
|
left: -26px;
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background: var(--color-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:before {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:after {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:first-of-type:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:last-of-type:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
lottery-app/src/components/icons/IconCommunity.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
lottery-app/src/components/icons/IconDocumentation.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
lottery-app/src/components/icons/IconEcosystem.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
lottery-app/src/components/icons/IconSupport.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
19
lottery-app/src/components/icons/IconTooling.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
class="iconify iconify--mdi"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
46
lottery-app/src/main.js
Normal 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')
|
||||||
269
lottery-app/src/router/index.js
Normal 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
|
||||||
215
lottery-app/src/store/user.js
Normal 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();
|
||||||
|
}
|
||||||
253
lottery-app/src/styles/global.css
Normal 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;
|
||||||
|
}
|
||||||
636
lottery-app/src/views/AIAssistant.vue
Normal 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>
|
||||||
257
lottery-app/src/views/AboutUs.vue
Normal 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>
|
||||||
546
lottery-app/src/views/DataAnalysis.vue
Normal 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>
|
||||||
699
lottery-app/src/views/ExcelImportManagement.vue
Normal 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>• 需要包含T1、T2、T3、T4、T5、T6、T7工作表的Excel文件</p>
|
||||||
|
<p>• 导入红球、蓝球、接续系数和组合系数数据到相应的数据库表</p>
|
||||||
|
<p>• 适用于系统初始化或全量数据更新</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<h4>🎯 开奖数据导入(覆盖):</h4>
|
||||||
|
<p>• 需要包含T10工作表的Excel文件</p>
|
||||||
|
<p>• 清空lottery_draws表的现有数据,重新导入</p>
|
||||||
|
<p>• 适用于完全替换开奖数据</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<h4>➕ 开奖数据追加:</h4>
|
||||||
|
<p>• 需要包含T10工作表的Excel文件</p>
|
||||||
|
<p>• 保留现有数据,只添加新的开奖记录</p>
|
||||||
|
<p>• 自动跳过重复的期号,适用于增量更新</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
341
lottery-app/src/views/ExchangeRecords.vue
Normal 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>
|
||||||
163
lottery-app/src/views/HelpCenter.vue
Normal 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>
|
||||||
2217
lottery-app/src/views/Home.vue
Normal file
755
lottery-app/src/views/LineAnalysis.vue
Normal 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>
|
||||||
1014
lottery-app/src/views/Login.vue
Normal file
823
lottery-app/src/views/LotteryInfo.vue
Normal 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>
|
||||||
610
lottery-app/src/views/LotterySelection.vue
Normal 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>
|
||||||
121
lottery-app/src/views/NotFound.vue
Normal 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>
|
||||||
383
lottery-app/src/views/PredictRecords.vue
Normal 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>
|
||||||
2355
lottery-app/src/views/Profile.vue
Normal file
875
lottery-app/src/views/Register.vue
Normal 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>
|
||||||
848
lottery-app/src/views/ResetPassword.vue
Normal 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>
|
||||||
847
lottery-app/src/views/SurfaceAnalysis.vue
Normal 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>
|
||||||
420
lottery-app/src/views/TableAnalysis.vue
Normal 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>
|
||||||
666
lottery-app/src/views/TrendAnalysis.vue
Normal 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>
|
||||||
195
lottery-app/src/views/UserAgreement.vue
Normal 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>  在开始使用我们的服务之前,请您(用户)仔细阅读并充分理解本《用户服务协议》(以下简称 "本协议")的全部内容。本协议是您(用户)与西安精彩数据服务社之间关于使用本服务的法律协议,一旦您(用户)使用本服务,即表示您(用户)已同意接受本协议的约束。如果您(用户)不同意本协议的任何条款,请不要使用本服务。</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>
|
||||||
11
lottery-app/src/views/VipCodeManagement.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
422
lottery-app/src/views/admin/AdminLogin.vue
Normal 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>
|
||||||
675
lottery-app/src/views/admin/Dashboard.vue
Normal 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>
|
||||||
8
lottery-app/src/views/admin/ExcelImportManagement.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
435
lottery-app/src/views/admin/OperationHistory.vue
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
<template>
|
||||||
|
<div class="operation-history">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1>操作历史管理</h1>
|
||||||
|
<p>查看和管理系统操作历史记录</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选器 -->
|
||||||
|
<el-card class="filter-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-icon><Filter /></el-icon>
|
||||||
|
<span>筛选选项</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :inline="true" :model="filterForm" class="filter-form">
|
||||||
|
<el-form-item label="关键词">
|
||||||
|
<el-input
|
||||||
|
v-model="filterForm.keyword"
|
||||||
|
placeholder="搜索详细信息"
|
||||||
|
clearable
|
||||||
|
@input="handleFilter"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="操作模块">
|
||||||
|
<div class="custom-select-wrapper">
|
||||||
|
<select
|
||||||
|
v-model="filterForm.operationModule"
|
||||||
|
class="custom-select"
|
||||||
|
@change="handleFilter"
|
||||||
|
>
|
||||||
|
<option value="">全部模块</option>
|
||||||
|
<option value="0">会员码管理</option>
|
||||||
|
<option value="1">Excel导入</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="操作结果">
|
||||||
|
<div class="custom-select-wrapper">
|
||||||
|
<select
|
||||||
|
v-model="filterForm.operationResult"
|
||||||
|
class="custom-select"
|
||||||
|
@change="handleFilter"
|
||||||
|
>
|
||||||
|
<option value="">全部结果</option>
|
||||||
|
<option value="成功">成功</option>
|
||||||
|
<option value="失败">失败</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleFilter">搜索</el-button>
|
||||||
|
<el-button @click="resetFilter">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 操作历史列表 -->
|
||||||
|
<el-card class="history-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
<span>操作历史列表</span>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button @click="refreshHistory">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="historyList"
|
||||||
|
v-loading="loading"
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="operationTime" label="操作时间" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.operationTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="operationModule" label="操作模块" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag>{{ getModuleText(row.operationModule) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="operationType" label="操作类型" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getOperationType(row.operationType)">
|
||||||
|
{{ row.operationType }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="userName" label="操作人" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.userName || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="operationResult" label="操作结果" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.operationResult === '成功' ? 'success' : 'danger'">
|
||||||
|
{{ row.operationResult }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="operationDetail" label="详细信息" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.operationDetail || row.resultMessage || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.current"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Clock,
|
||||||
|
Refresh,
|
||||||
|
Search,
|
||||||
|
Filter
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { lotteryApi } from '../../api/index.js'
|
||||||
|
import { userStore } from '../../store/user.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'OperationHistory',
|
||||||
|
components: {
|
||||||
|
Clock,
|
||||||
|
Refresh,
|
||||||
|
Search,
|
||||||
|
Filter
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
// 筛选表单
|
||||||
|
const filterForm = reactive({
|
||||||
|
operationModule: '',
|
||||||
|
operationResult: '',
|
||||||
|
keyword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 操作历史
|
||||||
|
const historyList = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadOperationHistory()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载操作历史
|
||||||
|
const loadOperationHistory = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 构建查询参数
|
||||||
|
const params = {
|
||||||
|
operationModule: filterForm.operationModule,
|
||||||
|
operationResult: filterForm.operationResult,
|
||||||
|
keyword: filterForm.keyword
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用统一接口获取操作历史
|
||||||
|
const response = await lotteryApi.getOperationHistoryList(params)
|
||||||
|
console.log('操作历史接口响应:', response)
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
// 处理响应数据
|
||||||
|
const data = response.data || []
|
||||||
|
|
||||||
|
// 简单的前端分页
|
||||||
|
const startIndex = (pagination.current - 1) * pagination.size
|
||||||
|
const endIndex = startIndex + pagination.size
|
||||||
|
|
||||||
|
historyList.value = data.slice(startIndex, endIndex)
|
||||||
|
pagination.total = data.length
|
||||||
|
} else {
|
||||||
|
ElMessage.error('获取操作历史失败: ' + (response?.message || '未知错误'))
|
||||||
|
historyList.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载操作历史失败:', error)
|
||||||
|
ElMessage.error('加载操作历史失败: ' + (error?.message || '未知错误'))
|
||||||
|
historyList.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新历史
|
||||||
|
const refreshHistory = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
loadOperationHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选处理
|
||||||
|
const handleFilter = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
loadOperationHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置筛选
|
||||||
|
const resetFilter = () => {
|
||||||
|
filterForm.operationModule = ''
|
||||||
|
filterForm.operationResult = ''
|
||||||
|
filterForm.keyword = ''
|
||||||
|
pagination.current = 1
|
||||||
|
loadOperationHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size
|
||||||
|
pagination.current = 1
|
||||||
|
loadOperationHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (current) => {
|
||||||
|
pagination.current = current
|
||||||
|
loadOperationHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取操作模块文本
|
||||||
|
const getModuleText = (module) => {
|
||||||
|
const modules = {
|
||||||
|
0: '会员码管理',
|
||||||
|
1: 'Excel导入',
|
||||||
|
2: '用户管理'
|
||||||
|
}
|
||||||
|
return modules[module] || '未知模块'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取操作类型标签样式
|
||||||
|
const getOperationType = (type) => {
|
||||||
|
const types = {
|
||||||
|
'完整数据导入': 'primary',
|
||||||
|
'开奖数据覆盖导入': 'warning',
|
||||||
|
'开奖数据追加': 'success',
|
||||||
|
'生成会员码': 'info',
|
||||||
|
'删除会员码': 'danger'
|
||||||
|
}
|
||||||
|
return types[type] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
filterForm,
|
||||||
|
historyList,
|
||||||
|
loading,
|
||||||
|
pagination,
|
||||||
|
handleFilter,
|
||||||
|
resetFilter,
|
||||||
|
refreshHistory,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
getModuleText,
|
||||||
|
getOperationType,
|
||||||
|
formatDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.operation-history {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面标题 */
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: linear-gradient(135deg, #3a7bd5, #00d2ff);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content p {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片样式 */
|
||||||
|
.filter-card, .history-card {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header .el-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 筛选表单 */
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义下拉框样式 */
|
||||||
|
.custom-select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23606266'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 8px center;
|
||||||
|
background-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select:hover {
|
||||||
|
border-color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页 */
|
||||||
|
.pagination-wrapper {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.operation-history {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form .el-form-item {
|
||||||
|
margin-right: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
979
lottery-app/src/views/admin/UserList.vue
Normal 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>
|
||||||
11
lottery-app/src/views/admin/UserRole.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
1067
lottery-app/src/views/admin/VipCodeManagement.vue
Normal file
536
lottery-app/src/views/admin/layout/AdminLayout.vue
Normal 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
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
echo 正在启动双色球智能推测系统...
|
||||||
|
echo.
|
||||||
|
npm run dev
|
||||||
|
pause
|
||||||
20
lottery-app/vite.config.js
Normal 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
@@ -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
@@ -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:移动端风格,聊天框窗口铺满移动设备全屏。
|
||||||
|
pc:PC 端风格,聊天框窗口位于页面右下角。
|
||||||
|
未设置此参数时,系统会自动识别设备,设置相应的布局风格。
|
||||||
|
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>
|
||||||
|
|
||||||
|
相关文档
|
||||||