diff --git a/Controller.txt b/Controller.txt deleted file mode 100644 index 52b4119..0000000 --- a/Controller.txt +++ /dev/null @@ -1,402 +0,0 @@ -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> 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 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> getRecentDraws( - @Parameter(description = "获取条数,默认15条", required = false) - @RequestParam(required = false, defaultValue = "15") Integer limit) { - - try { - log.info("接收到获取近期开奖信息请求:条数={}", limit); - - // 调用服务获取近期开奖信息 - List 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> 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 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 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 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 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> 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 redBallList = parseRedBalls(redBalls, 6, "红球"); - - // 调用分析服务 - List 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> 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 firstThreeRedBallList = parseRedBalls(firstThreeRedBalls, 3, "前3个红球"); - List lastSixRedBallList = parseRedBalls(lastSixRedBalls, 6, "后6个红球"); - - // 调用分析服务 - List 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 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 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> 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 predictedRedBallList = parseRedBalls(predictedRedBalls, 6, "推测红球"); - List predictedBlueBallList = parseBlueBalls(predictedBlueBalls, 2, "推测蓝球"); - List lastRedBallList = parseRedBalls(lastRedBalls, 6, "上期红球"); - - // 调用分析服务 - List 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 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 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 + "号码格式错误,请使用逗号分隔的数字"); - } - } - -} \ No newline at end of file diff --git a/Store登录状态管理增强说明.md b/Store登录状态管理增强说明.md new file mode 100644 index 0000000..29c3f2c --- /dev/null +++ b/Store登录状态管理增强说明.md @@ -0,0 +1,351 @@ +# Store 登录状态管理增强说明 + +## 📋 问题背景 + +在多设备登录踢出场景中,仅依赖 API 拦截器**被动响应**是不够的,Store 层面也需要: +1. **主动检查**登录状态是否有效 +2. **标记状态**区分正常登出和被踢出 +3. **提供方法**供其他组件调用检查 + +--- + +## ✅ 已实现的增强功能 + +### 1. **新增状态字段** + +```javascript +export const userStore = reactive({ + user: null, + isLoggedIn: false, + isKickedOut: false, // 🆕 标记是否被其他设备踢出 + lastCheckTime: null, // 🆕 最后一次检查登录状态的时间 + // ... +}) +``` + +### 2. **增强 `logout()` 方法** + +支持标记被踢出状态: + +```javascript +// 正常登出 +userStore.logout() + +// 被踢出时登出 +userStore.logout(true) // 会标记 isKickedOut = true +``` + +**实现代码:** +```javascript +logout(isKickedOut = false) { + // 如果是被踢出,标记状态 + if (isKickedOut) { + this.isKickedOut = true + console.log('[安全] 账号在其他设备登录,当前会话已被踢出') + } + + this.user = null + this.isLoggedIn = false + this.lastCheckTime = null + + // 清除本地存储... +} +``` + +### 3. **增强 `adminLogout()` 方法** + +同样支持标记被踢出: + +```javascript +adminLogout(isKickedOut = false) { + if (isKickedOut) { + this.isKickedOut = true + console.log('[安全] 管理员账号在其他设备登录,当前会话已被踢出') + } + + // 清除状态和存储... +} +``` + +### 4. **新增 `checkLoginStatus()` 方法** 🔥 + +主动检查登录状态是否仍然有效: + +```javascript +async checkLoginStatus() { + // 1️⃣ 如果没有登录,直接返回 + if (!this.isLoggedIn) { + return { valid: false, reason: 'not_logged_in' } + } + + // 2️⃣ 避免频繁检查(10秒内只检查一次) + const now = Date.now() + if (this.lastCheckTime && (now - this.lastCheckTime) < 10000) { + return { valid: true, reason: 'recently_checked' } + } + + try { + // 3️⃣ 调用后端接口验证登录状态 + const response = await lotteryApi.getLoginUser() + + if (response.success === true) { + // ✅ 登录状态有效 + this.lastCheckTime = now + this.isKickedOut = false + return { valid: true, reason: 'verified' } + } else { + // ❌ 登录状态无效 + const message = response.message || '' + + // 检查是否是被踢出 + if (message.includes('其他设备登录') || message.includes('当前会话已失效')) { + this.logout(true) // 标记为被踢出 + return { valid: false, reason: 'kicked_out', message } + } else { + this.logout(false) + return { valid: false, reason: 'session_expired', message } + } + } + } catch (error) { + // ⚠️ 网络错误等情况,不清除登录状态,让用户继续使用 + console.error('[Store] 检查登录状态失败:', error) + return { valid: true, reason: 'check_failed', error } + } +} +``` + +### 5. **新增 `resetKickedOutStatus()` 方法** + +用于重新登录后重置被踢出状态: + +```javascript +resetKickedOutStatus() { + this.isKickedOut = false +} +``` + +### 6. **增强 `login()` 方法** + +登录时自动重置被踢出状态: + +```javascript +login(userInfo) { + // ... 设置用户信息 + + this.isLoggedIn = true + this.isKickedOut = false // 🆕 重置被踢出状态 + this.lastCheckTime = Date.now() // 🆕 记录登录时间 + + // 保存到本地存储... +} +``` + +--- + +## 🎯 使用场景 + +### 场景 1:组件中主动检查登录状态 + +```javascript +// 在关键操作前检查登录状态 +async function performCriticalAction() { + const status = await userStore.checkLoginStatus() + + if (!status.valid) { + if (status.reason === 'kicked_out') { + ElMessage.warning('您的账号已在其他设备登录') + // 跳转到登录页 + router.push('/login') + } else { + ElMessage.error('登录已过期,请重新登录') + router.push('/login') + } + return + } + + // 继续执行操作 + // ... +} +``` + +### 场景 2:路由守卫中使用 + +```javascript +// router/index.js +router.beforeEach(async (to, from, next) => { + if (to.meta.requiresAuth) { + const status = await userStore.checkLoginStatus() + + if (!status.valid) { + next('/login') + return + } + } + next() +}) +``` + +### 场景 3:定期心跳检测(可选) + +```javascript +// 在 App.vue 中设置定期检查 +import { onMounted, onUnmounted } from 'vue' +import { userStore } from '@/store/user' + +let heartbeatTimer = null + +onMounted(() => { + // 每 5 分钟检查一次登录状态 + heartbeatTimer = setInterval(async () => { + if (userStore.isLoggedIn) { + await userStore.checkLoginStatus() + } + }, 5 * 60 * 1000) // 5分钟 +}) + +onUnmounted(() => { + if (heartbeatTimer) { + clearInterval(heartbeatTimer) + } +}) +``` + +### 场景 4:判断被踢出状态 + +```javascript +// 在登录页面显示提示 +if (userStore.isKickedOut) { + ElMessage.warning({ + message: '您的账号已在其他设备登录,请重新登录', + duration: 5000 + }) + userStore.resetKickedOutStatus() // 重置状态 +} +``` + +--- + +## 🔄 完整流程 + +### 被动检测(API 拦截器) + +``` +用户操作 → API 请求 → 后端检测 token 不一致 → 返回错误 + ↓ +API 拦截器捕获 → 调用 userStore.logout(true) + ↓ +标记 isKickedOut = true → 显示提示 → 跳转登录页 +``` + +### 主动检测(Store 方法) + +``` +组件调用 checkLoginStatus() + ↓ +10秒内检查过? + ├─ 是 → 返回缓存结果 + └─ 否 → 调用后端 API + ↓ + token 有效? + ├─ 是 → 更新检查时间,返回 valid: true + └─ 否 → 检查错误类型 + ├─ 被踢出 → logout(true), 返回 kicked_out + └─ 其他 → logout(false), 返回 session_expired +``` + +--- + +## 🛡️ 防护机制 + +### 1. **防止频繁检查** +- 10 秒内只检查一次 +- 避免给后端造成压力 + +### 2. **容错处理** +- 网络错误时不清除登录状态 +- 让用户继续使用,避免误判 + +### 3. **状态标记** +- 明确区分正常登出和被踢出 +- 便于显示不同的提示信息 + +### 4. **自动恢复** +- 登录时自动重置被踢出状态 +- 提供手动重置方法 + +--- + +## 📊 返回值说明 + +`checkLoginStatus()` 方法返回对象: + +```typescript +interface CheckResult { + valid: boolean // 登录状态是否有效 + reason: string // 原因 + message?: string // 错误消息(可选) + error?: Error // 错误对象(可选) +} +``` + +### Reason 枚举值: + +| Reason | 说明 | valid | +|--------|------|-------| +| `not_logged_in` | 未登录 | false | +| `recently_checked` | 10秒内已检查过 | true | +| `verified` | 后端验证通过 | true | +| `kicked_out` | 被其他设备踢出 | false | +| `session_expired` | 会话过期 | false | +| `check_failed` | 检查失败(网络错误等) | true | + +--- + +## ✅ 配合 API 拦截器 + +API 拦截器已更新,会在检测到被踢出时调用: + +```javascript +// src/api/index.js +userStore.logout(true) // 前台用户 +userStore.adminLogout(true) // 后台管理员 +``` + +这样确保了**被动响应**和**主动检查**两种机制的完美配合。 + +--- + +## 🎨 最佳实践 + +### ✅ 推荐 + +1. **关键操作前检查**:支付、提交表单等操作前主动检查 +2. **路由守卫检查**:切换到需要登录的页面时检查 +3. **显示友好提示**:根据 `isKickedOut` 显示不同的提示信息 +4. **定期心跳检测**:长时间停留在页面时定期检查(可选) + +### ❌ 避免 + +1. **过度频繁检查**:利用 10 秒缓存机制,避免每次操作都检查 +2. **忽略返回值**:检查失败时要及时处理,引导用户登录 +3. **混淆状态**:不区分正常登出和被踢出,提示信息不明确 + +--- + +## 🔧 相关文件 + +- ✅ `src/store/user.js` - Store 状态管理(已增强) +- ✅ `src/api/index.js` - API 拦截器(已更新) +- 📄 `前端多设备登录处理方案.md` - 总体方案说明 + +--- + +## 🎉 总结 + +通过在 Store 层面添加: +- ✅ 被踢出状态标记 `isKickedOut` +- ✅ 主动检查方法 `checkLoginStatus()` +- ✅ 增强的登出方法 `logout(isKickedOut)` +- ✅ 防频繁检查机制(10秒缓存) + +实现了**被动响应 + 主动检查**的双重保障机制,确保多设备登录场景下的安全性和用户体验! diff --git a/lottery-app.zip b/lottery-app.zip deleted file mode 100644 index 6ee33b8..0000000 Binary files a/lottery-app.zip and /dev/null differ diff --git a/lottery-app/DEPLOY.md b/lottery-app/DEPLOY.md new file mode 100644 index 0000000..87e462f --- /dev/null +++ b/lottery-app/DEPLOY.md @@ -0,0 +1,195 @@ +# 部署说明文档 + +## 🚀 打包部署流程 + +### 1. 打包前准备 + +确保已安装依赖: +```bash +npm install +``` + +### 2. 执行打包 + +```bash +npm run build +``` + +打包完成后,会在项目根目录生成 `dist` 文件夹。 + +### 3. 缓存控制策略 + +本项目已配置完善的缓存控制策略,避免浏览器缓存导致更新不生效: + +#### ✅ 已配置的缓存方案 + +1. **文件名 Hash 化**(`vite.config.js` 已配置) + - 所有 JS、CSS、图片等资源文件名都会带上 hash 值 + - 例如:`index.a1b2c3d4.js`、`style.e5f6g7h8.css` + - 每次构建后,修改过的文件 hash 会变化,自动避免缓存 + +2. **HTML 文件不缓存** + - `index.html` 设置为不缓存,每次都会获取最新版本 + - 通过服务器配置实现(见下方) + +#### 📝 服务器配置 + +##### Apache 服务器(使用 .htaccess) + +项目已包含 `public/.htaccess` 文件,打包后会自动复制到 `dist` 目录。 + +##### Nginx 服务器 + +参考项目根目录的 `nginx.conf.example` 文件,配置说明: + +```nginx +# HTML 文件不缓存 +location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; +} + +# 静态资源长期缓存 +location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; +} +``` + +### 4. 部署步骤 + +#### 方法一:手动部署 + +1. 将 `dist` 目录下的所有文件上传到服务器 +2. 配置服务器(Apache 或 Nginx) +3. 重启服务器 + +#### 方法二:使用 FTP/SFTP + +```bash +# 上传 dist 目录到服务器 +scp -r dist/* user@your-server:/var/www/html/ +``` + +#### 方法三:使用 Docker(可选) + +创建 `Dockerfile`: +```dockerfile +FROM nginx:alpine +COPY dist /usr/share/nginx/html +COPY nginx.conf.example /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] +``` + +### 5. 更新部署注意事项 + +#### ⚠️ 每次更新部署时: + +1. **清理旧文件** + ```bash + # 删除服务器上的旧文件 + rm -rf /var/www/html/* + ``` + +2. **上传新文件** + ```bash + # 上传新的 dist 文件 + scp -r dist/* user@your-server:/var/www/html/ + ``` + +3. **清理服务器缓存**(如果使用了缓存服务器) + ```bash + # Nginx + nginx -s reload + + # Apache + systemctl reload apache2 + ``` + +4. **通知用户清理浏览器缓存**(可选) + - 可以在系统中添加版本提示 + - 或者使用 Service Worker 强制更新 + +### 6. 验证部署 + +部署完成后,验证步骤: + +1. **清除浏览器缓存** + - Chrome: `Ctrl + Shift + Delete` 或 `Cmd + Shift + Delete` + - 或使用隐身模式访问 + +2. **检查文件版本** + - 打开浏览器开发者工具(F12) + - 查看 Network 标签 + - 确认 JS/CSS 文件名带有新的 hash 值 + +3. **检查 HTML 缓存** + - 查看 `index.html` 的响应头 + - 确认 `Cache-Control: no-cache` + +### 7. 常见问题 + +#### Q1: 用户反馈看到的还是旧版本? + +**解决方案:** +1. 确认服务器配置正确(.htaccess 或 nginx 配置) +2. 清理 CDN 缓存(如果使用了 CDN) +3. 通知用户强制刷新(Ctrl + F5 或 Cmd + Shift + R) + +#### Q2: 静态资源 404 错误? + +**解决方案:** +1. 检查资源路径配置 +2. 确认 `vite.config.js` 中的 `base` 配置正确 +3. 检查服务器的静态文件路径 + +#### Q3: SPA 路由刷新 404? + +**解决方案:** +- Apache: 确保 `.htaccess` 中的 rewrite 规则生效 +- Nginx: 确保配置了 `try_files $uri $uri/ /index.html;` + +### 8. 自动化部署(可选) + +#### 使用 GitHub Actions + +创建 `.github/workflows/deploy.yml`: + +```yaml +name: Deploy + +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: Deploy to server + uses: easingthemes/ssh-deploy@main + env: + SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }} + REMOTE_HOST: ${{ secrets.REMOTE_HOST }} + REMOTE_USER: ${{ secrets.REMOTE_USER }} + TARGET: /var/www/html/ +``` + +### 9. 性能优化建议 + +1. **启用 Gzip/Brotli 压缩** +2. **使用 CDN 加速静态资源** +3. **配置 HTTP/2** +4. **启用 HTTPS** + +## 📞 技术支持 + +如有部署问题,请联系技术团队。 + + diff --git a/lottery-app/git-commands.txt b/lottery-app/git-commands.txt new file mode 100644 index 0000000..a16ed6e --- /dev/null +++ b/lottery-app/git-commands.txt @@ -0,0 +1,19 @@ +# Git 初始化并上传到远程仓库的命令 + +# 1. 初始化 Git 仓库 +git init + +# 2. 添加所有文件到暂存区 +git add . + +# 3. 提交代码 +git commit -m "初始提交:彩票推测系统前端代码" + +# 4. 添加远程仓库 +git remote add origin http://49.234.3.145:3018/lihanqi/cpzs-frontend.git + +# 5. 将当前分支重命名为 main +git branch -M main + +# 6. 推送到远程仓库并设置上游分支 +git push -u origin main diff --git a/lottery-app/nginx.conf.example b/lottery-app/nginx.conf.example new file mode 100644 index 0000000..8f21650 --- /dev/null +++ b/lottery-app/nginx.conf.example @@ -0,0 +1,41 @@ +# Nginx 服务器配置示例 +# 使用方法:将此配置添加到你的 Nginx 配置文件中 + +server { + listen 80; + server_name your-domain.com; # 修改为你的域名 + root /path/to/lottery-app/dist; # 修改为你的项目路径 + index index.html; + + # Gzip 压缩配置 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss image/svg+xml; + + # HTML 文件不缓存 + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # 静态资源长期缓存(带hash的JS、CSS、图片等) + location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # 代理 API 请求(如果需要) + location /api/ { + proxy_pass http://localhost:8123; # 修改为你的后端地址 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + + diff --git a/lottery-app/package-lock.json b/lottery-app/package-lock.json index 0cb9e30..f6052fa 100644 --- a/lottery-app/package-lock.json +++ b/lottery-app/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.1", "axios": "^1.10.0", - "element-plus": "^2.10.4", + "echarts": "^6.0.0", + "element-plus": "^2.11.8", "qrcode": "^1.5.4", "vue": "^3.5.13", "vue-router": "^4.5.1", @@ -18,6 +19,7 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", + "terser": "^5.44.0", "vite": "^6.2.4", "vite-plugin-vue-devtools": "^7.7.2" } @@ -501,9 +503,9 @@ } }, "node_modules/@element-plus/icons-vue": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz", - "integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==", + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", "license": "MIT", "peerDependencies": { "vue": "^3.2.0" @@ -994,6 +996,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1704,6 +1717,19 @@ } } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1794,6 +1820,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", @@ -1894,6 +1927,13 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1939,9 +1979,9 @@ "license": "MIT" }, "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "version": "1.11.19", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "license": "MIT" }, "node_modules/debug": { @@ -2043,6 +2083,16 @@ "node": ">= 0.4" } }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.167", "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz", @@ -2051,24 +2101,23 @@ "license": "ISC" }, "node_modules/element-plus": { - "version": "2.10.4", - "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.10.4.tgz", - "integrity": "sha512-UD4elWHrCnp1xlPhbXmVcaKFLCRaRAY6WWRwemGfGW3ceIjXm9fSYc9RNH3AiOEA6Ds1p9ZvhCs76CR9J8Vd+A==", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.11.8.tgz", + "integrity": "sha512-2wzSj2uubFU1f0t/gHkkE1d09mUgV18fSZX5excw3Ar6hyWcxph4E57U8dgYLDt7HwkKYv1BiqPyBdy0WqWlOA==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^3.4.1", - "@element-plus/icons-vue": "^2.3.1", + "@element-plus/icons-vue": "^2.3.2", "@floating-ui/dom": "^1.0.1", "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", - "@types/lodash": "^4.14.182", - "@types/lodash-es": "^4.17.6", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", "@vueuse/core": "^9.1.0", "async-validator": "^4.2.5", - "dayjs": "^1.11.13", - "escape-html": "^1.0.3", + "dayjs": "^1.11.18", "lodash": "^4.17.21", "lodash-es": "^4.17.21", - "lodash-unified": "^1.0.2", + "lodash-unified": "^1.0.3", "memoize-one": "^6.0.0", "normalize-wheel-es": "^1.2.0" }, @@ -2200,12 +2249,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", @@ -3201,6 +3244,16 @@ "node": ">=18" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3210,6 +3263,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/speakingurl": { "version": "14.0.1", "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz", @@ -3272,6 +3336,25 @@ "node": ">=16" } }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -3299,6 +3382,12 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/unicorn-magic": { "version": "0.3.0", "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz", @@ -3657,6 +3746,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } } } } diff --git a/lottery-app/package.json b/lottery-app/package.json index 5e15064..d8e6ed8 100644 --- a/lottery-app/package.json +++ b/lottery-app/package.json @@ -6,12 +6,14 @@ "scripts": { "dev": "vite", "build": "vite build", + "build:clean": "rm -rf dist && vite build", "preview": "vite preview" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", "axios": "^1.10.0", - "element-plus": "^2.10.4", + "echarts": "^6.0.0", + "element-plus": "^2.11.8", "qrcode": "^1.5.4", "vue": "^3.5.13", "vue-router": "^4.5.1", @@ -19,6 +21,7 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", + "terser": "^5.44.0", "vite": "^6.2.4", "vite-plugin-vue-devtools": "^7.7.2" } diff --git a/lottery-app/public/.htaccess b/lottery-app/public/.htaccess new file mode 100644 index 0000000..64021b1 --- /dev/null +++ b/lottery-app/public/.htaccess @@ -0,0 +1,32 @@ +# 缓存控制配置(Apache服务器) + +# 禁用 HTML 文件的缓存 + + FileETag None + Header unset ETag + Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate" + Header set Pragma "no-cache" + Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT" + + +# 静态资源长期缓存(带hash的文件) + + Header set Cache-Control "max-age=31536000, public, immutable" + + +# 启用 Gzip 压缩 + + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json application/xml + + +# SPA 路由支持 + + RewriteEngine On + RewriteBase / + RewriteRule ^index\.html$ - [L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule . /index.html [L] + + + diff --git a/lottery-app/public/assets/admin/logo.svg b/lottery-app/public/assets/admin/logo.svg new file mode 100644 index 0000000..4d00f84 --- /dev/null +++ b/lottery-app/public/assets/admin/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lottery-app/public/assets/banner/banner.png b/lottery-app/public/assets/banner/banner.png new file mode 100644 index 0000000..bf55eac Binary files /dev/null and b/lottery-app/public/assets/banner/banner.png differ diff --git a/lottery-app/public/assets/banner/banner.svg b/lottery-app/public/assets/banner/banner.svg new file mode 100644 index 0000000..3c26d43 --- /dev/null +++ b/lottery-app/public/assets/banner/banner.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/lottery-app/public/assets/banner/banner1.png b/lottery-app/public/assets/banner/banner1.png new file mode 100644 index 0000000..53f8760 Binary files /dev/null and b/lottery-app/public/assets/banner/banner1.png differ diff --git a/lottery-app/public/assets/bottom/faxian-0.svg b/lottery-app/public/assets/bottom/faxian-0.svg new file mode 100644 index 0000000..6f9be96 --- /dev/null +++ b/lottery-app/public/assets/bottom/faxian-0.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/bottom/faxian-1.svg b/lottery-app/public/assets/bottom/faxian-1.svg new file mode 100644 index 0000000..6b5c270 --- /dev/null +++ b/lottery-app/public/assets/bottom/faxian-1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/bottom/home-0.svg b/lottery-app/public/assets/bottom/home-0.svg new file mode 100644 index 0000000..1f778e4 --- /dev/null +++ b/lottery-app/public/assets/bottom/home-0.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/bottom/home-1.svg b/lottery-app/public/assets/bottom/home-1.svg new file mode 100644 index 0000000..f5f8908 --- /dev/null +++ b/lottery-app/public/assets/bottom/home-1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/bottom/kaijiang-0.svg b/lottery-app/public/assets/bottom/kaijiang-0.svg new file mode 100644 index 0000000..5b9887c --- /dev/null +++ b/lottery-app/public/assets/bottom/kaijiang-0.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/bottom/kaijiang-1.svg b/lottery-app/public/assets/bottom/kaijiang-1.svg new file mode 100644 index 0000000..2cb38c6 --- /dev/null +++ b/lottery-app/public/assets/bottom/kaijiang-1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/bottom/tuice-0.svg b/lottery-app/public/assets/bottom/tuice-0.svg new file mode 100644 index 0000000..389fb78 --- /dev/null +++ b/lottery-app/public/assets/bottom/tuice-0.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lottery-app/public/assets/bottom/tuice-1.svg b/lottery-app/public/assets/bottom/tuice-1.svg new file mode 100644 index 0000000..2e76033 --- /dev/null +++ b/lottery-app/public/assets/bottom/tuice-1.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lottery-app/public/assets/bottom/wode-0.svg b/lottery-app/public/assets/bottom/wode-0.svg new file mode 100644 index 0000000..018f86f --- /dev/null +++ b/lottery-app/public/assets/bottom/wode-0.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/lottery-app/public/assets/bottom/wode-1.svg b/lottery-app/public/assets/bottom/wode-1.svg new file mode 100644 index 0000000..38a8e9f --- /dev/null +++ b/lottery-app/public/assets/bottom/wode-1.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/lottery-app/public/assets/fenxi/3D-0.svg b/lottery-app/public/assets/fenxi/3D-0.svg new file mode 100644 index 0000000..6b5dccf --- /dev/null +++ b/lottery-app/public/assets/fenxi/3D-0.svg @@ -0,0 +1,3 @@ + + + diff --git a/lottery-app/public/assets/fenxi/3D-1.svg b/lottery-app/public/assets/fenxi/3D-1.svg new file mode 100644 index 0000000..b7842ce --- /dev/null +++ b/lottery-app/public/assets/fenxi/3D-1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/fenxi/7lecai-0.svg b/lottery-app/public/assets/fenxi/7lecai-0.svg new file mode 100644 index 0000000..627b903 --- /dev/null +++ b/lottery-app/public/assets/fenxi/7lecai-0.svg @@ -0,0 +1,3 @@ + + + diff --git a/lottery-app/public/assets/fenxi/7lecai-1.svg b/lottery-app/public/assets/fenxi/7lecai-1.svg new file mode 100644 index 0000000..5357370 --- /dev/null +++ b/lottery-app/public/assets/fenxi/7lecai-1.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lottery-app/public/assets/fenxi/7xingcai-0.svg b/lottery-app/public/assets/fenxi/7xingcai-0.svg new file mode 100644 index 0000000..8e9bc63 --- /dev/null +++ b/lottery-app/public/assets/fenxi/7xingcai-0.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/lottery-app/public/assets/fenxi/7xingcai-1.svg b/lottery-app/public/assets/fenxi/7xingcai-1.svg new file mode 100644 index 0000000..1d9b737 --- /dev/null +++ b/lottery-app/public/assets/fenxi/7xingcai-1.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lottery-app/public/assets/fenxi/daletou-0.svg b/lottery-app/public/assets/fenxi/daletou-0.svg new file mode 100644 index 0000000..902e007 --- /dev/null +++ b/lottery-app/public/assets/fenxi/daletou-0.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lottery-app/public/assets/fenxi/daletou-1.svg b/lottery-app/public/assets/fenxi/daletou-1.svg new file mode 100644 index 0000000..620a54e --- /dev/null +++ b/lottery-app/public/assets/fenxi/daletou-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/lottery-app/public/assets/fenxi/fenxi-1.svg b/lottery-app/public/assets/fenxi/fenxi-1.svg new file mode 100644 index 0000000..d9b50cf --- /dev/null +++ b/lottery-app/public/assets/fenxi/fenxi-1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/fenxi/fenxi-2.svg b/lottery-app/public/assets/fenxi/fenxi-2.svg new file mode 100644 index 0000000..7da5070 --- /dev/null +++ b/lottery-app/public/assets/fenxi/fenxi-2.svg @@ -0,0 +1,3 @@ + + + diff --git a/lottery-app/public/assets/fenxi/fenxi-3.svg b/lottery-app/public/assets/fenxi/fenxi-3.svg new file mode 100644 index 0000000..e21b955 --- /dev/null +++ b/lottery-app/public/assets/fenxi/fenxi-3.svg @@ -0,0 +1,3 @@ + + + diff --git a/lottery-app/public/assets/fenxi/fenxi-4.svg b/lottery-app/public/assets/fenxi/fenxi-4.svg new file mode 100644 index 0000000..9f2184c --- /dev/null +++ b/lottery-app/public/assets/fenxi/fenxi-4.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/fenxi/kuaile-0.svg b/lottery-app/public/assets/fenxi/kuaile-0.svg new file mode 100644 index 0000000..d970617 --- /dev/null +++ b/lottery-app/public/assets/fenxi/kuaile-0.svg @@ -0,0 +1,3 @@ + + + diff --git a/lottery-app/public/assets/fenxi/kuaile8-1.svg b/lottery-app/public/assets/fenxi/kuaile8-1.svg new file mode 100644 index 0000000..e180e78 --- /dev/null +++ b/lottery-app/public/assets/fenxi/kuaile8-1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/fenxi/pailie-1.svg b/lottery-app/public/assets/fenxi/pailie-1.svg new file mode 100644 index 0000000..b8e4b30 --- /dev/null +++ b/lottery-app/public/assets/fenxi/pailie-1.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lottery-app/public/assets/fenxi/pailie3-0.svg b/lottery-app/public/assets/fenxi/pailie3-0.svg new file mode 100644 index 0000000..1bac077 --- /dev/null +++ b/lottery-app/public/assets/fenxi/pailie3-0.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/lottery-app/public/assets/fenxi/pailie3-1.svg b/lottery-app/public/assets/fenxi/pailie3-1.svg new file mode 100644 index 0000000..7d6f821 --- /dev/null +++ b/lottery-app/public/assets/fenxi/pailie3-1.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/lottery-app/public/assets/fenxi/pailie5.svg b/lottery-app/public/assets/fenxi/pailie5.svg new file mode 100644 index 0000000..58ebe4d --- /dev/null +++ b/lottery-app/public/assets/fenxi/pailie5.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lottery-app/public/assets/fenxi/ssq-0.svg b/lottery-app/public/assets/fenxi/ssq-0.svg new file mode 100644 index 0000000..c624296 --- /dev/null +++ b/lottery-app/public/assets/fenxi/ssq-0.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lottery-app/public/assets/fenxi/ssq-1.svg b/lottery-app/public/assets/fenxi/ssq-1.svg new file mode 100644 index 0000000..c92603c --- /dev/null +++ b/lottery-app/public/assets/fenxi/ssq-1.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lottery-app/public/assets/home/about.svg b/lottery-app/public/assets/home/about.svg new file mode 100644 index 0000000..045dcd2 --- /dev/null +++ b/lottery-app/public/assets/home/about.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/home/dingdan.svg b/lottery-app/public/assets/home/dingdan.svg new file mode 100644 index 0000000..c3b4f21 --- /dev/null +++ b/lottery-app/public/assets/home/dingdan.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lottery-app/public/assets/home/duihuan.svg b/lottery-app/public/assets/home/duihuan.svg new file mode 100644 index 0000000..2dfe500 --- /dev/null +++ b/lottery-app/public/assets/home/duihuan.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/home/erweima.png b/lottery-app/public/assets/home/erweima.png new file mode 100644 index 0000000..49722f7 Binary files /dev/null and b/lottery-app/public/assets/home/erweima.png differ diff --git a/lottery-app/public/assets/home/help.svg b/lottery-app/public/assets/home/help.svg new file mode 100644 index 0000000..f88fa08 --- /dev/null +++ b/lottery-app/public/assets/home/help.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/home/jiangjing.svg b/lottery-app/public/assets/home/jiangjing.svg new file mode 100644 index 0000000..b8c0872 --- /dev/null +++ b/lottery-app/public/assets/home/jiangjing.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/home/kefu.svg b/lottery-app/public/assets/home/kefu.svg new file mode 100644 index 0000000..c54d0f8 --- /dev/null +++ b/lottery-app/public/assets/home/kefu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/home/mingzhong.svg b/lottery-app/public/assets/home/mingzhong.svg new file mode 100644 index 0000000..7d50e3b --- /dev/null +++ b/lottery-app/public/assets/home/mingzhong.svg @@ -0,0 +1,3 @@ + + + diff --git a/lottery-app/public/assets/home/qianbao.svg b/lottery-app/public/assets/home/qianbao.svg new file mode 100644 index 0000000..959b23d --- /dev/null +++ b/lottery-app/public/assets/home/qianbao.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lottery-app/public/assets/home/tongji.svg b/lottery-app/public/assets/home/tongji.svg new file mode 100644 index 0000000..c8c6660 --- /dev/null +++ b/lottery-app/public/assets/home/tongji.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lottery-app/public/assets/home/tuice.svg b/lottery-app/public/assets/home/tuice.svg new file mode 100644 index 0000000..ca4a1e9 --- /dev/null +++ b/lottery-app/public/assets/home/tuice.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lottery-app/public/assets/type/3D.svg b/lottery-app/public/assets/type/3D.svg new file mode 100644 index 0000000..e028302 --- /dev/null +++ b/lottery-app/public/assets/type/3D.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/type/7lecai.svg b/lottery-app/public/assets/type/7lecai.svg new file mode 100644 index 0000000..dc5c5e1 --- /dev/null +++ b/lottery-app/public/assets/type/7lecai.svg @@ -0,0 +1,3 @@ + + + diff --git a/lottery-app/public/assets/type/7xingcai.svg b/lottery-app/public/assets/type/7xingcai.svg new file mode 100644 index 0000000..59fccbd --- /dev/null +++ b/lottery-app/public/assets/type/7xingcai.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lottery-app/public/assets/type/daletou.svg b/lottery-app/public/assets/type/daletou.svg new file mode 100644 index 0000000..55424f9 --- /dev/null +++ b/lottery-app/public/assets/type/daletou.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lottery-app/public/assets/type/kl8.svg b/lottery-app/public/assets/type/kl8.svg new file mode 100644 index 0000000..77fc42b --- /dev/null +++ b/lottery-app/public/assets/type/kl8.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lottery-app/public/assets/type/pailie3.svg b/lottery-app/public/assets/type/pailie3.svg new file mode 100644 index 0000000..703a6a7 --- /dev/null +++ b/lottery-app/public/assets/type/pailie3.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/lottery-app/public/assets/type/pailie5.svg b/lottery-app/public/assets/type/pailie5.svg new file mode 100644 index 0000000..67a278e --- /dev/null +++ b/lottery-app/public/assets/type/pailie5.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lottery-app/public/assets/type/ssq.svg b/lottery-app/public/assets/type/ssq.svg new file mode 100644 index 0000000..b7709dc --- /dev/null +++ b/lottery-app/public/assets/type/ssq.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lottery-app/public/favicon.ico b/lottery-app/public/favicon.ico index df36fcf..c21a0e3 100644 Binary files a/lottery-app/public/favicon.ico and b/lottery-app/public/favicon.ico differ diff --git a/lottery-app/src/App.vue b/lottery-app/src/App.vue index b06620b..473b1b1 100644 --- a/lottery-app/src/App.vue +++ b/lottery-app/src/App.vue @@ -4,6 +4,8 @@ import { useRoute } from 'vue-router' import { userStore } from './store/user' import HelloWorld from './components/HelloWorld.vue' import TheWelcome from './components/TheWelcome.vue' +import BottomNavigation from './components/BottomNavigation.vue' +import CozeChat from './components/CozeChat.vue' // 获取当前路由 const route = useRoute() @@ -13,6 +15,12 @@ const isAdminRoute = computed(() => { return route.path.startsWith('/admin') }) +// 处理发现按钮点击事件 +const handleDiscoveryClick = () => { + // 触发全局事件,让CozeChat组件处理 + window.dispatchEvent(new CustomEvent('showAIAssistant')) +} + // 应用启动时获取用户信息 onMounted(async () => { // 如果用户已登录但没有完整的用户信息,则从后端获取 @@ -22,8 +30,9 @@ onMounted(async () => { await userStore.fetchLoginUser() console.log('用户信息获取成功:', userStore.user) } catch (error) { - console.error('获取用户信息失败:', error) - // 如果获取失败,可能是token过期,清除登录状态 + console.warn('获取用户信息失败,可能是后端服务不可用:', error) + // 不强制清除登录状态,让用户可以继续浏览页面 + // 如果是401错误才清除登录状态 if (error.response?.status === 401) { userStore.logout() } @@ -47,33 +56,24 @@ onMounted(async () => {
+ +
+
+
+ 精彩猪手 +
+
+
+
-
- -
- 首页 -
-
首页
-
- - -
- 开奖 -
-
开奖
-
+ - -
- 我的 -
-
我的
-
-
+ +
@@ -97,11 +97,11 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - background: #f0f2f5 !important; + background: #f5f5f5 !important; line-height: 1.5; min-height: 100vh; margin: 0 !important; - padding: 30px !important; + padding: 0 !important; width: 100%; display: flex; flex-direction: column; @@ -111,136 +111,109 @@ body { /* 应用容器 - 包含主内容和底部导航 */ .app-container { - min-height: calc(100vh - 60px); + min-height: 100vh; display: flex; flex-direction: column; position: relative; - max-width: 900px; + max-width: 850px; width: 100%; margin: 0 auto; background: white; box-shadow: 0 0 20px rgba(0, 0, 0, 0.08); - border-radius: 12px; + border-radius: 0; overflow: hidden; } +/* 全局顶部导航栏 */ +.global-header { + height: 60px; + background: linear-gradient(135deg, #ff7b7b 0%, #ff6363 50%, #f85555 100%); + position: sticky; + top: 0; + z-index: 999; + box-shadow: 0 2px 12px rgba(248, 85, 85, 0.3); + position: relative; + overflow: hidden; +} + +/* 装饰性波浪背景 */ +.global-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 60' preserveAspectRatio='none'%3E%3Cpath d='M0,30 Q100,10 200,30 T400,30 T600,30 T800,30 L800,60 L0,60 Z' fill='rgba(255,255,255,0.08)'/%3E%3Cpath d='M0,40 Q150,20 300,40 T600,40 T800,35 L800,60 L0,60 Z' fill='rgba(255,255,255,0.05)'/%3E%3Ccircle cx='50' cy='15' r='3' fill='rgba(255,255,255,0.15)'/%3E%3Ccircle cx='150' cy='45' r='2' fill='rgba(255,255,255,0.12)'/%3E%3Ccircle cx='300' cy='12' r='2.5' fill='rgba(255,255,255,0.1)'/%3E%3Ccircle cx='500' cy='48' r='2' fill='rgba(255,255,255,0.15)'/%3E%3Ccircle cx='650' cy='18' r='3' fill='rgba(255,255,255,0.1)'/%3E%3Ccircle cx='750' cy='40' r='2' fill='rgba(255,255,255,0.12)'/%3E%3C/svg%3E"); + background-size: cover; + background-position: center; + opacity: 1; + z-index: 0; +} + +.header-content { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 0 20px; + max-width: 1200px; + margin: 0 auto; + position: relative; + z-index: 2; +} + +.app-logo { + display: flex; + align-items: center; + justify-content: center; + transform: translateY(0); + transition: all 0.3s ease; +} + +.app-logo:hover { + transform: translateY(-1px) scale(1.02); +} + +.logo-text { + font-size: 22px; + font-weight: 700; + color: white; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), + 0 1px 2px rgba(0, 0, 0, 0.5); + letter-spacing: 2px; + position: relative; + display: inline-block; +} + +.logo-text::after { + content: ''; + position: absolute; + bottom: -2px; + left: 50%; + width: 0; + height: 2px; + background: rgba(255, 255, 255, 0.8); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +.app-logo:hover .logo-text::after { + width: 100%; +} + + + /* 主要内容区域 */ .main-content { flex: 1; background: #f0f2f5; position: relative; + padding-bottom: 65px; + min-height: calc(100vh - 130px); } -/* 底部导航栏 */ -.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 { @@ -437,30 +410,66 @@ body { /* 响应式设计 */ @media (max-width: 1024px) { .app-container { - max-width: 800px; + max-width: 850px; } } @media (max-width: 768px) { body { - padding: 15px !important; + padding: 0 !important; } .app-container { max-width: 100%; - min-height: calc(100vh - 30px); - border-radius: 8px; + min-height: 100vh; + border-radius: 0; + } + + .global-header { + height: 55px; + } + + .header-content { + padding: 0 16px; + } + + .logo-text { + font-size: 20px; + } + + .main-content { + padding-bottom: 55px; + min-height: calc(100vh - 120px); } } @media (max-width: 480px) { body { - padding: 10px !important; + padding: 0 !important; + margin: 0 !important; } .app-container { border-radius: 0; min-height: 100vh; + box-shadow: none; + } + + .global-header { + height: 50px; + } + + .header-content { + padding: 0 12px; + } + + .logo-text { + font-size: 18px; + } + + .main-content { + padding-bottom: 55px; + min-height: calc(100vh - 110px); } .page-header { @@ -497,4 +506,6 @@ html { background: rgba(229, 62, 62, 0.2); color: #e53e3e; } + + diff --git a/lottery-app/src/api/dlt/index.js b/lottery-app/src/api/dlt/index.js new file mode 100644 index 0000000..2c61376 --- /dev/null +++ b/lottery-app/src/api/dlt/index.js @@ -0,0 +1,380 @@ +import axios from 'axios' + +// 创建大乐透专用的axios实例 +const dltApi = axios.create({ + // baseURL: 'http://localhost:8123/api', + baseURL: 'https://www.yicaishuzhi.com/api', + timeout: 300000, // 5分钟超时时间 + withCredentials: true, // 关键:支持跨域携带cookie和session + headers: { + 'Content-Type': 'application/json' + } +}) + +// 响应拦截器 +dltApi.interceptors.response.use( + response => { + const data = response.data + + // 检查是否是session过期的响应 + if (data && data.success === false) { + const message = data.message || '' + if (message.includes('未登录') || message.includes('登录过期') || message.includes('无权限') || message.includes('Invalid session')) { + console.log('检测到session过期,清除本地登录状态') + + // 动态导入userStore避免循环依赖 + 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过期或无权限') + + // 动态导入userStore避免循环依赖 + import('../../store/user.js').then(({ userStore }) => { + userStore.logout() + }) + } + + return Promise.reject(error) + } +) + +// 大乐透API接口方法 +export const dltLotteryApi = { + // 获取近10期大乐透开奖期号 + getRecent10DrawIds() { + return dltApi.get('/dlt-draw/recent-10-draw-ids') + }, + + // 根据期号获取大乐透开奖号码 + getDrawNumbersById(drawId) { + return dltApi.get(`/dlt-draw/draw/${drawId}/numbers`) + }, + + // 大乐透前区首球分析 + analyzeFrontBalls(level, frontBalls, backBalls) { + return dltApi.post('/dlt/ball-analysis/predict-first-ball', { + level, + redBalls: frontBalls, // 前区球 + blueBalls: backBalls // 后区球 + }) + }, + + // 大乐透前区随球分析 + analyzeFollowFrontBalls(level, wellRegardedBalls, previousFrontBalls, previousBackBalls) { + return dltApi.post('/dlt/ball-analysis/predict-follower-ball', { + level, + wellRegardedBalls, + previousFrontBalls, + previousBackBalls + }) + }, + + // 大乐透后区球分析 + analyzeBackBalls(level, nextFrontBalls, previousFrontBalls, previousBackBalls, nextBackBalls) { + return dltApi.post('/dlt/ball-analysis/predict-back-ball', { + level, + nextFrontBalls, + previousFrontBalls, + previousBackBalls, + nextBackBalls + }) + }, + + // 大乐透后区随球分析 + analyzeFollowBackBalls(level, backFirstBall, nextFrontBalls, previousFrontBalls, previousBackBalls) { + return dltApi.post('/dlt/ball-analysis/predict-follow-back-ball', { + level, + backFirstBall, + nextFrontBalls, + previousFrontBalls, + previousBackBalls + }) + }, + + // 创建大乐透推测记录 + createPredictRecord(userId, drawId, drawDate, frontBalls, backBalls) { + return dltApi.post('/dlt/ball-analysis/create-predict', { + userId, + drawId, + drawDate, + frontBalls: frontBalls.join(','), + backBalls: backBalls.join(',') + }) + }, + + // 获取大乐透推测记录 + getPredictRecordsByUserId(userId, page = 1) { + return dltApi.get(`/dlt/ball-analysis/predict-records/${userId}?page=${page}`) + }, + + // 获取近期大乐透开奖记录 + getRecentDraws(limit = 10) { + return dltApi.get(`/dlt-draw/recent-draws?limit=${limit}`) + }, + + // 根据期号获取大乐透开奖记录 + getDrawById(drawId) { + return dltApi.get(`/dlt-draw/draw/${drawId}`) + }, + + // 根据日期范围查询大乐透开奖记录 + queryDraws(startDate, endDate) { + return dltApi.get(`/dlt-draw/query-draws?startDate=${startDate}&endDate=${endDate}`) + }, + + // 获取近100期大乐透开奖记录(用于表相查询) + getRecent100Draws() { + return dltApi.get('/dlt-draw/recent-100-draws') + }, + + // 创建大乐透预测记录(新接口) + createDltPredictRecord(userId, drawId, frontBalls, backBalls, drawDate = null) { + const params = new URLSearchParams({ + userId: userId, + drawId: drawId, + frontBalls: frontBalls, + backBalls: backBalls + }) + + if (drawDate) { + params.append('drawDate', drawDate) + } + + return dltApi.post(`/dlt/ball-analysis/create-dlt-predict?${params.toString()}`) + }, + + // 前区与前区的组合性分析 + frontFrontCombinationAnalysis(masterBall, slaveBall) { + return dltApi.get(`/dlt/ball-analysis/front-front-combination-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`) + }, + + // 前区与后区的组合性分析 + frontBackCombinationAnalysis(masterBall, slaveBall) { + return dltApi.get(`/dlt/ball-analysis/front-back-combination-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`) + }, + + // 后区与后区的组合性分析 + backBackCombinationAnalysis(masterBall, slaveBall) { + return dltApi.get(`/dlt/ball-analysis/back-back-combination-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`) + }, + + // 后区与前区的组合性分析 + backFrontCombinationAnalysis(masterBall, slaveBall) { + return dltApi.get(`/dlt/ball-analysis/back-front-combination-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`) + }, + + // 前区与前区的持续性分析 + frontFrontPersistenceAnalysis(masterBall, slaveBall) { + return dltApi.get(`/dlt/ball-analysis/front-front-persistence-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`) + }, + + // 后区与后区的持续性分析 + backBackPersistenceAnalysis(masterBall, slaveBall) { + return dltApi.get(`/dlt/ball-analysis/back-back-persistence-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`) + }, + + // 前区与后区的持续性分析 + frontBackPersistenceAnalysis(masterBall, slaveBall) { + return dltApi.get(`/dlt/ball-analysis/front-back-persistence-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`) + }, + + // 后区与前区的持续性分析 + backFrontPersistenceAnalysis(masterBall, slaveBall) { + return dltApi.get(`/dlt/ball-analysis/back-front-persistence-analysis?masterBall=${masterBall}&slaveBall=${slaveBall}`) + }, + + // 根据用户ID获取大乐透预测记录(新接口) + getDltPredictRecordsByUserId(userId, page = 1, pageSize = 10) { + return dltApi.get(`/dlt-predict/predict-records/${userId}?page=${page}&pageSize=${pageSize}`) + }, + + // 条件查询大乐透预测记录 + queryDltPredictRecords(userId, predictStatus, page = 1, pageSize = 10) { + return dltApi.post('/dlt-predict/query-predict-records', { + userId: userId, + predictStatus: predictStatus, + current: page, + pageSize: pageSize + }) + }, + + // 前区历史数据查询 + getFrontendHistoryAll() { + return dltApi.get('/dlt/ball-active-analysis/frontend-history-all') + }, + + getFrontendHistory100() { + return dltApi.get('/dlt/ball-active-analysis/frontend-history-100') + }, + + getFrontendHistoryTop() { + return dltApi.get('/dlt/ball-active-analysis/frontend-history-top') + }, + + getFrontendHistoryTop100() { + return dltApi.get('/dlt/ball-active-analysis/frontend-history-top-100') + }, + + // 后区历史数据查询 + getBackendHistoryAll() { + return dltApi.get('/dlt/ball-active-analysis/backend-history-all') + }, + + getBackendHistory100() { + return dltApi.get('/dlt/ball-active-analysis/backend-history-100') + }, + + getBackendHistoryTop() { + return dltApi.get('/dlt/ball-active-analysis/backend-history-top') + }, + + getBackendHistoryTop100() { + return dltApi.get('/dlt/ball-active-analysis/backend-history-top-100') + }, + + // Excel数据导入相关API + // 上传Excel文件完整导入大乐透数据(D3-D12工作表) + uploadDltExcelFile(file) { + const formData = new FormData() + formData.append('file', file) + return dltApi.post('/dlt/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 上传Excel文件导入大乐透开奖数据(覆盖)(D1工作表) + uploadDltDrawsFile(file) { + const formData = new FormData() + formData.append('file', file) + return dltApi.post('/dlt/upload-draw-data', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 追加导入大乐透开奖数据(D1工作表) + appendDltDrawsFile(file) { + const formData = new FormData() + formData.append('file', file) + return dltApi.post('/dlt/append-draw-data', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 手动处理待开奖记录(双色球+大乐透) + processPendingPredictions() { + return dltApi.post('/dlt-predict/process-pending') + }, + + // 获取用户大乐透预测统计 + getUserPredictStats(userId) { + return dltApi.get(`/dlt-predict/user-predict-stat/${userId}`) + }, + + // 获取用户大乐透奖金统计 + getPrizeStatistics(userId) { + return dltApi.get('/dlt-predict/prize-statistics', { + params: { + userId: userId + } + }) + }, + + // 大乐透命中率分析接口 + // 前区首球命中率分析 + getFrontFirstBallHitRate() { + return dltApi.get('/dlt-predict/front-first-ball-hit-rate') + }, + + // 前区球号命中率分析 + getFrontBallHitRate() { + return dltApi.get('/dlt-predict/front-ball-hit-rate') + }, + + // 后区首球命中率分析 + getBackFirstBallHitRate() { + return dltApi.get('/dlt-predict/back-first-ball-hit-rate') + }, + + // 后区球号命中率分析 + getBackBallHitRate() { + return dltApi.get('/dlt-predict/back-ball-hit-rate') + }, + + // 精推版大乐透第一步分析 + jtdltFirstStepAnalysis(level, frontBalls, backBalls) { + const params = new URLSearchParams() + params.append('level', level) + params.append('frontBalls', frontBalls) + params.append('backBalls', backBalls) + + return dltApi.post('/jtdlt/analysis/first-step', params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + }, + + // 精推版大乐透第二步分析 + jtdltSecondStepAnalysis(level, previousFrontBalls, previousBackBalls, currentFirstBall) { + const params = new URLSearchParams() + params.append('level', level) + params.append('previousFrontBalls', previousFrontBalls) + params.append('previousBackBalls', previousBackBalls) + params.append('currentFirstBall', currentFirstBall) + + return dltApi.post('/jtdlt/analysis/second-step', params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + }, + + // 精推版大乐透第三步分析 + jtdltThirdStepAnalysis(level, previousFrontBalls, previousBackBalls, currentFrontBalls) { + const params = new URLSearchParams() + params.append('level', level) + params.append('previousFrontBalls', previousFrontBalls) + params.append('previousBackBalls', previousBackBalls) + params.append('currentFrontBalls', currentFrontBalls) + + return dltApi.post('/jtdlt/analysis/third-step', params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + }, + + // 精推版大乐透第四步分析 + jtdltFourthStepAnalysis(level, previousFrontBalls, previousBackBalls, currentFrontBalls, currentBackFirstBall) { + const params = new URLSearchParams() + params.append('level', level) + params.append('previousFrontBalls', previousFrontBalls) + params.append('previousBackBalls', previousBackBalls) + params.append('currentFrontBalls', currentFrontBalls) + params.append('currentBackFirstBall', currentBackFirstBall) + + return dltApi.post('/jtdlt/analysis/fourth-step', params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + } +} + +export default dltLotteryApi diff --git a/lottery-app/src/api/index.js b/lottery-app/src/api/index.js index 21e8967..1f71203 100644 --- a/lottery-app/src/api/index.js +++ b/lottery-app/src/api/index.js @@ -3,7 +3,7 @@ import axios from 'axios' // 创建axios实例 const api = axios.create({ // baseURL: 'http://localhost:8123/api', - baseURL: 'https://www.jingcaishuju.com/api', + baseURL: 'https://www.yicaishuzhi.com/api', timeout: 300000, // 5分钟超时时间(300秒) withCredentials: true, // 关键:支持跨域携带cookie和session headers: { @@ -11,6 +11,9 @@ const api = axios.create({ } }) +// 防止重复提示的标志 +let isKickedOut = false + // 响应拦截器 api.interceptors.response.use( response => { @@ -20,16 +23,56 @@ api.interceptors.response.use( if (data && data.success === false) { // 可以根据后端返回的错误码或消息判断是否是session过期 const message = data.message || '' + + // 专门处理账号在其他设备登录的情况 + if (message.includes('其他设备登录') || message.includes('当前会话已失效')) { + if (!isKickedOut) { + isKickedOut = true + console.log('检测到账号在其他设备登录,正在踢出当前会话...') + + // 动态导入 Element Plus 的消息组件 + import('element-plus').then(({ ElMessage }) => { + ElMessage.warning({ + message: '您的账号已在其他设备登录,请重新登录', + duration: 3000, + showClose: true + }) + }) + + // 检查当前路径是否为后台管理路径 + if (window.location.pathname.startsWith('/cpzsadmin') && window.location.pathname !== '/cpzsadmin/login') { + // 后台管理会话 + import('../store/user.js').then(({ userStore }) => { + userStore.adminLogout(true) // 标记为被踢出 + setTimeout(() => { + window.location.href = '/cpzsadmin/login' + isKickedOut = false // 重置标志 + }, 1500) + }) + } else { + // 前台用户会话 + import('../store/user.js').then(({ userStore }) => { + userStore.logout(true) // 标记为被踢出 + setTimeout(() => { + isKickedOut = false // 重置标志 + }, 3000) + }) + } + } + return data + } + + // 处理其他登录/权限相关的错误 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') { + if (window.location.pathname.startsWith('/cpzsadmin') && window.location.pathname !== '/cpzsadmin/login') { console.log('后台管理会话过期,正在注销...') // 动态导入userStore避免循环依赖 import('../store/user.js').then(({ userStore }) => { userStore.adminLogout() - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' }) } else { // 前台用户会话过期处理 @@ -50,12 +93,12 @@ api.interceptors.response.use( console.log('检测到401/403错误,可能是session过期或无权限') // 检查当前路径是否为后台管理路径 - if (window.location.pathname.startsWith('/admin') && window.location.pathname !== '/admin/login') { + if (window.location.pathname.startsWith('/cpzsadmin') && window.location.pathname !== '/cpzsadmin/login') { console.log('后台管理会话过期,正在注销...') // 动态导入userStore避免循环依赖 import('../store/user.js').then(({ userStore }) => { userStore.adminLogout() - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' }) } else { // 前台用户会话过期处理 @@ -99,11 +142,42 @@ export const lotteryApi = { return api.get('/user/get/login') }, + // 获取用户信息(用于编辑) + getUserInfo() { + return api.get('/user/get/login') + }, + + // 更新用户信息 + updateUserInfo(userInfo) { + console.log('调用更新用户信息接口:', userInfo) + return api.post('/user/update', userInfo, { + headers: { + 'Content-Type': 'application/json' + } + }) + }, + + // 上传文件 + uploadFile(file) { + const formData = new FormData() + formData.append('file', file) + return api.post('/file/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + // 获取用户统计信息(总用户数和VIP用户数) getUserCount() { return api.get('/user/count') }, + // 检查当前用户VIP是否过期 + checkVipExpire() { + return api.get('/user/check-vip-expire') + }, + // 获取用户推测记录(支持分页) getPredictRecordsByUserId(userId, page = 1) { return api.get(`/ball-analysis/predict-records/${userId}?page=${page}`) @@ -112,7 +186,7 @@ export const lotteryApi = { // 按条件查询推测记录(支持分页和状态筛选) queryPredictRecords(userId, predictStatus, page = 1, pageSize = 10) { return api.post('/data-analysis/query-predict-records', { - userId: Number(userId), + userId: userId, predictStatus, current: page, pageSize @@ -124,7 +198,7 @@ export const lotteryApi = { return api.get(`/ball-analysis/recent-draws?limit=${limit}`) }, - // 获取最新100条开奖信息(表相性分析) + // 获取最新100条开奖信息(表相查询) getRecent100Draws() { return api.get('/ball-analysis/recent-100-draws') }, @@ -173,6 +247,50 @@ export const lotteryApi = { }) }, + // 精推版双色球第一步分析 + jtssqFirstStepAnalysis(level, redBalls, blueBall) { + const params = new URLSearchParams() + params.append('level', level) + params.append('redBalls', redBalls) + params.append('blueBall', blueBall) + + return api.post('/jtssq/analysis/first-step', params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + }, + + // 精推版双色球第二步分析 + jtssqSecondStepAnalysis(level, redBalls, blueBall, nextFirstBall) { + const params = new URLSearchParams() + params.append('level', level) + params.append('redBalls', redBalls) + params.append('blueBall', blueBall) + params.append('nextFirstBall', nextFirstBall) + + return api.post('/jtssq/analysis/second-step', params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + }, + + // 精推版双色球第三步分析 + jtssqThirdStepAnalysis(level, redBalls, blueBall, nextRedBalls) { + const params = new URLSearchParams() + params.append('level', level) + params.append('redBalls', redBalls) + params.append('blueBall', blueBall) + params.append('nextRedBalls', nextRedBalls) + + return api.post('/jtssq/analysis/third-step', params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + }, + // 跟随球号分析算法 fallowBallAnalysis(userId, level, firstThreeRedBalls, lastSixRedBalls, blueBall) { const params = new URLSearchParams() @@ -207,23 +325,31 @@ export const lotteryApi = { }, // 获取用户推测统计数据 - getUserPredictStat(userId) { - return api.get(`/data-analysis/user-predict-stat/${userId}`) + getUserPredictStat(userId, lotteryType) { + return api.get(`/data-analysis/user-predict-stat/${userId}`, { + params: { lotteryType } + }) }, // 获取首球命中率统计 - getFirstBallHitRate() { - return api.get('/ball-analysis/first-ball-hit-rate') + getFirstBallHitRate(lotteryType) { + return api.get('/ball-analysis/first-ball-hit-rate', { + params: { lotteryType } + }) }, // 获取蓝球命中率统计 - getBlueBallHitRate() { - return api.get('/ball-analysis/blue-ball-hit-rate') + getBlueBallHitRate(lotteryType) { + return api.get('/ball-analysis/blue-ball-hit-rate', { + params: { lotteryType } + }) }, // 获取红球命中率统计 - getRedBallHitRate() { - return api.get('/ball-analysis/red-ball-hit-rate') + getRedBallHitRate(lotteryType) { + return api.get('/ball-analysis/red-ball-hit-rate', { + params: { lotteryType } + }) }, // 获取奖金统计 @@ -657,6 +783,134 @@ export const lotteryApi = { } return api.get(`/operation-history/list${queryParams.toString() ? '?' + queryParams.toString() : ''}`) + }, + + // ==================== 公告管理接口 ==================== + + // 添加公告 + addAnnouncement(data) { + console.log('调用添加公告接口:', data) + return api.post('/announcement/add', data) + }, + + // 查询公告列表(分页) + getAnnouncementList(params) { + console.log('调用查询公告列表接口:', params) + const queryParams = new URLSearchParams() + + if (params?.current) queryParams.append('current', params.current) + if (params?.pageSize) queryParams.append('pageSize', params.pageSize) + if (params?.title) queryParams.append('title', params.title) + if (params?.status !== undefined && params?.status !== null && params?.status !== '') { + queryParams.append('status', params.status) + } + if (params?.priority !== undefined && params?.priority !== null && params?.priority !== '') { + queryParams.append('priority', params.priority) + } + if (params?.publisherId) queryParams.append('publisherId', params.publisherId) + if (params?.publisherName) queryParams.append('publisherName', params.publisherName) + if (params?.startTime) queryParams.append('startTime', params.startTime) + if (params?.endTime) queryParams.append('endTime', params.endTime) + + return api.get(`/announcement/list/page${queryParams.toString() ? '?' + queryParams.toString() : ''}`) + }, + + // 根据ID查询公告详情 + getAnnouncementById(id) { + console.log('调用根据ID查询公告接口:', id) + return api.get(`/announcement/${id}`) + }, + + // 更新公告 + updateAnnouncement(data) { + console.log('调用更新公告接口:', data) + return api.post('/announcement/update', data) + }, + + // 删除公告 + deleteAnnouncement(id) { + console.log('调用删除公告接口:', id) + return api.delete(`/announcement/delete/${id}`) + }, + + // 获取置顶公告 + getTopAnnouncements() { + console.log('调用获取置顶公告接口') + return api.get('/announcement/top') + }, + + // 获取所有已发布公告 + getPublishedAnnouncements() { + console.log('调用获取所有已发布公告接口') + return api.get('/announcement/published') + }, + + // ==================== 推测管理接口 ==================== + + // 管理员获取所有双色球推测记录 + getAllSsqPredictRecords(params) { + const queryParams = new URLSearchParams() + if (params?.userId) queryParams.append('userId', params.userId) + if (params?.predictResult) queryParams.append('predictResult', params.predictResult) + if (params?.current) queryParams.append('current', params.current) + if (params?.pageSize) queryParams.append('pageSize', params.pageSize) + + return api.get(`/ball-analysis/admin/all-records?${queryParams.toString()}`) + }, + + // 管理员获取所有大乐透推测记录 + getAllDltPredictRecords(params) { + const queryParams = new URLSearchParams() + if (params?.userId) queryParams.append('userId', params.userId) + if (params?.predictResult) queryParams.append('predictResult', params.predictResult) + if (params?.current) queryParams.append('current', params.current) + if (params?.pageSize) queryParams.append('pageSize', params.pageSize) + + return api.get(`/dlt-predict/admin/all-records?${queryParams.toString()}`) + }, + + // ==================== 奖金统计接口 ==================== + + // 管理员获取双色球中奖记录明细 + getAdminPrizeStatistics(params) { + const queryParams = new URLSearchParams() + if (params?.userId) queryParams.append('userId', params.userId) + if (params?.prizeGrade) queryParams.append('prizeGrade', params.prizeGrade) + if (params?.current) queryParams.append('current', params.current) + if (params?.pageSize) queryParams.append('pageSize', params.pageSize) + + return api.get(`/ball-analysis/admin/prize-statistics?${queryParams.toString()}`) + }, + + // 管理员获取大乐透中奖记录明细 + getAdminDltPrizeStatistics(params) { + const queryParams = new URLSearchParams() + if (params?.userId) queryParams.append('userId', params.userId) + if (params?.prizeGrade) queryParams.append('prizeGrade', params.prizeGrade) + if (params?.current) queryParams.append('current', params.current) + if (params?.pageSize) queryParams.append('pageSize', params.pageSize) + + return api.get(`/dlt-predict/admin/prize-statistics?${queryParams.toString()}`) + }, + + // 记录页面访问PV + recordPageView(pagePath) { + return api.post(`/pv/record?pagePath=${encodeURIComponent(pagePath)}`) + }, + + // 获取总PV + getTotalPageViews() { + return api.get('/pv/total') + }, + + // 获取今日PV + getTodayPageViews() { + return api.get('/pv/today') + }, + + // 获取近N天PV统计 + getPageViewsByDays(days = 7) { + return api.get(`/pv/stats?days=${days}`) } } diff --git a/lottery-app/src/assets/banner/banner.svg b/lottery-app/src/assets/banner/banner.svg new file mode 100644 index 0000000..3c26d43 --- /dev/null +++ b/lottery-app/src/assets/banner/banner.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/lottery-app/src/assets/font/font1.png b/lottery-app/src/assets/font/font1.png deleted file mode 100644 index d6938fd..0000000 Binary files a/lottery-app/src/assets/font/font1.png and /dev/null differ diff --git a/lottery-app/src/assets/font/font2.png b/lottery-app/src/assets/font/font2.png deleted file mode 100644 index e48261f..0000000 Binary files a/lottery-app/src/assets/font/font2.png and /dev/null differ diff --git a/lottery-app/src/assets/font/font3.png b/lottery-app/src/assets/font/font3.png deleted file mode 100644 index 75be0b3..0000000 Binary files a/lottery-app/src/assets/font/font3.png and /dev/null differ diff --git a/lottery-app/src/assets/font/font4.png b/lottery-app/src/assets/font/font4.png deleted file mode 100644 index 59202c5..0000000 Binary files a/lottery-app/src/assets/font/font4.png and /dev/null differ diff --git a/lottery-app/src/assets/font/font5.png b/lottery-app/src/assets/font/font5.png deleted file mode 100644 index 7e1d543..0000000 Binary files a/lottery-app/src/assets/font/font5.png and /dev/null differ diff --git a/lottery-app/src/assets/main.css b/lottery-app/src/assets/main.css index 8a9fa52..004a380 100644 --- a/lottery-app/src/assets/main.css +++ b/lottery-app/src/assets/main.css @@ -5,6 +5,7 @@ font-weight: normal; width: 100%; min-height: 100vh; + background-color: rgb(240, 242, 245); } a, diff --git a/lottery-app/src/assets/weixin.png b/lottery-app/src/assets/weixin.png deleted file mode 100644 index 508e58f..0000000 Binary files a/lottery-app/src/assets/weixin.png and /dev/null differ diff --git a/lottery-app/src/components/AlgorithmProcessModal.vue b/lottery-app/src/components/AlgorithmProcessModal.vue new file mode 100644 index 0000000..d08772a --- /dev/null +++ b/lottery-app/src/components/AlgorithmProcessModal.vue @@ -0,0 +1,440 @@ + + + + + diff --git a/lottery-app/src/components/BottomNavigation.vue b/lottery-app/src/components/BottomNavigation.vue new file mode 100644 index 0000000..2c3c7b0 --- /dev/null +++ b/lottery-app/src/components/BottomNavigation.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/lottery-app/src/components/CozeChat.vue b/lottery-app/src/components/CozeChat.vue new file mode 100644 index 0000000..f12996e --- /dev/null +++ b/lottery-app/src/components/CozeChat.vue @@ -0,0 +1,526 @@ + + + + + diff --git a/lottery-app/src/components/CustomSelect.vue b/lottery-app/src/components/CustomSelect.vue new file mode 100644 index 0000000..60ac710 --- /dev/null +++ b/lottery-app/src/components/CustomSelect.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/lottery-app/src/main.js b/lottery-app/src/main.js index 5d41268..f7fa1fd 100644 --- a/lottery-app/src/main.js +++ b/lottery-app/src/main.js @@ -11,6 +11,8 @@ import './store/user' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import * as ElementPlusIconsVue from '@element-plus/icons-vue' +// 导入 Element Plus 中文语言包 +import zhCn from 'element-plus/es/locale/lang/zh-cn' // 导入 Toast 通知组件 import Toast from 'vue-toastification' @@ -41,6 +43,8 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.use(router) app.use(Toast, toastOptions) -app.use(ElementPlus) +app.use(ElementPlus, { + locale: zhCn, +}) app.mount('#app') diff --git a/lottery-app/src/router/index.js b/lottery-app/src/router/index.js index 94dc2e6..f190a93 100644 --- a/lottery-app/src/router/index.js +++ b/lottery-app/src/router/index.js @@ -1,31 +1,64 @@ import { createRouter, createWebHistory } from 'vue-router' import { ElMessage } from 'element-plus' import LotterySelection from '../views/LotterySelection.vue' -import Home from '../views/Home.vue' +import LotteryPremium from '../views/LotteryPremium.vue' +import Home from '../views/ssq/Home.vue' +import DltHome from '../views/dlt/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 DltPredictRecords from '../views/dlt/PredictRecords.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 TrendAnalysis from '../views/ssq/TrendAnalysis.vue' +import SurfaceAnalysis from '../views/ssq/SurfaceAnalysis.vue' +import LineAnalysis from '../views/ssq/LineAnalysis.vue' +import SsqTableAnalysis from '../views/ssq/SsqTableAnalysis.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 UserGuide from '../views/UserGuide.vue' +import MemberAgreement from '../views/MemberAgreement.vue' +import PrivacyPolicy from '../views/PrivacyPolicy.vue' +import HitAnalysis from '../views/ssq/HitAnalysis.vue' +import DltHitAnalysis from '../views/dlt/HitAnalysis.vue' +import UsageStats from '../views/ssq/UsageStats.vue' +import DltUsageStats from '../views/dlt/UsageStats.vue' +import PrizeStatistics from '../views/ssq/PrizeStatistics.vue' +import DltPrizeStatistics from '../views/dlt/PrizeStatistics.vue' + +// 双色球相关页面 +import SsqLottery from '../views/ssq/Lottery.vue' + +// 大乐透相关页面 +import DltLottery from '../views/dlt/Lottery.vue' +import DltTableAnalysis from '../views/dlt/DltTableAnalysis.vue' +import DltSurfaceAnalysis from '../views/dlt/SurfaceAnalysis.vue' +import DltLineAnalysis from '../views/dlt/LineAnalysis.vue' +import DltTrendAnalysis from '../views/dlt/TrendAnalysis.vue' + +// 精推版页面 +import JtSsqHome from '../views/jt/SsqHome.vue' +import JtDltHome from '../views/jt/DltHome.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' +import AdminDltExcelImportManagement from '../views/admin/DltExcelImportManagement.vue' +import AdminPredictionManagement from '../views/admin/PredictionManagement.vue' +import AdminDltPredictionManagement from '../views/admin/DltPredictionManagement.vue' +import AdminPrizeStatistics from '../views/admin/PrizeStatistics.vue' +import AdminDltPrizeStatistics from '../views/admin/DltPrizeStatistics.vue' +import AdminUsageStats from '../views/ssq/UsageStats.vue' +import AdminDltUsageStats from '../views/dlt/UsageStats.vue' const routes = [ // 前台用户路由 @@ -34,16 +67,70 @@ const routes = [ name: 'LotterySelection', component: LotterySelection }, + { + path: '/lottery-premium', + name: 'LotteryPremium', + component: LotteryPremium + }, { path: '/shuangseqiu', name: 'Shuangseqiu', component: Home }, + { + path: '/daletou', + name: 'DaLeTou', + component: DltHome + }, + { + path: '/jt/shuangseqiu', + name: 'JtShuangseqiu', + component: JtSsqHome + }, + { + path: '/jt/daletou', + name: 'JtDaLeTou', + component: JtDltHome + }, { path: '/lottery-info', name: 'LotteryInfo', component: LotteryInfo }, + { + path: '/lottery-info/ssq', + name: 'SsqLottery', + component: SsqLottery + }, + { + path: '/lottery-info/dlt', + name: 'DltLottery', + component: DltLottery + }, + { + path: '/dlt-table-analysis', + name: 'DltTableAnalysis', + component: DltTableAnalysis, + meta: { requiresAuth: true } + }, + { + path: '/dlt-surface-analysis', + name: 'DltSurfaceAnalysis', + component: DltSurfaceAnalysis, + meta: { requiresAuth: true } + }, + { + path: '/dlt-line-analysis', + name: 'DltLineAnalysis', + component: DltLineAnalysis, + meta: { requiresAuth: true } + }, + { + path: '/dlt-trend-analysis', + name: 'DltTrendAnalysis', + component: DltTrendAnalysis, + meta: { requiresAuth: true } + }, { path: '/profile', name: 'Profile', @@ -70,10 +157,12 @@ const routes = [ component: PredictRecords }, { - path: '/vip-management', - name: 'VipCodeManagement', - component: VipCodeManagement + path: '/dlt/predict-records', + name: 'DltPredictRecords', + component: DltPredictRecords, + meta: { requiresAuth: true } }, + { path: '/excel-import', name: 'ExcelImportManagement', @@ -106,6 +195,42 @@ const routes = [ component: DataAnalysis, meta: { requiresAuth: true } }, + { + path: '/hit-analysis', + name: 'HitAnalysis', + component: HitAnalysis, + meta: { requiresAuth: true } + }, + { + path: '/daletou/hit-analysis', + name: 'DltHitAnalysis', + component: DltHitAnalysis, + meta: { requiresAuth: true } + }, + { + path: '/usage-stats', + name: 'UsageStats', + component: UsageStats, + meta: { requiresAuth: true } + }, + { + path: '/daletou/usage-stats', + name: 'DltUsageStats', + component: DltUsageStats, + meta: { requiresAuth: true } + }, + { + path: '/prize-statistics', + name: 'PrizeStatistics', + component: PrizeStatistics, + meta: { requiresAuth: true } + }, + { + path: '/daletou/prize-statistics', + name: 'DltPrizeStatistics', + component: DltPrizeStatistics, + meta: { requiresAuth: true } + }, { path: '/help-center', name: 'HelpCenter', @@ -125,19 +250,33 @@ const routes = [ meta: { requiresAuth: true } }, { - path: '/table-analysis', - name: 'TableAnalysis', - component: TableAnalysis + path: '/user-guide', + name: 'UserGuide', + component: UserGuide, + meta: { requiresAuth: true } }, { - path: '/ai-assistant', - name: 'AIAssistant', - component: AIAssistant + path: '/member-agreement', + name: 'MemberAgreement', + component: MemberAgreement, + meta: { requiresAuth: false } }, + { + path: '/privacy-policy', + name: 'PrivacyPolicy', + component: PrivacyPolicy, + meta: { requiresAuth: false } + }, + { + path: '/table-analysis', + name: 'SsqTableAnalysis', + component: SsqTableAnalysis + }, + // 后台管理路由 - 完全隔离 { - path: '/admin/login', + path: '/cpzsadmin/login', name: 'AdminLogin', component: AdminLogin, meta: { @@ -147,7 +286,7 @@ const routes = [ } }, { - path: '/admin', + path: '/cpzsadmin', component: AdminLayout, meta: { requiresAuth: true, @@ -156,7 +295,7 @@ const routes = [ children: [ { path: '', - redirect: '/admin/dashboard' + redirect: '/cpzsadmin/dashboard' }, { path: 'dashboard', @@ -180,13 +319,133 @@ const routes = [ }, { path: 'excel-import', - name: 'AdminExcelImportManagement', - component: AdminExcelImportManagement, meta: { title: '数据导入', requiresAuth: true, isAdmin: true - } + }, + children: [ + { + path: '', + name: 'AdminExcelImportManagement', + component: AdminExcelImportManagement, + meta: { + title: '数据导入', + requiresAuth: true, + isAdmin: true + } + }, + { + path: 'ssq', + name: 'AdminExcelImportSSQ', + component: AdminExcelImportManagement, + meta: { + title: '双色球数据导入', + requiresAuth: true, + isAdmin: true + } + }, + { + path: 'dlt', + name: 'AdminExcelImportDLT', + component: AdminDltExcelImportManagement, + meta: { + title: '大乐透数据导入', + requiresAuth: true, + isAdmin: true + } + } + ] + }, + { + path: 'prediction', + meta: { + title: '推测管理', + requiresAuth: true, + isAdmin: true + }, + children: [ + { + path: 'ssq', + name: 'AdminPredictionSSQ', + component: AdminPredictionManagement, + meta: { + title: '双色球推测管理', + requiresAuth: true, + isAdmin: true + } + }, + { + path: 'dlt', + name: 'AdminPredictionDLT', + component: AdminDltPredictionManagement, + meta: { + title: '大乐透推测管理', + requiresAuth: true, + isAdmin: true + } + } + ] + }, + { + path: 'prize-statistics', + meta: { + title: '奖金统计', + requiresAuth: true, + isAdmin: true + }, + children: [ + { + path: 'ssq', + name: 'AdminPrizeStatisticsSSQ', + component: AdminPrizeStatistics, + meta: { + title: '双色球奖金统计', + requiresAuth: true, + isAdmin: true + } + }, + { + path: 'dlt', + name: 'AdminPrizeStatisticsDLT', + component: AdminDltPrizeStatistics, + meta: { + title: '大乐透奖金统计', + requiresAuth: true, + isAdmin: true + } + } + ] + }, + { + path: 'usage-stats', + meta: { + title: '使用统计', + requiresAuth: true, + isAdmin: true + }, + children: [ + { + path: 'ssq', + name: 'AdminUsageStatsSSQ', + component: AdminUsageStats, + meta: { + title: '双色球使用统计', + requiresAuth: true, + isAdmin: true + } + }, + { + path: 'dlt', + name: 'AdminUsageStatsDLT', + component: AdminDltUsageStats, + meta: { + title: '大乐透使用统计', + requiresAuth: true, + isAdmin: true + } + } + ] }, { path: 'user-list', @@ -207,6 +466,16 @@ const routes = [ requiresAuth: true, isAdmin: true } + }, + { + path: 'announcement', + name: 'AdminAnnouncementManagement', + component: () => import('../views/admin/AnnouncementManagement.vue'), + meta: { + title: '公告管理', + requiresAuth: true, + isAdmin: true + } } ] }, @@ -221,7 +490,16 @@ const routes = [ const router = createRouter({ history: createWebHistory(), - routes + routes, + scrollBehavior(to, from, savedPosition) { + // 如果有保存的位置(比如浏览器后退),使用保存的位置 + if (savedPosition) { + return savedPosition + } else { + // 否则滚动到页面顶部 + return { top: 0, behavior: 'smooth' } + } + } }) // 路由守卫 - 权限控制 @@ -244,7 +522,7 @@ router.beforeEach((to, from, next) => { // 检查是否已登录(使用session存储) if (!userStore.isAdminLoggedIn()) { ElMessage.error('请先登录后台管理系统') - next('/admin/login') + next('/cpzsadmin/login') return } @@ -252,14 +530,14 @@ router.beforeEach((to, from, next) => { const adminInfo = JSON.parse(sessionStorage.getItem('adminInfo') || '{}') if (adminInfo.userRole === 'user') { ElMessage.error('您没有权限访问后台管理系统') - next('/admin/login') + next('/cpzsadmin/login') return } next() }).catch(error => { console.error('加载用户状态出错:', error) - next('/admin/login') + next('/cpzsadmin/login') }) } else { next() diff --git a/lottery-app/src/store/user.js b/lottery-app/src/store/user.js index 797a443..b7260fd 100644 --- a/lottery-app/src/store/user.js +++ b/lottery-app/src/store/user.js @@ -6,6 +6,8 @@ export const userStore = reactive({ // 用户信息 user: null, isLoggedIn: false, + isKickedOut: false, // 标记是否被其他设备踢出 + lastCheckTime: null, // 最后一次检查登录状态的时间 // 获取登录用户信息 async fetchLoginUser() { @@ -15,7 +17,7 @@ export const userStore = reactive({ const userData = response.data // 更新用户信息,保留现有的本地数据结构 this.user = { - id: userData.id, + id: String(userData.id), // 确保ID始终为字符串,避免精度丢失 username: userData.userName || userData.userAccount || userData.username || userData.name, email: userData.email, phone: userData.phone, @@ -70,6 +72,8 @@ export const userStore = reactive({ } } this.isLoggedIn = true + this.isKickedOut = false // 重置被踢出状态 + this.lastCheckTime = Date.now() // 记录登录时间 // 保存到本地存储 localStorage.setItem('user', JSON.stringify(this.user)) @@ -77,9 +81,16 @@ export const userStore = reactive({ }, // 登出 - logout() { + logout(isKickedOut = false) { + // 如果是被踢出,标记状态 + if (isKickedOut) { + this.isKickedOut = true + console.log('[安全] 账号在其他设备登录,当前会话已被踢出') + } + this.user = null; this.isLoggedIn = false; + this.lastCheckTime = null; // 清除所有本地存储的用户信息 localStorage.removeItem('user'); @@ -106,7 +117,12 @@ export const userStore = reactive({ const savedLoginState = localStorage.getItem('isLoggedIn') if (savedUser && savedLoginState === 'true') { - this.user = JSON.parse(savedUser) + const user = JSON.parse(savedUser) + // 确保ID始终为字符串,避免精度丢失 + if (user.id) { + user.id = String(user.id) + } + this.user = user this.isLoggedIn = true } }, @@ -132,7 +148,7 @@ export const userStore = reactive({ // 设置用户信息(用于管理员登录) setUserInfo(userInfo) { this.user = { - id: userInfo.id, + id: String(userInfo.id), // 确保ID始终为字符串,避免精度丢失 username: userInfo.userAccount || userInfo.userName, nickname: userInfo.userName || userInfo.userAccount, avatar: userInfo.avatar || null, @@ -187,13 +203,65 @@ export const userStore = reactive({ }, // 管理员登出 - adminLogout() { + adminLogout(isKickedOut = false) { + // 如果是被踢出,标记状态 + if (isKickedOut) { + this.isKickedOut = true + console.log('[安全] 管理员账号在其他设备登录,当前会话已被踢出') + } + this.user = null; this.isLoggedIn = false; + this.lastCheckTime = null; // 清除所有session存储的管理员信息 sessionStorage.removeItem('adminInfo'); sessionStorage.removeItem('adminLoggedIn'); + }, + + // 主动检查登录状态是否有效 + async checkLoginStatus() { + // 如果没有登录,直接返回 + if (!this.isLoggedIn) { + return { valid: false, reason: 'not_logged_in' } + } + + // 避免频繁检查(10秒内只检查一次) + const now = Date.now() + if (this.lastCheckTime && (now - this.lastCheckTime) < 10000) { + return { valid: true, reason: 'recently_checked' } + } + + try { + // 调用后端接口验证登录状态 + const response = await lotteryApi.getLoginUser() + + if (response.success === true) { + // 登录状态有效,更新检查时间 + this.lastCheckTime = now + this.isKickedOut = false + return { valid: true, reason: 'verified' } + } else { + // 检查是否是被踢出 + const message = response.message || '' + if (message.includes('其他设备登录') || message.includes('当前会话已失效')) { + this.logout(true) // 标记为被踢出 + return { valid: false, reason: 'kicked_out', message } + } else { + this.logout(false) + return { valid: false, reason: 'session_expired', message } + } + } + } catch (error) { + console.error('[Store] 检查登录状态失败:', error) + // 网络错误等情况,不清除登录状态,让用户继续使用 + return { valid: true, reason: 'check_failed', error } + } + }, + + // 重置被踢出状态(用于重新登录后) + resetKickedOutStatus() { + this.isKickedOut = false } }) diff --git a/lottery-app/src/styles/global.css b/lottery-app/src/styles/global.css index f8b77a4..5377866 100644 --- a/lottery-app/src/styles/global.css +++ b/lottery-app/src/styles/global.css @@ -250,4 +250,43 @@ body.admin-body { font-size: 14px; margin-bottom: 15px; border-left: 4px solid #4caf50; +} + +/* 隐藏子页面顶部背景图的浮动元素 */ +.page-header-modern::before { + display: none !important; +} + +/* 移除所有链接的选中效果 */ +a, a:link, a:visited, a:hover, a:active, a:focus { + -webkit-tap-highlight-color: transparent !important; + -webkit-touch-callout: none !important; + -webkit-user-select: none !important; + -khtml-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; + outline: none !important; +} + +/* 移除所有元素的tap高亮效果 */ +* { + -webkit-tap-highlight-color: transparent !important; + -webkit-touch-callout: none !important; +} + +/* 特别针对协议链接 */ +.terms-link, +.terms-link:link, +.terms-link:visited, +.terms-link:hover, +.terms-link:active, +.terms-link:focus { + -webkit-tap-highlight-color: transparent !important; + -webkit-touch-callout: none !important; + -webkit-user-select: none !important; + user-select: none !important; + outline: none !important; + background: transparent !important; + background-color: transparent !important; } \ No newline at end of file diff --git a/lottery-app/src/views/AIAssistant.vue b/lottery-app/src/views/AIAssistant.vue deleted file mode 100644 index 1dc336a..0000000 --- a/lottery-app/src/views/AIAssistant.vue +++ /dev/null @@ -1,636 +0,0 @@ - - - - - \ No newline at end of file diff --git a/lottery-app/src/views/AboutUs.vue b/lottery-app/src/views/AboutUs.vue index 2b7d28f..7e0004d 100644 --- a/lottery-app/src/views/AboutUs.vue +++ b/lottery-app/src/views/AboutUs.vue @@ -1,91 +1,69 @@ \ No newline at end of file diff --git a/lottery-app/src/views/HelpCenter.vue b/lottery-app/src/views/HelpCenter.vue index 87feac5..03eca8e 100644 --- a/lottery-app/src/views/HelpCenter.vue +++ b/lottery-app/src/views/HelpCenter.vue @@ -1,60 +1,114 @@ - - \ No newline at end of file diff --git a/lottery-app/src/views/Login.vue b/lottery-app/src/views/Login.vue index d26443b..f160be6 100644 --- a/lottery-app/src/views/Login.vue +++ b/lottery-app/src/views/Login.vue @@ -3,13 +3,25 @@
-

彩票猪手

+

用户登录

您的专属彩票数据助理

+ + +
- +
- +
+ + + {{ codeButtonText }} + +
{{ errors.phone }}
请输入11位手机号码
@@ -92,17 +116,7 @@ prefix-icon="Key" size="large" maxlength="6" - > - - + />
{{ errors.code }}
@@ -126,24 +140,6 @@ - -
-
-

登录成功

-

欢迎回来!正在跳转到个人中心...

- -
-
- - -
-
-
❌
-

登录失败

-

{{ errorMessage }}

- -
-
@@ -152,7 +148,7 @@ 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 { ElCard, ElInput, ElButton, ElCheckbox, ElAlert } from 'element-plus' import { User, Lock, Iphone, Key } from '@element-plus/icons-vue' export default { @@ -162,6 +158,7 @@ export default { ElInput, ElButton, ElCheckbox, + ElAlert, User, Lock, Iphone, @@ -177,13 +174,11 @@ export default { loginType: 'account', // 默认使用账号登录方式 showPassword: false, loading: false, - showSuccessModal: false, - showErrorModal: false, - errorMessage: '', codeCountdown: 0, timer: null, showPhoneError: false, phoneValid: false, + showKickedOutAlert: false, // 是否显示被踢出提示 formData: { username: '', @@ -204,6 +199,16 @@ export default { return this.codeCountdown > 0 || !this.isValidPhone(this.formData.phone); } }, + mounted() { + // 检查是否因为被踢出而跳转到登录页 + if (userStore.isKickedOut) { + this.showKickedOutAlert = true + // 显示提示后重置状态 + setTimeout(() => { + userStore.resetKickedOutStatus() + }, 500) + } + }, methods: { // 切换登录方式 switchLoginType(type) { @@ -386,50 +391,37 @@ export default { try { const userInfo = await userStore.fetchLoginUser(); if (userInfo) { - // 显示成功提示 - this.showSuccessModal = true; + // 触发Coze SDK重新初始化事件 + setTimeout(() => { + window.dispatchEvent(new CustomEvent('reinitializeCozeSDK')); + console.log('已触发Coze SDK重新初始化事件'); + }, 200); + + // 直接跳转到个人中心 + setTimeout(() => { + this.router.push('/profile'); + }, 300); } else { - this.showError('获取用户信息失败,请重新登录'); + this.toast.error('获取用户信息失败,请重新登录'); } } catch (error) { console.error('获取用户信息失败:', error); - this.showError('获取用户信息失败,请重新登录'); + this.toast.error('获取用户信息失败,请重新登录'); } } else { // 登录失败 - this.showError(response.message || '登录失败,请检查账号密码'); + this.toast.error(response.message || '登录失败,请检查账号密码'); } } catch (error) { console.error('登录失败:', error); if (error.response && error.response.data) { - this.showError(error.response.data.message || '登录失败,请检查账号密码'); + this.toast.error(error.response.data.message || '登录失败,请检查账号密码'); } else { - this.showError('网络错误,请重试'); + this.toast.error('网络错误,请重试'); } } finally { this.loading = false; } - }, - - // 关闭成功弹窗 - closeSuccessModal() { - this.showSuccessModal = false; - // 延迟跳转,让用户看到弹窗 - setTimeout(() => { - this.router.push('/profile'); - }, 500); - }, - - // 显示错误弹窗 - showError(message) { - this.errorMessage = message; - this.showErrorModal = true; - }, - - // 关闭错误弹窗 - closeErrorModal() { - this.showErrorModal = false; - this.errorMessage = ''; } }, // 组件销毁时清除定时器 @@ -447,18 +439,19 @@ export default { .login-page-container { min-height: calc(100vh - 70px); background: #f0f2f5; - padding: 20px 20px 0px 20px; + padding: 20px 20px 8px 20px; } /* 页面头部 */ .page-header { - background: url('@/assets/banner/backend1.png') center/cover no-repeat, linear-gradient(135deg, #ff6b6b, #ee5a52); + background: linear-gradient(135deg, #ff6b6b, #ee5a52); color: white; - padding: 45px 20px 25px; + padding: 35px 20px 25px; text-align: center; position: relative; - margin-bottom: 20px; - border-radius: 8px; + margin-bottom: 15px; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(238, 90, 82, 0.3); } .page-title { @@ -467,31 +460,31 @@ export default { } .main-title { - font-size: 38px; - margin: 0 auto 10px; + font-size: 32px; + margin: 0 auto 4px; 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-shadow: 0 2px 8px rgba(0,0,0,0.5), 0 0 20px rgba(0,0,0,0.3); + letter-spacing: 1px; text-align: center; width: 100%; } .subtitle { - font-size: 22px; + font-size: 16px; 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); + opacity: 0.95; + text-shadow: 0 2px 4px rgba(0,0,0,0.4); text-align: center; width: 100%; - font-weight: 500; + font-weight: 400; } /* 桌面端样式 */ @media (min-width: 1024px) { .page-header { - padding: 40px 20px 30px; + padding: 30px 20px 25px; } } @@ -499,59 +492,52 @@ export default { padding: 0; background: white; margin: 0 0 20px 0; - border-radius: 8px; - box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); + overflow: hidden; } /* 登录方式切换标签 */ .login-tabs { display: flex; - border-bottom: 1px solid #eee; + border-bottom: 1px solid #f0f0f0; + background: #fafafa; } .login-tab { flex: 1; text-align: center; - padding: 15px 0; - font-size: 16px; - color: #666; + padding: 18px 0; + font-size: 15px; + color: #888; cursor: pointer; - transition: all 0.3s; + transition: all 0.3s ease; position: relative; + font-weight: 500; } .login-tab.active { color: #e53e3e; - font-weight: 500; + background: white; + font-weight: 600; } -.login-tab.active::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 40px; - height: 3px; - background: #e53e3e; - border-radius: 2px; -} .login-tab:hover:not(.active) { - color: #333; + color: #666; + background: #f8f8f8; } .login-form { background: white; border-radius: 0; - padding: 30px 25px; + padding: 28px 24px 20px; box-shadow: none; - min-height: calc(100vh - 400px); } /* 表单组 */ .form-group { - margin-bottom: 24px; + margin-bottom: 16px; } .input-wrapper { @@ -634,7 +620,49 @@ input:-webkit-autofill:active { outline: none !important; } -/* Element Plus验证码按钮已在组件中实现,移除原来的样式以避免冲突 */ +/* 手机号和验证码按钮同行布局 */ +.phone-code-row { + display: flex; + gap: 12px; + align-items: stretch; +} + +.phone-input { + flex: 1; +} + +/* 内联发送验证码按钮样式 */ +.send-code-btn-inline { + background: linear-gradient(135deg, #e53e3e, #ff6b6b); + border: none; + border-radius: 12px; + font-weight: 500; + transition: all 0.3s ease; + min-width: 120px; + flex-shrink: 0; + height: auto; + display: flex; + align-items: center; + justify-content: center; +} + +.send-code-btn-inline:hover:not(.is-disabled) { + background: linear-gradient(135deg, #d43030, #ff5a5a); + transform: translateY(-1px); + box-shadow: 0 4px 15px rgba(229, 62, 62, 0.3); +} + +.send-code-btn-inline:active:not(.is-disabled) { + transform: translateY(0); +} + +.send-code-btn-inline.is-disabled { + background: #cccccc !important; + border-color: #cccccc !important; + color: #888 !important; + transform: none !important; + box-shadow: none !important; +} /* 提示文本 */ .error-text { @@ -686,7 +714,6 @@ input::-webkit-credentials-auto-fill-button { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 25px; } .checkbox-wrapper { @@ -736,42 +763,57 @@ input::-webkit-credentials-auto-fill-button { /* 登录按钮 */ .login-btn { width: 100%; - margin: 30px 0 25px 0; - padding: 12px; - font-size: 18px; - height: auto; + margin: 24px 0 24px 0; + padding: 14px; + font-size: 16px; + font-weight: 600; + height: 52px; background: linear-gradient(135deg, #e53e3e, #ff6b6b); border: none; - box-shadow: 0 4px 15px rgba(229, 62, 62, 0.3); + border-radius: 12px; + box-shadow: 0 4px 20px rgba(229, 62, 62, 0.25); + transition: all 0.3s ease; } .login-btn:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(229, 62, 62, 0.4); + transform: translateY(-1px); + box-shadow: 0 6px 30px rgba(229, 62, 62, 0.35); background: linear-gradient(135deg, #d43030, #ff5a5a); border: none; } +.login-btn:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 2px 10px rgba(229, 62, 62, 0.3); +} + /* Element UI 组件自定义样式 */ :deep(.el-input__wrapper) { - padding: 4px 11px; + padding: 6px 16px; box-shadow: none !important; background-color: #f8f9fa; border: 2px solid #e9ecef; + border-radius: 12px; + transition: all 0.3s ease; } + :deep(.el-input__wrapper.is-focus) { background-color: #fff; border-color: #e53e3e; + box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1); } + :deep(.el-input__prefix) { - margin-right: 8px; + margin-right: 12px; + color: #999; } :deep(.el-input__inner) { - height: 40px; - font-size: 16px; + height: 44px; + font-size: 15px; + color: #333; } :deep(.el-checkbox__label) { @@ -788,35 +830,6 @@ input::-webkit-credentials-auto-fill-button { 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; @@ -840,175 +853,72 @@ input::-webkit-credentials-auto-fill-button { 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) { .login-page-container { - padding: 10px; + padding: 10px 10px 5px 10px; } .login-form { - padding: 25px 20px; + padding: 22px 20px 18px; } - .send-code-btn { - font-size: 12px; - min-width: 90px; - padding: 0 10px; - } } @media (max-width: 480px) { + .login-page-container { + padding: 5px 5px 3px 5px; + } + .page-header { - padding: 40px 12px 30px; + padding: 30px 20px 25px; + margin-bottom: 12px; } .page-title { - margin: 0 0 6px 0; + margin: 0; } .main-title { font-size: 28px; margin-bottom: 3px; + letter-spacing: 0.5px; } .subtitle { - font-size: 18px; + font-size: 14px; } + .login-form { - padding: 20px 15px; + padding: 28px 20px 20px; } - .input-icon { - padding: 12px 25px; + .login-tab { + padding: 16px 0; + font-size: 14px; } - .icon-img { - width: 22px; - height: 22px; + :deep(.el-input__inner) { + height: 42px; + font-size: 14px; } - .toggle-icon-img { - width: 20px; - height: 20px; + .login-btn { + height: 48px; + font-size: 15px; + margin: 20px 0 20px 0; } - .password-toggle { - padding: 0 12px; - } - - .send-code-btn { + .send-code-btn-inline { font-size: 12px; - min-width: 85px; - padding: 0 8px; + min-width: 90px; + padding: 0 10px; + } + + .phone-code-row { + gap: 8px; } } /* 桌面端样式 - 这部分已在上面定义,这里移除重复定义 */ - \ No newline at end of file + \ No newline at end of file diff --git a/lottery-app/src/views/LotteryInfo.vue b/lottery-app/src/views/LotteryInfo.vue index f0a0f3a..3fcba21 100644 --- a/lottery-app/src/views/LotteryInfo.vue +++ b/lottery-app/src/views/LotteryInfo.vue @@ -1,823 +1,770 @@ \ No newline at end of file + \ No newline at end of file diff --git a/lottery-app/src/views/LotteryPremium.vue b/lottery-app/src/views/LotteryPremium.vue new file mode 100644 index 0000000..280512c --- /dev/null +++ b/lottery-app/src/views/LotteryPremium.vue @@ -0,0 +1,1275 @@ + + + + + \ No newline at end of file diff --git a/lottery-app/src/views/LotterySelection.vue b/lottery-app/src/views/LotterySelection.vue index 7a143cb..a27c7e3 100644 --- a/lottery-app/src/views/LotterySelection.vue +++ b/lottery-app/src/views/LotterySelection.vue @@ -1,105 +1,138 @@ \ No newline at end of file diff --git a/lottery-app/src/views/MemberAgreement.vue b/lottery-app/src/views/MemberAgreement.vue new file mode 100644 index 0000000..264c897 --- /dev/null +++ b/lottery-app/src/views/MemberAgreement.vue @@ -0,0 +1,541 @@ + + + + + diff --git a/lottery-app/src/views/NotFound.vue b/lottery-app/src/views/NotFound.vue index 22005a2..8c8261e 100644 --- a/lottery-app/src/views/NotFound.vue +++ b/lottery-app/src/views/NotFound.vue @@ -1,98 +1,361 @@ \ No newline at end of file diff --git a/lottery-app/src/views/PredictRecords.vue b/lottery-app/src/views/PredictRecords.vue index e758744..a6ff277 100644 --- a/lottery-app/src/views/PredictRecords.vue +++ b/lottery-app/src/views/PredictRecords.vue @@ -1,51 +1,126 @@ @@ -336,34 +458,38 @@ import { userStore } from '../store/user' import { lotteryApi } from '../api/index.js' import QRCode from 'qrcode' -import { ElIcon, ElTable, ElTableColumn } from 'element-plus' +import { ElIcon, ElTable, ElTableColumn, ElMessage } from 'element-plus' +import CustomSelect from '../components/CustomSelect.vue' import { - Search, - DataAnalysis, - Aim, - FolderChecked, - ChatDotRound, + View, + TrendCharts, + Star, + Download, + Comment, + Reading, Collection, Tickets, ShoppingBag, - Document + Warning } from '@element-plus/icons-vue' export default { name: 'Profile', components: { ElIcon, - Search, - DataAnalysis, - Aim, - FolderChecked, - ChatDotRound, + View, + TrendCharts, + Star, + Download, + Comment, + Reading, Collection, ElTable, ElTableColumn, Tickets, ShoppingBag, - Document + Warning, + CustomSelect }, data() { return { @@ -374,7 +500,11 @@ export default { showLogoutConfirm: false, showMemberExchangeModal: false, showCustomerServiceModal: false, + showEditProfileModal: false, + savingUserInfo: false, + uploadingAvatar: false, exchangeCode: '', + agreeTerms: true, statsLoading: false, userInfoLoading: false, detailedStatsLoading: false, @@ -390,7 +520,21 @@ export default { redBall: { totalHitCount: 0, totalPredictedCount: 0, hitRate: 0 }, prize: { totalPrize: 0, prizeDetails: [] } }, - cozeWebSDK: null + editForm: { + id: '', + userAccount: '', + userRole: '', + userName: '', + userAvatar: '', + phone: '', + gender: 2, + location: '', + preference: '', + channel: '', + createTime: '', + vipType: '', + vipExpire: '' + } } }, async mounted() { @@ -405,12 +549,7 @@ export default { // 初始检测设备类型 this.checkDeviceType(); - - // 监听窗口大小变化 - window.addEventListener('resize', this.checkDeviceType); - - // 加载Coze Chat SDK - this.loadCozeChatSDK() + }, async activated() { if (this.userStore.isLoggedIn) { @@ -420,8 +559,6 @@ export default { beforeUnmount() { // 移除事件监听 window.removeEventListener('resize', this.checkDeviceType); - // 清理资源,避免内存泄漏 - this.cozeWebSDK = null }, computed: { userStore() { @@ -477,172 +614,7 @@ export default { } }, methods: { - // 加载Coze Chat SDK - loadCozeChatSDK() { - // 创建script标签 - const script = document.createElement('script'); - script.src = 'https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/1.2.0-beta.10/libs/cn/index.js'; - script.async = true; - script.onload = () => { - // SDK加载完成后初始化聊天客户端 - console.log('Coze SDK 加载完成'); - this.initCozeChatClient() - .then(() => { - console.log('Coze客户端初始化成功'); - }) - .catch(error => { - console.error('Failed to initialize Coze chat client:', error); - }); - }; - script.onerror = (error) => { - console.error('加载Coze SDK失败:', error); - }; - // 添加到文档中 - document.body.appendChild(script); - }, - - // 初始化Coze聊天客户端 - async initCozeChatClient() { - if (!window.CozeWebSDK) { - console.error('Coze SDK not loaded'); - throw new Error('Coze SDK未加载'); - } - - try { - console.log('Initializing Coze chat client...'); - - // 获取token - const tokenData = await this.fetchCozeToken(); - console.log('Token data received:', tokenData); - - if (!tokenData || !tokenData.access_token) { - console.error('Failed to get valid Coze token'); - throw new Error('无法获取有效的Coze token'); - } - - console.log('Creating Coze chat client with token...'); - - // 检测设备类型 - const isMobile = this.checkDeviceType(); - console.log('设备类型:', isMobile ? '移动设备' : 'PC设备'); - - // 保存Coze SDK配置 - const config = { - config: { - bot_id: '7524958762676305971', - }, - componentProps: { - title: '彩票猪手', - }, - auth: { - type: 'token', - token: tokenData.access_token, - onRefreshToken: async () => { - try { - console.log('Refreshing token...'); - const refreshedData = await this.fetchCozeToken(); - if (refreshedData && refreshedData.access_token) { - console.log('Token refreshed successfully'); - return refreshedData.access_token; - } else { - throw new Error('Failed to refresh token'); - } - } catch (error) { - console.error('Error refreshing token:', error); - return ''; - } - } - }, - ui: { - base: { - icon: 'https://lf26-appstore-sign.oceancloudapi.com/ocean-cloud-tos/FileBizType.BIZ_BOT_ICON/2534800929065520_1751962457338211464.jpeg?lk3s=ca44e09c&x-expires=1752109178&x-signature=b4hRlUI61B9g8wHnao1%2FiRdqGtg%3D', - layout: isMobile ? 'mobile' : 'pc', // 根据设备类型设置布局 - zIndex: 1000, - }, - asstBtn: { - isNeed: false, // 不展示默认悬浮球 - } - } - }; - - // 仅输出部分配置信息,避免暴露token - console.log('Coze client config (partial):', { - botId: config.config.bot_id, - title: config.componentProps.title, - layout: config.ui.base.layout, - authType: config.auth.type - }); - - // 创建聊天客户端实例 - this.cozeWebSDK = new window.CozeWebSDK.WebChatClient(config); - console.log('Coze chat client initialized successfully'); - return this.cozeWebSDK; - - } catch (error) { - console.error('Error initializing Coze chat:', error); - throw error; // 重新抛出错误,让调用者知道初始化失败 - } - }, - - // 从API获取Coze token - async fetchCozeToken() { - try { - const userId = this.userStore.user ? this.userStore.user.id : 'guest'; - const jwtExpireSeconds = 3600; // 1小时 - const tokenDurationSeconds = 86400; // 24小时 - - // 根据环境确定API基础URL - let baseUrl = ''; - if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { - baseUrl = 'http://localhost:8123'; - } else { - // 生产环境或其他环境使用相对路径 - // baseUrl = 'http://47.117.22.239'; - baseUrl = 'https://www.jingcaishuju.com'; - } - - const url = `${baseUrl}/api/jwt/one-step-token?jwtExpireSeconds=${jwtExpireSeconds}&sessionName=${userId}&tokenDurationSeconds=${tokenDurationSeconds}`; - - console.log('Fetching Coze token, URL:', url); - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'include' // 包含cookies - }); - - if (!response.ok) { - console.error(`API error: ${response.status}`, await response.text()); - throw new Error(`API error: ${response.status}`); - } - - const data = await response.json(); - console.log('Token API response:', data); - - // 根据API实际返回结构判断 - if (!data.success || !data.data) { - throw new Error('API返回错误: ' + (data.message || '未知错误')); - } - - // 确保返回的是正确格式的token - if (data.data.access_token) { - console.log('Token received successfully:', data.data.access_token.substring(0, 10) + '...'); - return data.data; - } else if (typeof data.data === 'string') { - // 如果直接返回了token字符串 - console.log('Token string received:', data.data.substring(0, 10) + '...'); - return { access_token: data.data }; - } else { - console.error('Invalid token format:', data.data); - throw new Error('Invalid token format'); - } - } catch (error) { - console.error('Error fetching Coze token:', error); - return null; - } - }, + // 刷新用户信息(统一方法) async refreshUserInfo() { @@ -664,11 +636,8 @@ export default { return // 禁用状态下不再加载统计数据 } - // 获取用户信息成功后,获取用户统计数据 - if (this.userStore.user && this.userStore.user.id) { - await this.fetchUserStats() - await this.fetchDetailedStats() - } + // 获取用户信息成功后,不再获取统计数据 + // 统计数据已移至专门的页面 } catch (error) { console.error('获取用户信息失败:', error) // fetchLoginUser内部已经处理了logout,如果失败会跳转到登录页 @@ -856,46 +825,172 @@ export default { async showCustomerService() { this.showCustomerServiceModal = true - // 等待弹窗打开后再生成二维码 - await this.$nextTick() - this.generateQRCode() }, hideCustomerService() { this.showCustomerServiceModal = false }, - async generateQRCode() { + // 显示编辑用户信息弹窗 + async showEditProfile() { try { - const canvas = this.$refs.qrcodeCanvas - if (canvas) { - await QRCode.toCanvas(canvas, 'https://work.weixin.qq.com/kfid/kfc69665176351bc02f', { - width: 200, - margin: 2, - color: { - dark: '#000000', - light: '#ffffff' - } - }) + // 获取最新的用户信息 + const response = await lotteryApi.getUserInfo() + if (response.code === 0 && response.data) { + const user = response.data + this.editForm = { + id: user.id, + userAccount: user.userAccount, + userRole: user.userRole, + userName: user.userName || '', + userAvatar: user.userAvatar || '', + phone: user.phone || '', + gender: user.gender !== null ? user.gender : 2, + location: user.location || '', + preference: user.preference || '', + channel: user.channel || '', + createTime: user.createTime, + vipType: user.vipType, + vipExpire: user.vipExpire + } + this.showEditProfileModal = true + } else { + ElMessage.error('获取用户信息失败') } } catch (error) { - console.error('生成二维码失败:', error) - this.modalTitle = '错误' - this.modalMessage = '生成二维码失败,请稍后重试' - this.showModal = true + console.error('获取用户信息失败:', error) + ElMessage.error('获取用户信息失败,请稍后重试') } }, + // 隐藏编辑用户信息弹窗 + hideEditProfile() { + this.showEditProfileModal = false + }, + + // 保存用户信息 + async saveUserInfo() { + // 验证手机号 + if (this.editForm.phone && this.editForm.phone.length !== 11) { + ElMessage.error('手机号格式不正确,请输入11位手机号') + return + } + + this.savingUserInfo = true + try { + const updateData = { + id: this.editForm.id, + userName: this.editForm.userName, + userAvatar: this.editForm.userAvatar, + phone: this.editForm.phone, + gender: parseInt(this.editForm.gender), + location: this.editForm.location, + preference: this.editForm.preference, + channel: this.editForm.channel + } + + const response = await lotteryApi.updateUserInfo(updateData) + if (response.code === 0) { + ElMessage.success('保存成功!') + this.hideEditProfile() + // 刷新用户信息 + await this.refreshUserInfo() + } else { + ElMessage.error(response.message || '保存失败') + } + } catch (error) { + console.error('保存用户信息失败:', error) + ElMessage.error('保存失败,请稍后重试') + } finally { + this.savingUserInfo = false + } + }, + + // 触发文件上传 + triggerFileUpload() { + console.log('triggerFileUpload 被调用') + console.log('avatarInput ref:', this.$refs.avatarInput) + if (this.$refs.avatarInput) { + this.$refs.avatarInput.click() + } else { + console.error('avatarInput ref 不存在') + } + }, + + // 处理头像上传 + async handleAvatarChange(event) { + console.log('handleAvatarChange 被调用', event) + const file = event.target.files[0] + console.log('选择的文件:', file) + if (!file) return + + // 验证文件类型 + if (!file.type.startsWith('image/')) { + ElMessage.error('请选择图片文件') + return + } + + // 验证文件大小(限制2MB) + if (file.size > 2 * 1024 * 1024) { + ElMessage.error('图片大小不能超过2MB') + return + } + + this.uploadingAvatar = true + try { + // 显示上传提示 + ElMessage.info('正在上传头像...') + + console.log('开始调用上传接口...') + // 调用上传接口 + const response = await lotteryApi.uploadFile(file) + console.log('上传接口响应:', response) + + if (response.code === 0 && response.data && response.data.fileUrl) { + // 上传成功,设置头像URL + this.editForm.userAvatar = response.data.fileUrl + ElMessage.success('头像上传成功!') + } else { + ElMessage.error(response.message || '头像上传失败') + } + } catch (error) { + console.error('头像上传失败:', error) + ElMessage.error('头像上传失败,请稍后重试') + } finally { + this.uploadingAvatar = false + // 清空文件输入框,允许重新选择同一文件 + event.target.value = '' + } + }, + + // 格式化日期 + formatDate(dateString) { + if (!dateString) return '' + const date = new Date(dateString) + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + }, + + async generateQRCode() { + // 已移除:使用图片替代生成二维码 + }, + + showQRCodeError() { + // 已移除:使用图片替代生成二维码 + }, + downloadQRCode() { try { - const canvas = this.$refs.qrcodeCanvas - if (canvas) { - // 创建一个链接用于下载 - const link = document.createElement('a') - link.download = '客服二维码.png' - link.href = canvas.toDataURL('image/png') - link.click() - } + // 直接下载图片 + const link = document.createElement('a') + link.download = '客服二维码.png' + link.href = '/assets/home/erweima.png' + link.click() } catch (error) { console.error('下载二维码失败:', error) this.modalTitle = '错误' @@ -917,81 +1012,40 @@ export default { return numRate.toFixed(2) }, - showAIAssistant() { - if (this.cozeWebSDK) { - console.log('显示聊天窗口'); - try { - this.cozeWebSDK.showChatBot(); - } catch (error) { - console.error('显示聊天窗口失败:', error); - // 重新初始化并显示 - this.reinitializeCozeChat(); - } - } else { - console.log('Coze SDK未初始化,正在重新加载'); - this.reinitializeCozeChat(); - } - }, - - // 重新初始化Coze聊天 - async reinitializeCozeChat() { - try { - console.log('重新加载Coze SDK'); - - // 先移除已有的script标签 - const existingScripts = document.querySelectorAll('script[src*="coze"]'); - existingScripts.forEach(script => { - try { - document.body.removeChild(script); - } catch (e) { - console.warn('移除script标签失败:', e); - } - }); - - // 清空实例 - this.cozeWebSDK = null; - - // 重新加载SDK - const script = document.createElement('script'); - script.src = 'https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/1.2.0-beta.10/libs/cn/index.js'; - script.async = true; - - // 使用Promise等待脚本加载完成 - await new Promise((resolve, reject) => { - script.onload = () => { - console.log('Coze SDK 重新加载完成'); - resolve(); - }; - script.onerror = (err) => { - console.error('加载Coze SDK失败:', err); - reject(new Error('加载SDK失败')); - }; - document.body.appendChild(script); - }); - - console.log('Coze SDK 加载完成,初始化客户端'); - await this.initCozeChatClient(); - - // 检查是否成功初始化 - if (!this.cozeWebSDK) { - throw new Error('初始化失败,无法获取有效的聊天实例'); - } - - // 等待一段时间确保初始化完成 - await new Promise(resolve => setTimeout(resolve, 1000)); - - console.log('成功重新初始化,显示聊天窗口'); - this.cozeWebSDK.showChatBot(); - } catch (error) { - console.error('重新初始化失败:', error); - } - }, - // 检查是否为移动设备(基于屏幕宽度) checkDeviceType() { const isMobile = window.innerWidth <= 768; console.log('设备类型检测:', isMobile ? '移动设备' : 'PC设备', '屏幕宽度:', window.innerWidth); return isMobile; + }, + + // 打开AI助手 + openAIAssistant() { + // 通过发送自定义事件来触发App.vue中的showAIAssistant方法 + const event = new CustomEvent('showAIAssistant'); + window.dispatchEvent(event); + }, + + // 显示统计信息弹窗 - 现在直接跳转到统计页面 + showStatsModal() { + this.$router.push('/usage-stats') + }, + + // 获取会员类型标签 + getVipTypeLabel() { + const vipType = this.userStore.user?.vipType + if (vipType === '体验会员') return '体验会员' + if (vipType === '月度会员') return '月度会员' + if (vipType === '年度会员') return '年度会员' + return '体验会员' + }, + + // 获取会员类型样式类 + getVipBadgeClass() { + const vipType = this.userStore.user?.vipType + if (vipType === '年度会员') return 'vip-annual' + if (vipType === '月度会员') return 'vip-monthly' + return 'vip-trial' } } } @@ -1001,10 +1055,8 @@ export default { /* 用户中心容器 */ .profile-container { min-height: calc(100vh - 85px); - background: #f0f2f5; - padding: 0; - max-width: 800px; - margin: 0 auto; + background: #f5f5f5; + padding: 0 0 0px 0; width: 100%; } @@ -1030,16 +1082,6 @@ export default { overflow: hidden; } -.login-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(135deg, #e53e3e, #ff6b6b); -} - .login-icon { margin-bottom: 20px; } @@ -1110,92 +1152,35 @@ export default { border-color: #ccc; } -/* 会员兑换弹窗 */ -.member-exchange-modal { - max-width: 400px; - width: 90%; -} - -.member-exchange-modal h3 { - color: #333; - margin-bottom: 15px; - font-size: 18px; - font-weight: 600; - display: flex; - align-items: center; - justify-content: center; -} - -.member-exchange-modal h3::before { - content: '💎'; - margin-right: 8px; - font-size: 20px; -} - -.member-exchange-modal p { - margin-bottom: 20px; - color: #666; - line-height: 1.5; - text-align: center; -} - -.exchange-input-container { - margin-bottom: 25px; -} - -.exchange-input { - width: 100%; - padding: 12px 15px; - border: 2px solid #e0e0e0; - border-radius: 8px; - font-size: 16px; - outline: none; - transition: all 0.3s; - text-align: center; - letter-spacing: 1px; -} - -.exchange-input:focus { - border-color: #e53e3e; - box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.1); -} - -.exchange-input::placeholder { - color: #999; - text-align: center; -} - -/* 用户信息卡片 */ -.user-card { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +/* 用户信息红色区域 */ +.user-header { + background: linear-gradient(to top, #e53e3e, #ff6b6b); + padding: 25px 20px 80px 20px; color: white; - padding: 30px; - border-radius: 16px; - margin: 20px 30px; position: relative; - box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); - min-height: 150px; } .user-content { display: flex; align-items: center; - width: 100%; + gap: 15px; + margin-bottom: 25px; } .user-avatar { - margin-right: 20px; + flex-shrink: 0; } .avatar-circle { - width: 60px; - height: 60px; - background: rgba(255, 255, 255, 0.2); + width: 70px; + height: 70px; + background: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; - backdrop-filter: blur(10px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + border: 3px solid rgba(255, 255, 255, 0.3); } .avatar-img { @@ -1206,8 +1191,13 @@ export default { } .avatar-icon { - font-size: 24px; + font-size: 30px; font-weight: bold; + color: #e53e3e; + background: linear-gradient(135deg, #e53e3e, #ff6b6b); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } .user-info { @@ -1215,149 +1205,206 @@ export default { } .user-name { - font-size: 22px; + font-size: 20px; font-weight: bold; - margin-bottom: 15px; + margin-bottom: 5px; + color: white; } .user-level { - display: flex; - flex-direction: column; - gap: 5px; + margin-bottom: 8px; } .level-badge { - background: rgba(255, 255, 255, 0.2); + display: inline-block; padding: 4px 12px; - border-radius: 20px; - font-size: 14px; - align-self: flex-start; - backdrop-filter: blur(10px); -} - -.level-badge.premium { - background: rgba(255, 215, 0, 0.3); - color: #ffd700; + border-radius: 15px; + font-size: 12px; + font-weight: 600; + margin-bottom: 5px; } /* 会员样式 */ .level-badge.vip-member { - background: linear-gradient(135deg, #ffd700, #ffed4e); + background: #ffd700; color: #b45309; - font-weight: 700; - font-size: 13px; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - border: 1px solid rgba(255, 215, 0, 0.3); - box-shadow: 0 2px 8px rgba(255, 215, 0, 0.3); + box-shadow: 0 2px 6px rgba(255, 215, 0, 0.3); +} + +/* 年度会员样式 */ +.level-badge.vip-annual { + background: #ffd700; + color: #b45309; + box-shadow: 0 2px 6px rgba(255, 215, 0, 0.3); +} + +/* 月度会员样式 */ +.level-badge.vip-monthly { + background: #87ceeb; + color: #0066cc; + box-shadow: 0 2px 6px rgba(135, 206, 235, 0.3); } /* 体验版用户样式 */ -.level-badge.trial-user { +.level-badge.trial-user, +.level-badge.vip-trial { background: rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.9); - font-weight: 400; - font-size: 12px; } .expire-date { - font-size: 14px; - opacity: 0.8; - margin-top: 12px; + font-size: 13px; + color: rgba(255, 255, 255, 0.9); + margin-top: 3px; } -.user-actions { +/* 注销按钮 */ +.logout-btn { position: absolute; - top: 15px; - right: 15px; - display: flex; - gap: 10px; - align-items: center; -} - -.exchange-btn { - background: rgba(255, 255, 255, 0.2); + top: 25px; + right: 20px; + background: rgba(255, 255, 255, 0.15); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 20px; - display: flex; - align-items: center; - justify-content: center; + padding: 8px 16px; + font-size: 14px; cursor: pointer; transition: all 0.3s; - backdrop-filter: blur(10px); - padding: 8px 12px; + white-space: nowrap; + font-weight: 600; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.logout-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.service-btn { + position: absolute; + top: 42%; + right: 0px; + transform: translateY(-50%); + background: linear-gradient(135deg, #ffd700, #ffb347); + color: #8B4513; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 25px 0 0 25px; + padding: 10px 20px 10px 18px; + font-size: 15px; + cursor: pointer; + transition: all 0.3s; + white-space: nowrap; + font-weight: 600; + box-shadow: 0 3px 12px rgba(255, 215, 0, 0.4); + display: flex; + align-items: center; gap: 6px; } -.exchange-btn:hover { - background: rgba(255, 255, 255, 0.3); - transform: scale(1.1); +.service-btn:hover { + background: linear-gradient(135deg, #ffed4a, #ffc947); + transform: translateY(-50%) translateY(-2px); + box-shadow: 0 6px 16px rgba(255, 215, 0, 0.5); } -.exchange-icon { - font-size: 16px; +.service-icon { + width: 20px; + height: 20px; + filter: brightness(0.7); } -.logout-wrapper { - background: rgba(255, 255, 255, 0.2); - border-radius: 20px; +/* 快捷功能区 */ +.quick-actions { + position: absolute; + bottom: -30px; + left: 20px; + right: 20px; + background: white; + border-radius: 15px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + display: flex; + z-index: 10; + overflow: hidden; +} + +.action-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 18px 10px; + cursor: pointer; + position: relative; + transition: background-color 0.3s ease; +} + +.action-item:hover { + background-color: #f8f9fa; +} + +.action-item:not(:last-child)::after { + content: ''; + position: absolute; + right: 0; + top: 15px; + bottom: 15px; + width: 1px; + background-color: #e9ecef; +} + +.action-icon { + margin-bottom: 8px; + width: 50px; + height: 50px; + border-radius: 50%; + background: rgba(255, 225, 221, 1); display: flex; align-items: center; justify-content: center; - cursor: pointer; - transition: all 0.3s; - backdrop-filter: blur(10px); - padding: 8px; - height: 35px; + box-shadow: 0 2px 8px rgba(255, 225, 221, 0.4); } -.logout-wrapper:hover { - background: rgba(255, 255, 255, 0.3); - transform: scale(1.1); +.action-icon img { + width: 28px; + height: 28px; } -.logout-icon-img { - width: 20px; - height: 20px; - object-fit: contain; -} - -.exchange-icon { - animation: sparkle 2s infinite ease-in-out; -} - -.exchange-text { - color: white; - font-size: 12px; - font-weight: 500; - white-space: nowrap; -} - -@keyframes sparkle { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.1); } -} - -/* 会员权益 */ -.benefits-section { - background: white; - margin: 15px 30px; - border-radius: 12px; - padding: 12px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); -} - -.benefits-section h3 { - margin-bottom: 8px; +.action-text { + font-size: 14px; color: #333; - font-size: 18px; + font-weight: 500; +} + +/* 会员权益展示 */ +.vip-benefits { + background: white; + margin: 55px 20px 0 20px; + border-radius: 15px; + padding: 20px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); +} + +.benefits-header { + text-align: left; + margin-bottom: 8px; + padding-left: 10px; +} + +.benefits-header h3 { + color: #333; + font-size: 16px; font-weight: 600; - padding: 10px 20px; + margin: 0; + letter-spacing: -0.3px; } .benefits-grid { display: grid; grid-template-columns: repeat(3, 1fr); - gap: 12px; - margin-bottom: 12px; + gap: 20px; } .benefit-item { @@ -1365,321 +1412,75 @@ export default { flex-direction: column; align-items: center; text-align: center; - padding: 6px; - border-radius: 6px; - transition: transform 0.3s; + padding: 15px 10px; + border-radius: 12px; + background: transparent; + transition: all 0.3s ease; + cursor: pointer; } .benefit-item:hover { + background: rgba(0, 0, 0, 0.03); + transform: translateY(-3px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); +} + +.benefit-item:active { transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .benefit-icon { - width: 36px; - height: 36px; - border-radius: 50%; display: flex; align-items: center; justify-content: center; - color: white; - font-size: 16px; - margin-bottom: 6px; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); -} - -.benefit-label { - font-size: 15px; - color: #666; - font-weight: 500; - line-height: 1.1; -} - -.upgrade-button { - text-align: center; - margin-top: 8px; -} - -.btn-upgrade { - background: linear-gradient(135deg, #ffd700, #ffb347); - color: #333; - border: none; - padding: 8px 20px; - border-radius: 18px; - font-weight: bold; - cursor: pointer; - transition: all 0.3s; - font-size: 14px; -} - -.btn-upgrade:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(255, 215, 0, 0.3); -} - -/* 功能菜单 */ -.menu-section { - background: white; - margin: 20px 30px; - border-radius: 12px; - overflow: hidden; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); -} - -.menu-item { - display: flex; - align-items: center; - padding: 20px 25px; - border-bottom: 1px solid #f0f0f0; - cursor: pointer; - transition: background-color 0.3s; -} - -.menu-item:last-child { - border-bottom: none; -} - -.menu-item:hover { - background-color: #f8f9fa; -} - -.menu-icon { - margin-right: 18px; - width: 26px; - height: 26px; - display: flex; - align-items: center; - justify-content: center; -} - -.menu-icon-img { - width: 24px; - height: 24px; - object-fit: contain; -} - -.menu-label { - font-size: 16px; - color: #333; - flex: 1; -} - -.ai-label { - color: #1296db; - font-weight: bold; -} - -.menu-arrow { - color: #ccc; - font-size: 18px; -} - -/* 统计信息 */ -.stats-section { - background: white; - margin: 20px 30px; - border-radius: 12px; - padding: 25px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); - transition: all 0.3s ease; - border: 1px solid #f0f0f0; -} - -.stats-section h3 { - margin-bottom: 25px; - color: #333; - font-size: 20px; - font-weight: 700; - position: relative; - padding-left: 10px; -} - -/* 加载状态 */ -.stats-loading { - display: flex; - justify-content: center; - align-items: center; - padding: 40px; - color: #666; -} - -.loading-spinner { - font-size: 16px; -} - -/* 统计卡片网格 */ -.stats-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 10px; - margin-top: 15px; -} - -.detailed-stats-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 15px; - margin-top: 15px; -} - -/* 统计卡片样式 */ -.stat-item { - background: #f8f9fa; - border-radius: 10px; - padding: 20px 15px; - text-align: center; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); - transition: all 0.3s ease; - border: 1px solid rgba(0, 0, 0, 0.05); -} - -.stat-item:hover { - transform: translateY(-3px); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); - background: #fff; - border-color: rgba(229, 62, 62, 0.2); -} - -.stat-number { - font-size: 28px; - font-weight: 700; margin-bottom: 8px; - background: linear-gradient(135deg, #e53e3e, #ff6b6b); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - display: inline-block; } -.stat-label { - font-size: 14px; - color: #666; +.benefit-text { + font-size: 13px; font-weight: 500; - line-height: 1.4; -} - -/* 奖金统计样式 */ -.total-prize-summary { - text-align: center; - font-size: 20px; - font-weight: bold; - margin-bottom: 25px; - padding: 20px; - background: linear-gradient(135deg, #fffaf0, #fff5f5); - border-radius: 10px; - border: 1px solid rgba(229, 62, 62, 0.1); - box-shadow: 0 3px 10px rgba(0, 0, 0, 0.03); -} - -.total-prize-label { color: #333; - margin-right: 10px; + line-height: 1.2; } -.prize-amount { - color: #e53e3e; - font-size: 24px; - background: linear-gradient(135deg, #e53e3e, #ff6b6b); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} - -/* 表格样式 */ -:deep(.el-table) { - border-radius: 8px; - overflow: hidden; - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); -} - -:deep(.el-table th) { - background-color: #f8f9fa; - font-weight: 600; - color: #333; -} - -:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) { - background-color: #fafafa; -} - -:deep(.el-table__row:hover td) { - background-color: #fff9f9 !important; -} - -/* 响应式调整 */ -@media (max-width: 768px) { - .stats-grid { - grid-template-columns: repeat(2, 1fr); - } - - .detailed-stats-grid { - grid-template-columns: repeat(2, 1fr); - } - - .stat-number { - font-size: 24px; - } - - .stat-label { - font-size: 13px; - } -} - -@media (max-width: 480px) { - .stats-grid { - grid-template-columns: repeat(2, 1fr); - gap: 8px; - } - - .detailed-stats-grid { - grid-template-columns: repeat(2, 1fr); - gap: 8px; - } - - .stat-item { - padding: 15px 10px; - } - - .stat-number { - font-size: 20px; - margin-bottom: 5px; - } - - .stat-label { - font-size: 12px; - } -} /* 会员续费提醒 */ .vip-renewal-notice { background: linear-gradient(135deg, #fff3cd, #ffeaa7); - margin: 15px 30px; + margin: 15px 20px 0 20px; border-radius: 12px; - padding: 20px; + padding: 15px; border: 1px solid #ffc107; - box-shadow: 0 4px 15px rgba(255, 193, 7, 0.2); - animation: renewalPulse 2s ease-in-out infinite; } .renewal-content { display: flex; align-items: center; - gap: 15px; + gap: 10px; } -.renewal-icon { - font-size: 32px; - flex-shrink: 0; -} + .renewal-icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 8px; + } .renewal-text h4 { color: #856404; - font-size: 16px; + font-size: 14px; font-weight: 700; - margin-bottom: 5px; + margin: 0 0 5px 0; } .renewal-text p { color: #664d03; - font-size: 14px; - line-height: 1.5; + font-size: 12px; + line-height: 1.4; margin: 0; } @@ -1688,25 +1489,405 @@ export default { font-weight: 700; cursor: pointer; text-decoration: underline; +} + +/* 功能图标 */ +.function-icon { + width: 24px; + height: 24px; + margin: 0 auto 8px auto; + display: flex; + align-items: center; + justify-content: center; +} + +.function-icon img { + width: 24px; + height: 24px; + object-fit: contain; +} + +.function-label { + font-size: 12px; + color: #333; + font-weight: 500; + line-height: 1.2; +} + +/* 功能列表 */ +.function-list { + background: white; + margin: 15px 20px 0 20px; + padding: 0; + border-radius: 15px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); +} + +.function-list-item { + display: flex; + align-items: center; + padding: 20px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background-color 0.3s; + background: white; +} + +.function-list-item:hover { + background-color: #f8f9fa; +} + +.function-list-item:first-child { + border-radius: 15px 15px 0 0; +} + +.function-list-item:last-child { + border-bottom: none; + border-radius: 0 0 15px 15px; +} + +.function-list-item:first-child:last-child { + border-radius: 15px; +} + +.function-list-item .function-icon { + margin: 0 15px 0 0; + width: 24px; + height: 24px; +} + +.function-list-item .function-label { + flex: 1; + font-size: 16px; + color: #333; + margin: 0; +} + +.function-arrow { + color: #ccc; + font-size: 18px; + font-weight: bold; +} + + + +/* 现代化弹窗样式 */ +.modern-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(8px); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + animation: fadeIn 0.3s ease-out; +} + +.modern-modal-content { + background: white; + border-radius: 20px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 360px; + width: 90%; + max-height: 90vh; + overflow: hidden; + position: relative; + animation: slideUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(50px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* 关闭按钮 */ +.modal-close-btn { + position: absolute; + top: 12px; + right: 12px; + width: 28px; + height: 28px; + border: none; + background: rgba(0, 0, 0, 0.05); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; transition: all 0.3s ease; + z-index: 10; } -.renewal-link:hover { - color: #d32f2f; - text-shadow: 0 1px 2px rgba(229, 62, 62, 0.3); +.modal-close-btn:hover { + background: rgba(0, 0, 0, 0.1); + transform: scale(1.1); } -@keyframes renewalPulse { - 0%, 100% { - transform: scale(1); - box-shadow: 0 4px 15px rgba(255, 193, 7, 0.2); +.modal-close-btn span { + font-size: 16px; + color: #666; + line-height: 1; +} + +/* 弹窗头部 */ +.modal-header { + padding: 24px 24px 20px 24px; + text-align: center; + background: linear-gradient(135deg, #f8f9ff, #fff); +} + +.modal-icon { + width: 52px; + height: 52px; + margin: 0 auto 12px auto; + background: linear-gradient(135deg, #e53e3e, #ff6b6b); + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 6px 20px rgba(229, 62, 62, 0.3); +} + +.modal-icon img { + width: 26px; + height: 26px; + filter: brightness(0) invert(1); +} + +.modal-title { + font-size: 20px; + font-weight: 700; + color: #1a1a1a; + margin: 0 0 6px 0; + letter-spacing: -0.5px; +} + +.modal-subtitle { + font-size: 13px; + color: #666; + margin: 0; + line-height: 1.5; +} + +/* 弹窗主体 */ +.modal-body { + padding: 0 24px 20px 24px; +} + +.input-group { + position: relative; +} + + +.input-wrapper { + position: relative; +} + +.modern-input { + width: 100%; + padding: 12px 16px; + border: 2px solid #e8e8e8; + border-radius: 12px; + font-size: 14px; + outline: none; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + background: #fafafa; + color: #333; + font-weight: 500; + letter-spacing: 0.5px; + box-sizing: border-box; +} + +.modern-input::placeholder { + color: #999; + font-weight: 400; +} + +.modern-input:focus { + border-color: #ddd; + background: white; + outline: none; +} + +.input-focus-border { + display: none; +} + +/* 会员服务协议勾选框 */ +.agreement-checkbox { + margin-top: 15px; +} + +.checkbox-label { + display: flex; + align-items: center; + cursor: pointer; + font-size: 13px; + color: #666; +} + +.checkbox-label input[type="checkbox"] { + width: 16px; + height: 16px; + margin-right: 8px; + cursor: pointer; + accent-color: #e53e3e; +} + +.checkbox-text { + line-height: 1.4; +} + +.agreement-link { + color: #e53e3e; + text-decoration: none; +} + +.agreement-link:hover { + text-decoration: underline; +} + +/* 弹窗底部 */ +.modal-footer { + padding: 16px 24px 20px 24px; + display: flex; + gap: 8px; +} + +.modern-btn { + flex: 1; + padding: 10px 16px; + border: none; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.modern-btn:before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + background: rgba(255, 255, 255, 0.2); + border-radius: 50%; + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.modern-btn:active:before { + width: 300px; + height: 300px; +} + +.cancel-btn { + background: #f5f5f5; + color: #666; + border: 2px solid transparent; +} + +.cancel-btn:hover { + background: #ebebeb; + color: #333; + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); +} + +.primary-btn { + background: linear-gradient(135deg, #e53e3e, #ff6b6b); + color: white; + border: 2px solid transparent; + box-shadow: 0 4px 16px rgba(229, 62, 62, 0.3); +} + +.primary-btn:hover { + transform: translateY(-2px); + box-shadow: 0 12px 32px rgba(229, 62, 62, 0.4); +} + +.primary-btn:active { + transform: translateY(0); +} + +/* 响应式设计 */ +@media (max-width: 480px) { + .modern-modal-content { + margin: 20px; + width: calc(100% - 40px); + border-radius: 18px; } - 50% { - transform: scale(1.02); - box-shadow: 0 6px 20px rgba(255, 193, 7, 0.3); + + .modal-header, + .modal-body, + .modal-footer { + padding-left: 20px; + padding-right: 20px; + } + + .modal-header { + padding-top: 20px; + padding-bottom: 16px; + } + + .modal-icon { + width: 48px; + height: 48px; + border-radius: 14px; + } + + .modal-icon img { + width: 24px; + height: 24px; + } + + .modal-title { + font-size: 18px; + } + + .modal-subtitle { + font-size: 12px; + } + + .modern-input { + padding: 10px 14px; + font-size: 14px; + border-radius: 10px; + } + + .modern-btn { + padding: 8px 14px; + font-size: 13px; + border-radius: 10px; + } + + .modal-footer { + padding: 14px 20px 18px 20px; + gap: 6px; } } + + /* 模态框 */ .modal-overlay { position: fixed; @@ -1791,526 +1972,110 @@ export default { box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3); } -.btn-secondary { - background: #f8f9fa; - color: #666; - border: 1px solid #e0e0e0; -} - -.btn-secondary:hover { - background: #e9ecef; - border-color: #ccc; -} - -/* 响应式适配 */ -@media (max-width: 768px) { - .profile-container { - max-width: 100%; - margin: 0; - min-height: calc(100vh - 80px); - } - - .user-card, - .benefits-section, - .menu-section, - .stats-section, - .vip-renewal-notice { - margin: 12px 18px; - padding: 18px; - } - - .user-card { - min-height: 150px; - } - - .user-actions { - gap: 15px; - } - - .exchange-btn { - padding: 6px 10px; - gap: 4px; - } - - .exchange-icon { - font-size: 14px; - } - - .logout-wrapper { - padding: 6px; - height: 32px; - } - - .logout-icon-img { - width: 18px; - height: 18px; - } - - .exchange-text { - font-size: 11px; - } - - .benefits-grid { - grid-template-columns: repeat(2, 1fr); - gap: 18px; - } - - .stats-grid { - grid-template-columns: repeat(2, 1fr); - gap: 12px; - } - - .actions-grid { - grid-template-columns: repeat(2, 1fr); - gap: 12px; - } - - /* 中等屏幕功能菜单优化 */ - .menu-item { - padding: 18px 22px; - } - - .menu-icon { - font-size: 21px; - margin-right: 16px; - } - - .menu-label { - font-size: 14px; - } - - /* 中等屏幕统计区块优化 */ - .stats-section h3, - .vip-renewal-notice h4 { - margin-bottom: 18px; - font-size: 17px; - } - - .stat-item { - padding: 14px 10px; - } - - .stat-number { - font-size: 26px; - } - - .action-item { - padding: 20px 15px; - } - - .action-icon { - font-size: 26px; - margin-bottom: 8px; - } - - .action-label { - font-size: 13px; - } - - .login-actions { - flex-direction: column; - } - - .login-card { - padding: 30px 20px; - margin: 0 20px; - } -} - -@media (max-width: 480px) { - .benefits-grid { - grid-template-columns: repeat(2, 1fr); - } - - .actions-grid { - grid-template-columns: repeat(2, 1fr); - gap: 8px; - } - - .action-item { - padding: 12px 8px; - } - - .action-icon { - font-size: 22px; - margin-bottom: 6px; - } - - .action-label { - font-size: 11px; - line-height: 1.2; - } - - .user-card, - .benefits-section, - .menu-section, - .stats-section, - .vip-renewal-notice { - margin: 8px 12px; - padding: 12px; - } - - /* 功能菜单紧凑化 */ - .menu-item { - padding: 15px 18px; - } - - .menu-icon { - font-size: 20px; - margin-right: 15px; - width: 24px; - } - - .menu-label { - font-size: 14px; - } - - .menu-arrow { - font-size: 16px; - } - - /* 统计区块紧凑化 */ - .stats-section { - padding: 15px; - } - - .stats-section h3 { - margin-bottom: 15px; - font-size: 16px; - } - - .stats-grid { - gap: 12px; - } - - .stat-item { - padding: 12px 8px; - } - - .stat-number { - font-size: 24px; - margin-bottom: 4px; - } - - .stat-label { - font-size: 12px; - } - - /* 快捷入口紧凑化 */ - .vip-renewal-notice { - padding: 15px; - margin-bottom: 15px; - } - - .vip-renewal-notice h4 { - margin-bottom: 10px; - font-size: 15px; - } - - .benefits-section { - padding: 12px; - } - - .benefits-section h3 { - margin-bottom: 10px; - font-size: 15px; - } - - .benefits-grid { - gap: 10px; - margin-bottom: 12px; - } - - .benefit-item { - padding: 6px; - } - - .benefit-icon { - width: 30px; - height: 30px; - font-size: 14px; - margin-bottom: 4px; - } - - .benefit-label { - font-size: 15px; - line-height: 1.2; - } - - .btn-upgrade { - padding: 8px 16px; - font-size: 13px; - border-radius: 16px; - } - - .upgrade-button { - margin-top: 10px; - } - - .renewal-content { - gap: 10px; - } - - .renewal-icon { - font-size: 28px; - } - - .renewal-text h4 { - font-size: 15px; - } - - .renewal-text p { - font-size: 13px; - } - - /* 悬浮客服移动端适配 */ - .floating-customer-service { - bottom: 110px; - right: 15px; - } - - .service-avatar { - width: 50px; - height: 50px; - } - - .service-avatar .avatar-img { - width: 36px; - height: 36px; - } - - .online-indicator { - width: 10px; - height: 10px; - } - - .service-bubble { - padding: 8px 12px; - } - - .bubble-text { - font-size: 12px; - } -} - - - -/* 悬浮在线客服 */ -.floating-customer-service { - position: absolute; - bottom: 15px; - right: 15px; - z-index: 999; - cursor: pointer; - animation: floatBounce 3s ease-in-out infinite; -} - -.floating-service-content { - display: flex; - align-items: center; - gap: 12px; - flex-direction: column-reverse; -} - -.service-avatar { - position: relative; - width: 48px; - height: 48px; - border-radius: 50%; - background: white; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s ease; -} - -.service-avatar:hover { - transform: scale(1.1); - box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2); -} - -.service-avatar .avatar-img { - width: 36px; - height: 36px; - border-radius: 50%; - object-fit: contain; - padding: 2px; -} - -.online-indicator { - position: absolute; - bottom: 4px; - right: 4px; - width: 12px; - height: 12px; - background: #00d976; - border-radius: 50%; - border: 2px solid white; - animation: pulse 2s infinite; -} - -.service-bubble { - position: relative; - background: rgba(255, 255, 255, 0.95); - padding: 8px 12px; - border-radius: 16px; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); - opacity: 0; - transform: translateY(10px); - transition: all 0.3s ease; - white-space: nowrap; - backdrop-filter: blur(10px); -} - -.floating-customer-service:hover .service-bubble { - opacity: 1; - transform: translateY(0); -} - -.bubble-text { - color: #333; - font-size: 14px; - font-weight: 600; -} - -.bubble-arrow { - position: absolute; - bottom: -6px; - left: 50%; - transform: translateX(-50%); - width: 0; - height: 0; - border-top: 6px solid rgba(255, 255, 255, 0.95); - border-left: 6px solid transparent; - border-right: 6px solid transparent; -} - -@keyframes floatBounce { - 0%, 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-8px); - } -} - -@keyframes pulse { - 0% { - transform: scale(1); - opacity: 1; - } - 50% { - transform: scale(1.2); - opacity: 0.7; - } - 100% { - transform: scale(1); - opacity: 1; - } -} - -/* 客服二维码弹窗 */ +/* 客服弹窗现代化样式 */ .customer-service-modal { - max-width: 420px; + max-width: 380px; width: 90%; - border-radius: 16px; - overflow: hidden; - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); } -.customer-service-modal h3 { - color: #333; - margin-bottom: 15px; - font-size: 20px; - font-weight: 700; +/* 客服图标特殊样式 */ +.service-icon-bg { + background: linear-gradient(135deg, #25d366, #128c7e) !important; + box-shadow: 0 6px 20px rgba(37, 211, 102, 0.3) !important; +} + +.service-icon-bg img { + filter: brightness(0) invert(1) !important; +} + +/* 二维码包装器 */ +.qrcode-wrapper { display: flex; - align-items: center; justify-content: center; - padding: 10px 0; - background: linear-gradient(135deg, #f8f9fa, #e9ecef); - color: #333; - margin: -30px -30px 20px -30px; - border-bottom: 2px solid #e0e0e0; -} - -.customer-service-modal h3::before { - content: '💬'; - margin-right: 8px; - font-size: 22px; -} - -.customer-service-modal p { - margin-bottom: 20px; - color: #666; - line-height: 1.5; - text-align: center; - font-size: 15px; } .qrcode-container { - margin-bottom: 25px; - display: flex; - justify-content: center; - align-items: center; padding: 20px; - background: #f8f9fa; - border-radius: 12px; - border: 2px dashed #e0e0e0; -} - -.qrcode-canvas { - max-width: 200px; - max-height: 200px; - border-radius: 8px; -} - -.service-info { - text-align: center; - padding: 15px; - background: #f8f9fa; - border-radius: 10px; - margin-bottom: 20px; - border-left: 4px solid #666; -} - -.service-tip { - color: #666; - margin-bottom: 8px; - font-size: 14px; - font-weight: 500; -} - -.service-time { - color: #666; - font-size: 13px; - font-weight: 600; + background: linear-gradient(135deg, #f8fffe, #e8f8f5); + border-radius: 20px; + border: 2px solid #e0f2f1; + box-shadow: 0 8px 24px rgba(37, 211, 102, 0.1); + position: relative; display: flex; + justify-content: center; align-items: center; - justify-content: center; - gap: 5px; } -.service-time::before { - content: '🕐'; - font-size: 14px; +.qrcode-container::before { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + background: linear-gradient(45deg, #25d366, #128c7e, #25d366); + border-radius: 20px; + z-index: -1; + opacity: 0.1; } -.modal-actions { - display: flex; - gap: 15px; - justify-content: center; +.qrcode-canvas, +.qrcode-image { + width: 180px; + height: 180px; + max-width: 180px; + max-height: 180px; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + background: white; + padding: 8px; + display: block; + border: 2px solid #e0f2f1; + object-fit: contain; } -.btn-secondary { - background: #f8f9fa; + +/* 按钮特殊样式覆盖 */ +.customer-service-modal .cancel-btn { + background: linear-gradient(135deg, #f5f5f5, #e0e0e0); color: #666; - border: 1px solid #e0e0e0; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } -.btn-secondary:hover { - background: #e9ecef; - border-color: #ccc; +.customer-service-modal .cancel-btn:hover { + background: linear-gradient(135deg, #eeeeee, #d5d5d5); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); +} + +.customer-service-modal .primary-btn { + background: linear-gradient(135deg, #25d366, #128c7e); + color: white; + box-shadow: 0 4px 16px rgba(37, 211, 102, 0.4); +} + +.customer-service-modal .primary-btn:hover { + background: linear-gradient(135deg, #1fbd5a, #0f7a6b); + box-shadow: 0 8px 32px rgba(37, 211, 102, 0.5); +} + +/* 响应式设计 */ +@media (max-width: 480px) { + .customer-service-modal { + max-width: 340px; + } + + .qrcode-canvas, + .qrcode-image { + max-width: 160px; + max-height: 160px; + padding: 6px; + } + + .qrcode-container { + padding: 16px; + border-radius: 16px; + } + } /* 用户信息加载状态 */ @@ -2324,7 +2089,7 @@ export default { .user-info-loading .loading-spinner { color: #666; - font-size: 16px; + font-size: 16px; display: flex; align-items: center; gap: 10px; @@ -2345,11 +2110,771 @@ export default { 100% { transform: rotate(360deg); } } -/* Coze AI 聊天助手样式 */ -#coze-chat-container { - position: fixed; - bottom: 20px; - right: 20px; - z-index: 1000; +/* 响应式设计 */ +@media (max-width: 768px) { + .profile-container { + min-height: calc(100vh - 80px); + padding-bottom: 0px; + } + + .user-header { + padding: 20px 15px 70px 15px; + } + + .vip-benefits { + margin: 50px 15px 0 15px; + padding: 18px; + } + + .benefits-header h3 { + font-size: 15px; + } + + .benefits-grid { + gap: 10px; + } + + .benefit-item { + padding: 12px 8px; + } + + .benefit-icon { + margin-bottom: 6px; + } + + .benefit-icon .el-icon { + font-size: 32px !important; + } + + .benefit-text { + font-size: 12px; + } + + .vip-renewal-notice { + margin: 15px 15px 0 15px; + } + + .quick-actions { + bottom: -30px; + left: 15px; + right: 15px; + border-radius: 14px; + } + + .action-item { + padding: 16px 8px; + } + + .action-icon { + width: 46px; + height: 46px; + } + + .action-icon img { + width: 26px; + height: 26px; + } + + .action-text { + font-size: 13px; + } + + .function-list { + margin: 15px 15px 0 15px; + border-radius: 14px; + } + + .function-list-item:first-child { + border-radius: 14px 14px 0 0; + } + + .function-list-item:last-child { + border-radius: 0 0 14px 14px; + } + + .function-list-item:first-child:last-child { + border-radius: 14px; + } + + .user-content { + gap: 12px; + margin-bottom: 20px; + } + + .avatar-circle { + width: 60px; + height: 60px; + border-width: 2px; + } + + .avatar-icon { + font-size: 24px; + } + + .user-name { + font-size: 18px; + } + + .logout-btn { + top: 20px; + right: 15px; + padding: 6px 12px; + font-size: 13px; + } + + .service-btn { + top: 42%; + right: 0px; + transform: translateY(-50%); + padding: 8px 16px 8px 14px; + font-size: 14px; + gap: 5px; + border-radius: 22px 0 0 22px; + } + + .service-btn:hover { + transform: translateY(-50%) translateY(-2px); + } + + .service-icon { + width: 18px; + height: 18px; + } + + .function-list-item { + padding: 18px; + } + + .function-list-item .function-label { + font-size: 15px; + } + + .login-actions { + flex-direction: column; + } + + .login-card { + padding: 30px 20px; + margin: 0 20px; + } +} + +@media (max-width: 480px) { + .profile-container { + padding-bottom: 0px; + } + + .user-header { + padding: 18px 12px 65px 12px; + } + + .vip-benefits { + margin: 45px 10px 0 10px; + padding: 15px; + border-radius: 12px; + } + + .benefits-header { + margin-bottom: 6px; + } + + .benefits-header h3 { + font-size: 14px; + } + + .benefits-grid { + gap: 6px; + } + + .benefit-item { + padding: 10px 6px; + border-radius: 10px; + } + + .benefit-icon { + margin-bottom: 5px; + } + + .benefit-icon .el-icon { + font-size: 28px !important; + } + + .benefit-text { + font-size: 11px; + } + + .vip-renewal-notice { + margin: 12px 10px 0 10px; + padding: 12px; + } + + .quick-actions { + bottom: -28px; + left: 12px; + right: 12px; + border-radius: 12px; + } + + .action-item { + padding: 14px 6px; + } + + .action-icon { + width: 42px; + height: 42px; + } + + .action-icon img { + width: 24px; + height: 24px; + } + + .action-text { + font-size: 12px; + } + + .function-list { + margin: 12px 12px 0 12px; + border-radius: 12px; + } + + .function-list-item:first-child { + border-radius: 12px 12px 0 0; + } + + .function-list-item:last-child { + border-radius: 0 0 12px 12px; + } + + .function-list-item:first-child:last-child { + border-radius: 12px; + } + + .user-content { + gap: 10px; + margin-bottom: 18px; + } + + .avatar-circle { + width: 55px; + height: 55px; + border-width: 2px; + } + + .avatar-icon { + font-size: 22px; + } + + .user-name { + font-size: 16px; + } + + .logout-btn { + top: 18px; + right: 12px; + padding: 5px 10px; + font-size: 12px; + } + + .service-btn { + top: 42%; + right: 0px; + transform: translateY(-50%); + padding: 7px 14px 7px 12px; + font-size: 13px; + gap: 4px; + border-radius: 20px 0 0 20px; + } + + .service-btn:hover { + transform: translateY(-50%) translateY(-2px); + } + + .service-icon { + width: 16px; + height: 16px; + } + + .function-list-item { + padding: 15px; + } + + .function-list-item .function-icon { + width: 20px; + height: 20px; + } + + .function-list-item .function-label { + font-size: 14px; + } + + .function-arrow { + font-size: 16px; + } + + .renewal-content { + gap: 8px; + } + + .renewal-icon { + width: 28px; + height: 28px; + border-radius: 6px; + } + + .renewal-icon .el-icon { + font-size: 22px !important; + } + + .renewal-text h4 { + font-size: 13px; + } + + .renewal-text p { + font-size: 11px; + } +} + +/* 编辑按钮样式 */ +.user-name { + display: flex; + align-items: center; + gap: 8px; +} + +.edit-btn { + background: none; + border: none; + padding: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.3s ease; +} + +.edit-btn:hover { + background: rgba(255, 255, 255, 0.2); +} + +.edit-icon { + width: 16px; + height: 16px; + fill: white; +} + +/* 编辑用户信息弹窗样式 */ +.edit-profile-modal { + max-width: 520px; + width: 90%; + max-height: 75vh; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + margin-bottom: 60px; +} + +/* 头像区域 */ +.avatar-section { + background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%); + padding: 24px 24px; + display: flex; + justify-content: center; + border-radius: 16px 16px 0 0; + position: relative; +} + +.avatar-upload { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.avatar-preview { + width: 100px; + height: 100px; + border-radius: 50%; + overflow: hidden; + border: 4px solid white; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + background: white; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.3s ease; +} + +.avatar-preview:hover { + transform: scale(1.05); +} + +.avatar-preview img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatar-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 40px; + font-weight: bold; + color: #4a90e2; + background: linear-gradient(135deg, #f0f4f8 0%, #e8eef5 100%); +} + +.upload-btn { + background: white; + border: 2px solid #e0e6ed; + color: #555; + padding: 8px 18px; + border-radius: 24px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.upload-btn:hover { + background: #4a90e2; + border-color: #4a90e2; + color: white; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(74, 144, 226, 0.2); +} + +.upload-btn:hover .camera-icon { + fill: white; +} + +.upload-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + background: #f5f5f5; + border-color: #e0e6ed; + color: #999; +} + +.upload-btn:disabled:hover { + transform: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.camera-icon { + width: 16px; + height: 16px; + fill: #555; + transition: fill 0.3s ease; +} + +/* 上传遮罩层 */ +.avatar-preview.uploading { + position: relative; +} + +.upload-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.upload-spinner { + width: 24px; + height: 24px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* 表单区域 */ +.edit-profile-modal .modal-body { + overflow-y: auto; + padding: 20px 24px; + flex: 1; + background: #fafbfc; +} + +.edit-form { + padding: 0; +} + +.form-grid { + display: flex; + flex-direction: column; + gap: 14px; +} + +.form-column { + display: flex; + flex-direction: column; + gap: 14px; +} + +.form-item { + display: grid; + grid-template-columns: 130px 1fr; + gap: 14px; + align-items: center; +} + +.form-item label { + font-size: 13px; + font-weight: 600; + color: #4a5568; + padding: 8px 12px; + border-radius: 8px; + background: white; + border: 2px solid #e8eef5; + text-align: center; + transition: all 0.3s ease; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.input-wrapper { + position: relative; +} + +.input-wrapper input { + width: 100%; + padding: 10px 14px; + border: 2px solid #e8eef5; + border-radius: 10px; + font-size: 13px; + transition: all 0.3s ease; + background: white; + color: #2d3748; + box-sizing: border-box; + font-family: inherit; +} + +.input-wrapper input:focus { + outline: none; + border-color: #4a90e2; + background-color: #f7fafc; + box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); +} + +.input-wrapper input::placeholder { + color: #a0aec0; +} + +.form-item.readonly .input-wrapper input { + background-color: #f7fafc; + color: #718096; + cursor: not-allowed; + border-color: #e8eef5; +} + +.input-wrapper input[readonly] { + background-color: #f7fafc; + color: #718096; + cursor: not-allowed; + border-color: #e8eef5; +} + +/* 按钮区域 */ +.edit-profile-modal .modal-footer { + background: white; + border-top: 2px solid #f0f4f8; + padding: 12px 24px 12px; + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.edit-profile-modal .modern-btn { + padding: 11px; + font-size: 14px; + font-weight: 600; + border-radius: 22px; + transition: all 0.3s ease; +} + +.edit-profile-modal .cancel-btn { + background: white; + border: 2px solid #e0e6ed; + color: #4a5568; +} + +.edit-profile-modal .cancel-btn:hover { + background: #f7fafc; + border-color: #cbd5e0; + transform: translateY(-1px); +} + +.edit-profile-modal .primary-btn { + background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); + border: none; + color: white; + box-shadow: 0 4px 12px rgba(74, 144, 226, 0.25); +} + +.edit-profile-modal .primary-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #357abd 0%, #2868a8 100%); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(74, 144, 226, 0.3); +} + +.edit-profile-modal .primary-btn:active:not(:disabled) { + transform: translateY(0); +} + +.edit-profile-modal .primary-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +@media (max-width: 768px) { + .edit-profile-modal { + width: 95%; + max-height: 90vh; + } + + .avatar-section { + padding: 20px 20px; + } + + .avatar-preview { + width: 100px; + height: 100px; + } + + .avatar-placeholder { + font-size: 40px; + } + + .edit-profile-modal .modal-body { + padding: 24px 20px; + } + + .form-grid { + gap: 16px; + } + + .form-item { + grid-template-columns: 105px 1fr; + gap: 12px; + } + + .form-item label { + font-size: 13px; + padding: 8px 10px; + } + + .input-wrapper input { + padding: 10px 14px; + font-size: 13px; + } + + .edit-profile-modal .modal-footer { + padding: 10px 20px; + } + + .edit-profile-modal .modern-btn { + padding: 8px; + font-size: 13px; + border-radius: 20px; + } +} + +/* 版权信息样式 */ +.copyright-section { + background: transparent; + padding: 12px 16px 20px 16px; + margin: 0; +} + +.copyright-content { + text-align: center; +} + +.copyright-text { + font-size: 12px; + color: #666; + margin: 0 0 8px 0; + line-height: 1.6; +} + +.icp-info { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 8px; + font-size: 12px; + margin: 0 0 8px 0; +} + +.icp-info a { + color: #666; + text-decoration: none; + transition: color 0.3s ease; +} + +.icp-info a:hover { + color: #e53e3e; +} + +.license-info { + font-size: 12px; + color: #666; + margin: 0; + text-align: center; +} + +.separator { + color: #ccc; +} + +.police-link { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.police-icon { + width: 14px; + height: 14px; + vertical-align: middle; +} + +@media (max-width: 768px) { + .copyright-section { + padding: 10px 12px 16px 12px; + } + + .icp-info { + flex-direction: column; + gap: 4px; + } + + .separator { + display: none; + } } \ No newline at end of file diff --git a/lottery-app/src/views/Register.vue b/lottery-app/src/views/Register.vue index 0299c34..017cbad 100644 --- a/lottery-app/src/views/Register.vue +++ b/lottery-app/src/views/Register.vue @@ -3,7 +3,7 @@
-

彩票猪手

+

用户注册

您的专属彩票数据助理

@@ -39,20 +39,32 @@
{{ errors.nickname }}
- +
- +
+ + + {{ codeButtonText }} + +
{{ errors.phone }}
请输入11位手机号码
@@ -64,18 +76,9 @@ placeholder="请输入验证码" prefix-icon="Key" size="large" + maxlength="6" @blur="validateCode" - > - - + />
{{ errors.code }}
@@ -115,7 +118,8 @@
我已阅读并同意 - 《用户服务协议》 + 《会员服务协议》 + 《个人信息收集与处理规则》
{{ errors.agreeTerms }}
@@ -175,6 +179,7 @@ export default { codeCountdown: 0, timer: null, + saveTimer: null, // 用于防抖保存的定时器 showPhoneError: false, phoneValid: false, @@ -274,9 +279,7 @@ export default { const response = await lotteryApi.sendSmsCode(this.formData.phone); - if (response.success) { - this.toast.success('验证码已发送,请注意查收'); - } else { + if (!response.success) { // 如果发送失败,停止倒计时 this.codeCountdown = 0; clearInterval(this.timer); @@ -381,7 +384,7 @@ export default { // 服务条款验证 if (!this.formData.agreeTerms) { - this.errors.agreeTerms = '请同意用户服务协议'; + this.errors.agreeTerms = '请同意会员服务协议'; } return Object.keys(this.errors).length === 0; @@ -408,6 +411,7 @@ export default { if (response.success === true) { // 注册成功 + this.clearSavedFormData(); // 清除保存的表单数据 this.toast.success('注册成功!请登录您的账号'); this.router.push('/login'); } else { @@ -427,17 +431,101 @@ export default { } }, + // 保存表单数据到临时存储(带防抖) + saveFormData(immediate = false) { + // 如果不是立即保存,使用防抖 + if (!immediate) { + if (this.saveTimer) { + clearTimeout(this.saveTimer); + } + this.saveTimer = setTimeout(() => { + this.doSaveFormData(); + }, 1000); // 1秒防抖 + } else { + this.doSaveFormData(); + } + }, + + // 实际执行保存操作 + doSaveFormData() { + const formData = { + username: this.formData.username, + nickname: this.formData.nickname, + password: this.formData.password, + confirmPassword: this.formData.confirmPassword, + phone: this.formData.phone, + code: this.formData.code, + agreeTerms: this.formData.agreeTerms + }; + sessionStorage.setItem('register_form_data', JSON.stringify(formData)); + }, + + // 从临时存储恢复表单数据 + restoreFormData() { + const savedData = sessionStorage.getItem('register_form_data'); + if (savedData) { + try { + const formData = JSON.parse(savedData); + this.formData = { ...this.formData, ...formData }; + // 恢复手机号验证状态 + if (formData.phone && this.isValidPhone(formData.phone)) { + this.phoneValid = true; + } + } catch (error) { + console.warn('恢复表单数据失败:', error); + } + } + }, + + // 清除临时存储的表单数据 + clearSavedFormData() { + sessionStorage.removeItem('register_form_data'); + }, + // 显示服务条款 showTerms() { - this.$router.push('/user-agreement'); + // 跳转前立即保存当前表单数据 + this.saveFormData(true); + this.$router.push('/member-agreement'); + }, + + // 显示隐私政策 + showPrivacyPolicy() { + // 跳转前立即保存当前表单数据 + this.saveFormData(true); + this.$router.push('/privacy-policy'); } }, + // 监听表单数据变化,自动保存 + watch: { + 'formData': { + handler(newFormData) { + // 只有当表单有实际内容时才保存,避免保存空数据 + if (newFormData.username || newFormData.nickname || newFormData.phone || + newFormData.password || newFormData.code) { + this.saveFormData(); + } + }, + deep: true, + // 延迟保存,避免频繁操作 + immediate: false + } + }, + // 组件挂载时恢复表单数据 + mounted() { + this.restoreFormData(); + }, // 组件销毁时清除定时器 beforeUnmount() { if (this.timer) { clearInterval(this.timer); this.timer = null; } + // 清除保存定时器 + if (this.saveTimer) { + clearTimeout(this.saveTimer); + this.saveTimer = null; + } } } @@ -447,18 +535,19 @@ export default { .register-page-container { min-height: calc(100vh - 70px); background: #f0f2f5; - padding: 20px 20px 0px 20px; + padding: 20px 20px 8px 20px; } /* 页面头部 */ .page-header { - background: url('@/assets/banner/backend1.png') center/cover no-repeat, linear-gradient(135deg, #ff6b6b, #ee5a52); + background: linear-gradient(135deg, #ff6b6b, #ee5a52); color: white; - padding: 45px 20px 25px; + padding: 35px 20px 25px; text-align: center; position: relative; - margin-bottom: 20px; - border-radius: 8px; + margin-bottom: 15px; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(238, 90, 82, 0.3); } .page-title { @@ -467,31 +556,31 @@ export default { } .main-title { - font-size: 38px; - margin: 0 auto 10px; + font-size: 32px; + margin: 0 auto 4px; 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-shadow: 0 2px 8px rgba(0,0,0,0.5), 0 0 20px rgba(0,0,0,0.3); + letter-spacing: 1px; text-align: center; width: 100%; } .subtitle { - font-size: 22px; + font-size: 16px; 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); + opacity: 0.95; + text-shadow: 0 2px 4px rgba(0,0,0,0.4); text-align: center; width: 100%; - font-weight: 500; + font-weight: 400; } /* 桌面端样式 */ @media (min-width: 1024px) { .page-header { - padding: 40px 20px 30px; + padding: 30px 20px 25px; } } @@ -499,20 +588,21 @@ export default { padding: 0; background: white; margin: 0 0 20px 0; - border-radius: 8px; - box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); + overflow: hidden; } .register-form { background: white; border-radius: 0; - padding: 30px 25px; + padding: 28px 24px 20px; box-shadow: none; } /* 表单组 */ .form-group { - margin-bottom: 24px; + margin-bottom: 16px; } .input-wrapper { @@ -598,7 +688,49 @@ input:-webkit-autofill:active { outline: none !important; } -/* Element Plus验证码按钮已在组件中实现,移除原来的样式以避免冲突 */ +/* 手机号和验证码按钮同行布局 */ +.phone-code-row { + display: flex; + gap: 12px; + align-items: stretch; +} + +.phone-input { + flex: 1; +} + +/* 内联发送验证码按钮样式 */ +.send-code-btn-inline { + background: linear-gradient(135deg, #e53e3e, #ff6b6b); + border: none; + border-radius: 12px; + font-weight: 500; + transition: all 0.3s ease; + min-width: 120px; + flex-shrink: 0; + height: auto; + display: flex; + align-items: center; + justify-content: center; +} + +.send-code-btn-inline:hover:not(.is-disabled) { + background: linear-gradient(135deg, #d43030, #ff5a5a); + transform: translateY(-1px); + box-shadow: 0 4px 15px rgba(229, 62, 62, 0.3); +} + +.send-code-btn-inline:active:not(.is-disabled) { + transform: translateY(0); +} + +.send-code-btn-inline.is-disabled { + background: #cccccc !important; + border-color: #cccccc !important; + color: #888 !important; + transform: none !important; + box-shadow: none !important; +} /* 隐藏浏览器自带的密码控件 */ input::-ms-reveal, @@ -682,51 +814,81 @@ input::-webkit-credentials-auto-fill-button { color: #e53e3e; text-decoration: none; margin-left: 4px; + cursor: pointer; + -webkit-tap-highlight-color: transparent !important; + -webkit-touch-callout: none !important; + -webkit-user-select: none !important; + -khtml-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; + outline: none !important; + background-color: transparent !important; + display: inline; } .terms-link:hover { text-decoration: underline; } +.terms-link:active { + background-color: transparent !important; + outline: none !important; + -webkit-tap-highlight-color: transparent !important; +} + /* 注册按钮 */ .register-btn { width: 100%; - margin: 30px 0 25px 0; - padding: 12px; - font-size: 18px; - height: auto; + margin: 8px 0 24px 0; + padding: 14px; + font-size: 16px; + font-weight: 600; + height: 52px; background: linear-gradient(135deg, #e53e3e, #ff6b6b); border: none; - box-shadow: 0 4px 15px rgba(229, 62, 62, 0.3); + border-radius: 12px; + box-shadow: 0 4px 20px rgba(229, 62, 62, 0.25); + transition: all 0.3s ease; } .register-btn:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(229, 62, 62, 0.4); + transform: translateY(-1px); + box-shadow: 0 6px 30px rgba(229, 62, 62, 0.35); background: linear-gradient(135deg, #d43030, #ff5a5a); border: none; } +.register-btn:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 2px 10px rgba(229, 62, 62, 0.3); +} + /* Element UI 组件自定义样式 */ :deep(.el-input__wrapper) { - padding: 4px 11px; + padding: 6px 16px; box-shadow: none !important; background-color: #f8f9fa; border: 2px solid #e9ecef; + border-radius: 12px; + transition: all 0.3s ease; } :deep(.el-input__wrapper.is-focus) { background-color: #fff; border-color: #e53e3e; + box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1); } :deep(.el-input__prefix) { - margin-right: 8px; + margin-right: 12px; + color: #999; } :deep(.el-input__inner) { - height: 40px; - font-size: 16px; + height: 44px; + font-size: 15px; + color: #333; } :deep(.el-checkbox__label) { @@ -743,35 +905,6 @@ input::-webkit-credentials-auto-fill-button { 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; @@ -779,9 +912,36 @@ input::-webkit-credentials-auto-fill-button { } /* 处理用户协议链接 */ +.checkbox-custom { + align-items: flex-start !important; +} + +.checkbox-custom :deep(.el-checkbox__input) { + margin-top: 4px; +} + .checkbox-custom :deep(.el-checkbox__label) { - display: flex; - align-items: center; + display: block; + line-height: 1.8; + white-space: normal; + word-break: break-word; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + padding-top: 0; +} + +.checkbox-custom :deep(.el-checkbox__label) * { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -webkit-tap-highlight-color: transparent !important; + -webkit-touch-callout: none; + background-color: transparent !important; } /* 登录链接 */ @@ -806,70 +966,78 @@ input::-webkit-credentials-auto-fill-button { /* 响应式设计 */ @media (max-width: 768px) { .register-page-container { - padding: 10px; + padding: 10px 10px 5px 10px; } .page-header { - padding: 45px 15px 35px; + padding: 30px 15px 25px; } .page-title { - margin: 0 0 8px 0; + margin: 0; } - .send-code-btn { - font-size: 12px; - min-width: 90px; - padding: 0 10px; - } } @media (max-width: 480px) { + .register-page-container { + padding: 5px 5px 3px 5px; + } + .page-header { - padding: 40px 12px 30px; + padding: 30px 20px 25px; + margin-bottom: 12px; } .page-title { - margin: 0 0 6px 0; + margin: 0; } .main-title { font-size: 28px; margin-bottom: 3px; + letter-spacing: 0.5px; } .subtitle { - font-size: 18px; + font-size: 14px; } .register-form { - padding: 20px 15px; + padding: 24px 20px 18px; } - .input-icon { - padding: 12px 25px; + :deep(.el-input__inner) { + height: 42px; + font-size: 14px; } - .icon-img { - width: 22px; - height: 22px; + .register-btn { + height: 48px; + font-size: 15px; + margin: 8px 0 20px 0; } - .toggle-icon-img { - width: 20px; - height: 20px; - } - - .password-toggle { - padding: 0 12px; - } - - .send-code-btn { + .send-code-btn-inline { font-size: 12px; - min-width: 85px; - padding: 0 8px; + height: 42px; + min-width: 90px; + padding: 0 10px; + } + + .phone-code-row { + gap: 8px; + } + + .checkbox-custom :deep(.el-checkbox__label) { + font-size: 13px; + } + + .terms-link { + display: inline; + margin: 0 2px; } } /* 桌面端样式 - 这部分已在上面定义,这里移除重复定义 */ - \ No newline at end of file + \ No newline at end of file diff --git a/lottery-app/src/views/ResetPassword.vue b/lottery-app/src/views/ResetPassword.vue index abe196b..27c7193 100644 --- a/lottery-app/src/views/ResetPassword.vue +++ b/lottery-app/src/views/ResetPassword.vue @@ -3,7 +3,8 @@
-

找回密码

+

找回密码

+

重置您的登录密码

@@ -12,118 +13,81 @@
-
-
- 手机号 -
- -
+
{{ errors.phone }}
请输入11位手机号码
-
-
- 验证码 -
- - -
+ + +
{{ errors.code }}
-
-
- 新密码 -
- -
- 隐藏密码 - 显示密码 -
-
+
{{ errors.newPassword }}
-
-
- 确认新密码 -
- -
- 隐藏密码 - 显示密码 -
-
+
{{ errors.confirmPassword }}
- +
@@ -148,9 +112,18 @@ import { lotteryApi } from '../api/index.js' import { useToast } from 'vue-toastification' import { useRouter } from 'vue-router' +import { ElInput, ElButton } from 'element-plus' +import { Lock, Iphone, Key } from '@element-plus/icons-vue' export default { name: 'ResetPassword', + components: { + ElInput, + ElButton, + Lock, + Iphone, + Key + }, setup() { const toast = useToast() const router = useRouter() @@ -380,43 +353,63 @@ export default { /* 页面容器 */ .reset-password-page-container { min-height: calc(100vh - 70px); - background: #f8f9fa; - padding: 0; + background: #f0f2f5; + padding: 20px 20px 8px 20px; } /* 页面头部 */ .page-header { - background: url('@/assets/banner/backend1.png') center/cover no-repeat, linear-gradient(135deg, #ff6b6b, #ee5a52); + background: linear-gradient(135deg, #ff6b6b, #ee5a52); color: white; - padding: 50px 20px 40px; + padding: 35px 20px 25px; text-align: center; position: relative; + margin-bottom: 15px; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(238, 90, 82, 0.3); } .page-title { - margin: 0 0 8px 0; + margin: 0; text-align: center; } -.page-title h1 { - font-size: 36px; +.main-title { + font-size: 32px; + margin: 0 auto 4px; + font-weight: 700; + color: white; + text-shadow: 0 2px 8px rgba(0,0,0,0.5), 0 0 20px rgba(0,0,0,0.3); + letter-spacing: 1px; + text-align: center; + width: 100%; +} + +.subtitle { + font-size: 16px; margin: 0; - font-weight: bold; - text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + color: white; + opacity: 0.95; + text-shadow: 0 2px 4px rgba(0,0,0,0.4); + text-align: center; + width: 100%; + font-weight: 400; } .reset-password-form-container { padding: 0; background: white; - margin: 0 20px; + margin: 0 0 20px 0; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); + overflow: hidden; } .reset-password-form { background: white; border-radius: 0; - padding: 30px 25px; + padding: 32px 24px 24px; box-shadow: none; - min-height: calc(100vh - 320px); } /* 表单组 */ @@ -424,123 +417,87 @@ export default { margin-bottom: 24px; } -.input-wrapper { - position: relative; - display: flex; - align-items: center; +/* Element UI 组件自定义样式 */ +:deep(.el-input__wrapper) { + padding: 6px 16px; + box-shadow: none !important; + background-color: #f8f9fa; border: 2px solid #e9ecef; - border-radius: 6px; - background: #f8f9fa; + border-radius: 12px; transition: all 0.3s ease; - min-height: 56px; - overflow: hidden; } -.input-wrapper:focus-within { - border-color: #e9ecef; - background: white; - box-shadow: none; - outline: none; +/* 带有 append 按钮的输入框样式优化 */ +:deep(.el-input-group .el-input__wrapper) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: 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; +:deep(.el-input-group__append) { + border-top-right-radius: 12px; + border-bottom-right-radius: 12px; + padding: 0; 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; +:deep(.el-input__wrapper.is-focus) { + background-color: #fff; + border-color: #e53e3e; + box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1); } -/* 控制浏览器自动填充的样式 */ -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; +/* 带有 append 按钮的输入框聚焦时的样式 */ +:deep(.el-input-group .el-input__wrapper.is-focus) { + border-color: #e53e3e; + box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1); } -/* 发送验证码按钮 */ -.send-code-btn { - height: 32px; - margin-right: 5px; - padding: 0 10px; - border: none; - color: white; +:deep(.el-input-group.is-focus .el-input-group__append .el-button) { + border-color: #e53e3e; + box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1); +} + +:deep(.el-input__prefix) { + margin-right: 12px; + color: #999; +} + +:deep(.el-input__inner) { + height: 44px; + font-size: 15px; + color: #333; +} + +:deep(.el-input-group__append .el-button) { + background: linear-gradient(135deg, #e53e3e, #ff6b6b); + border: 2px solid #e53e3e; + border-left: none; + margin: 0; + height: 100%; + border-radius: 0 12px 12px 0; + padding: 0 16px; 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; + color: white; + min-width: 100px; box-shadow: none; + transition: all 0.3s ease; } -.send-code-btn:disabled { +:deep(.el-input-group__append .el-button:hover) { + background: linear-gradient(135deg, #d43030, #ff5a5a); + border-color: #d43030; + color: white; + transform: none; +} + +:deep(.el-input-group__append .el-button.is-disabled) { background: #cccccc; - cursor: not-allowed; -} - -.send-code-btn:hover:not(:disabled) { - background: #d43030; + border-color: #cccccc; + color: #888; + transform: none; } /* 提示文本 */ @@ -558,79 +515,36 @@ input:-webkit-autofill:active { 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; + margin: 32px 0 28px 0; + padding: 14px; + font-size: 16px; font-weight: 600; - cursor: pointer; + height: 52px; + background: linear-gradient(135deg, #e53e3e, #ff6b6b); + border: none; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(229, 62, 62, 0.25); 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); + transform: translateY(-1px); + box-shadow: 0 6px 30px rgba(229, 62, 62, 0.35); + background: linear-gradient(135deg, #d43030, #ff5a5a); + border: none; } -.reset-password-btn:disabled { - opacity: 0.7; - cursor: not-allowed; - transform: none; - box-shadow: 0 4px 15px rgba(229, 62, 62, 0.2); +.reset-password-btn:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 2px 10px rgba(229, 62, 62, 0.3); +} + +:deep(.el-button.is-disabled) { + background: #cccccc; + border-color: #cccccc; } /* 返回登录链接 */ @@ -757,25 +671,24 @@ input::-webkit-credentials-auto-fill-button { box-shadow: 0 8px 25px rgba(244, 67, 54, 0.3) !important; } +/* 桌面端样式 */ +@media (min-width: 1024px) { + .page-header { + padding: 30px 20px 25px; + } +} + /* 响应式设计 */ @media (max-width: 768px) { - .page-header { - padding: 45px 15px 35px; + .reset-password-page-container { + padding: 10px 10px 5px 10px; } - - .page-title { - margin: 0 0 8px 0; - } - - .page-title h1 { - font-size: 30px; - } - + .reset-password-form { padding: 25px 20px; } - - .send-code-btn { + + :deep(.el-input-group__append .el-button) { font-size: 12px; min-width: 90px; padding: 0 10px; @@ -783,66 +696,48 @@ input::-webkit-credentials-auto-fill-button { } @media (max-width: 480px) { + .reset-password-page-container { + padding: 5px 5px 3px 5px; + } + .page-header { - padding: 40px 12px 30px; + padding: 30px 20px 25px; + margin-bottom: 12px; } - + .page-title { - margin: 0 0 6px 0; + margin: 0; } - - .page-title h1 { - font-size: 26px; + + .main-title { + font-size: 28px; + margin-bottom: 3px; + letter-spacing: 0.5px; } - + + .subtitle { + font-size: 14px; + } + .reset-password-form { - padding: 20px 15px; + padding: 28px 20px 20px; } - - .input-icon { - padding: 12px 25px; + + :deep(.el-input__inner) { + height: 42px; + font-size: 14px; } - - .icon-img { - width: 22px; - height: 22px; + + .reset-password-btn { + height: 48px; + font-size: 15px; + margin: 28px 0 24px 0; } - - .toggle-icon-img { - width: 20px; - height: 20px; - } - - .password-toggle { + + :deep(.el-input-group__append .el-button) { + font-size: 12px; + min-width: 90px; 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; - } -} - \ No newline at end of file + \ No newline at end of file diff --git a/lottery-app/src/views/UserAgreement.vue b/lottery-app/src/views/UserAgreement.vue index ce66f91..fbc839e 100644 --- a/lottery-app/src/views/UserAgreement.vue +++ b/lottery-app/src/views/UserAgreement.vue @@ -7,11 +7,11 @@
-

欢迎使用《彩票猪手》数据服务!

+

欢迎使用《精彩猪手》数据服务!

  在开始使用我们的服务之前,请您(用户)仔细阅读并充分理解本《用户服务协议》(以下简称 "本协议")的全部内容。本协议是您(用户)与西安精彩数据服务社之间关于使用本服务的法律协议,一旦您(用户)使用本服务,即表示您(用户)已同意接受本协议的约束。如果您(用户)不同意本协议的任何条款,请不要使用本服务。

一、定义
-

1、本服务:指我们通过网站、应用程序或其他相关平台向您提供的《彩票猪手》数据服务,包括但不限于信息发布、数据存储、在线交流等。

+

1、本服务:指我们通过网站、应用程序或其他相关平台向您提供的《精彩猪手》数据服务,包括但不限于信息发布、数据存储、在线交流等。

2、用户:指承认本协议,接受本服务的自然人、法人或其他组织。具体包含付费账号用户和体验账号用户。

3、个人信息:以电子或者其他方式记录的与已识别或者可识别的自然人有关的各种信息,不包括匿名化处理后的信息。

4、知识产权:包括但不限于著作权、专利权、商标权、商业秘密等。

@@ -22,7 +22,7 @@

1.2既定彩票数据分析报告;

1.3个性化开奖号码辅助推导、推测;

1.4推测记录统计、备份;

-

1.5彩票猪手AI互动交流服务。

+

1.5精彩猪手AI互动交流服务。

2、我们会根据实际情况对服务内容进行调整、更新或终止。如有重大变更,我们将通过企业公众号通知您(用户),需要您(用户)及时接收。如您(用户)在服务内容发生变更后继续使用本服务,视为您(用户)接受变更后的协议内容。

3、您(用户)理解并同意,使用本服务可能需要您(用户)具备相应的设备、软件、网络环境和一定的专业知识,相关费用由您(用户)自行承担。

@@ -83,7 +83,7 @@

2、本协议各条款的标题仅为方便阅读而设,不影响条款的具体含义及解释。

3、若本协议任何条款被认定为无效或不可执行,不影响其他条款的效力及执行。

4、我们未行使或执行本协议任何权利或条款,不构成对该权利或条款的放弃。

-

5、本协议自您(用户)成功注册彩票猪手服务相关账号之日即刻生效。

+

5、本协议自您(用户)成功注册精彩猪手服务相关账号之日即刻生效。

6、任何有关本协议项下服务的问题,您(用户)可通过本企业微信号进行咨询。

@@ -109,7 +109,7 @@ export default { diff --git a/lottery-app/src/views/VipCodeManagement.vue b/lottery-app/src/views/VipCodeManagement.vue deleted file mode 100644 index 299f69a..0000000 --- a/lottery-app/src/views/VipCodeManagement.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/lottery-app/src/views/admin/AdminLogin.vue b/lottery-app/src/views/admin/AdminLogin.vue index 72da288..57979b5 100644 --- a/lottery-app/src/views/admin/AdminLogin.vue +++ b/lottery-app/src/views/admin/AdminLogin.vue @@ -37,6 +37,18 @@

请输入您的管理员账号和密码

+ + + + + diff --git a/lottery-app/src/views/admin/Dashboard.vue b/lottery-app/src/views/admin/Dashboard.vue index 583d0ac..b7e7fad 100644 --- a/lottery-app/src/views/admin/Dashboard.vue +++ b/lottery-app/src/views/admin/Dashboard.vue @@ -9,7 +9,7 @@
- +
@@ -22,7 +22,7 @@
- +
@@ -35,7 +35,7 @@
- +
@@ -48,7 +48,7 @@
- +
@@ -61,9 +61,55 @@
+ + +
+
+ +
+
+
{{ pvStats.totalPV }}
+
总访问量
+
+
+
+
+ + +
+
+ +
+
+
{{ pvStats.todayPV }}
+
今日访问
+
+
+
+
+ +
+ + +
+
+
+
@@ -75,7 +121,7 @@
-
+
@@ -85,7 +131,7 @@
-
+
@@ -95,7 +141,7 @@
-
+
@@ -118,7 +164,7 @@ 最近操作
- + 更多 @@ -177,7 +223,7 @@ + + + diff --git a/lottery-app/src/views/admin/DltPredictionManagement.vue b/lottery-app/src/views/admin/DltPredictionManagement.vue new file mode 100644 index 0000000..a32231f --- /dev/null +++ b/lottery-app/src/views/admin/DltPredictionManagement.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/lottery-app/src/views/admin/DltPrizeStatistics.vue b/lottery-app/src/views/admin/DltPrizeStatistics.vue new file mode 100644 index 0000000..116efd2 --- /dev/null +++ b/lottery-app/src/views/admin/DltPrizeStatistics.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/lottery-app/src/views/admin/ExcelImportManagement.vue b/lottery-app/src/views/admin/ExcelImportManagement.vue index f83831c..24736c3 100644 --- a/lottery-app/src/views/admin/ExcelImportManagement.vue +++ b/lottery-app/src/views/admin/ExcelImportManagement.vue @@ -1,8 +1,789 @@ \ No newline at end of file +.excel-import-management { + min-height: 100vh; + background: #f5f5f5; + padding: 24px; +} + +/* 页面头部 */ +.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: 24px; +} + +/* 权限检查样式 */ +.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; +} + +/* 功能区域 */ +.function-area { + margin-bottom: 24px; +} + +.function-card { + border: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + height: 100%; +} + +.card-header { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: #333; +} + +.card-header .el-icon { + font-size: 18px; + color: #409EFF; +} + +.card-desc { + margin-bottom: 20px; +} + +.card-desc p { + color: #666; + margin: 0; + font-size: 14px; + line-height: 1.5; +} + +/* 上传区域 */ +.upload-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.file-input-container { + position: relative; +} + +.file-input { + display: none; +} + +.file-label { + display: flex; + align-items: center; + padding: 12px 16px; + border: 2px dashed #dcdfe6; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + background: #fafafa; + min-height: 60px; +} + +.file-label:hover { + border-color: #409eff; + background: #ecf5ff; +} + +.file-icon { + font-size: 20px; + margin-right: 12px; + color: #409eff; +} + +.file-text { + color: #606266; + font-size: 14px; + flex: 1; + word-break: break-all; +} + +/* 减少卡片内边距 */ +:deep(.el-card__body) { + padding: 16px !important; +} + +/* 结果消息 */ +.result-message { + padding: 12px; + border-radius: 6px; + font-weight: 600; + font-size: 14px; + margin-top: 12px; +} + +.result-message.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.result-message.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +/* 信息卡片 */ +.info-card { + border: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* 信息说明 */ +.info-content { + display: flex; + flex-direction: column; + gap: 20px; +} + +.info-item h4 { + color: #333; + margin-bottom: 8px; + font-size: 16px; + font-weight: 600; +} + +.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; +} + +.btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 600; +} + +.btn-primary { + background: #409eff; + color: white; +} + +/* 响应式设计 */ +@media (max-width: 1200px) { + .function-area .el-col { + margin-bottom: 20px; + } +} + +@media (max-width: 768px) { + .excel-import-management { + padding: 16px; + } + + .function-area .el-row { + flex-wrap: wrap; + } + + .function-area .el-col { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 16px; + } + + .page-header { + padding: 20px; + } + + .page-header h1 { + font-size: 24px; + } + + .card-header span { + font-size: 14px; + } + + .card-desc { + margin-bottom: 12px; + } + + .card-desc p { + font-size: 12px; + line-height: 1.4; + } + + .file-label { + padding: 8px 10px; + min-height: 45px; + } + + .file-text { + font-size: 12px; + } + + .file-icon { + font-size: 16px; + margin-right: 8px; + } + + .upload-section { + gap: 12px; + } + + .info-item h4 { + font-size: 14px; + } + + .info-item p { + font-size: 12px; + } +} + \ No newline at end of file diff --git a/lottery-app/src/views/admin/PredictionManagement.vue b/lottery-app/src/views/admin/PredictionManagement.vue new file mode 100644 index 0000000..f99fddd --- /dev/null +++ b/lottery-app/src/views/admin/PredictionManagement.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/lottery-app/src/views/admin/PrizeStatistics.vue b/lottery-app/src/views/admin/PrizeStatistics.vue new file mode 100644 index 0000000..519ab48 --- /dev/null +++ b/lottery-app/src/views/admin/PrizeStatistics.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/lottery-app/src/views/admin/UserList.vue b/lottery-app/src/views/admin/UserList.vue index e2b397c..7deec2c 100644 --- a/lottery-app/src/views/admin/UserList.vue +++ b/lottery-app/src/views/admin/UserList.vue @@ -179,8 +179,11 @@ - + + + + + {{ currentUserDetail.id }} + {{ currentUserDetail.userAccount }} + {{ currentUserDetail.userName }} + {{ currentUserDetail.phone }} + + + {{ getRoleText(currentUserDetail.userRole) }} + + + + + {{ currentUserDetail.status === 0 ? '正常' : '禁用' }} + + + + + {{ currentUserDetail.isVip === 1 ? '是' : '否' }} + + + + + {{ currentUserDetail.vipType || '体验会员' }} + + + + {{ currentUserDetail.vipExpire ? formatDate(currentUserDetail.vipExpire) : '-' }} + + + {{ currentUserDetail.location || '-' }} + + + {{ currentUserDetail.preference || '-' }} + + + {{ currentUserDetail.channel || '-' }} + + + {{ formatDate(currentUserDetail.createTime) }} + + + {{ formatDate(currentUserDetail.updateTime) }} + + + +
+
+ +
+ +
+ 推测记录 + 使用统计 + 奖金统计 +
+
+ + +
+ + + +
+ + {{ usageStats.userId || '-' }} + + + {{ selectedLotteryType === 'ssq' ? '双色球' : '大乐透' }} + + + {{ usageStats.predictCount || 0 }} + {{ usageStats.pendingCount || 0 }} + {{ usageStats.drawnCount || 0 }} + {{ usageStats.hitCount || 0 }} + + {{ formatHitRate(usageStats.hitRate) }} + + +
+ +
+ { - router.push('/admin/dashboard') + router.push('/cpzsadmin/dashboard') }, 1500) return false } @@ -384,6 +490,21 @@ export default { const dialogVisible = ref(false) const dialogTitle = ref('添加用户') const submitLoading = ref(false) + // 详情对话框 + const detailDialogVisible = ref(false) + const currentUserDetail = ref({}) + const selectedLotteryType = ref('ssq') // 默认选中双色球 + // 使用统计弹窗 + const usageStatsDialogVisible = ref(false) + const usageStatsLoading = ref(false) + const usageStats = ref({ + userId: '', + predictCount: 0, + pendingCount: 0, + hitCount: 0, + drawnCount: 0, + hitRate: 0 + }) // 删除确认弹窗 const deleteDialogVisible = ref(false) const userToDelete = ref(null) @@ -550,6 +671,12 @@ export default { dialogVisible.value = true } + // 显示用户详情 + const showUserDetail = (row) => { + currentUserDetail.value = { ...row } + detailDialogVisible.value = true + } + // 编辑用户 const editUser = (row) => { dialogTitle.value = '编辑用户' @@ -613,7 +740,7 @@ export default { if (response?.code === 40100 || response?.code === 40101) { ElMessage.error('登录已过期,请重新登录') setTimeout(() => { - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' }, 1500) } } @@ -632,7 +759,7 @@ export default { if (error.response && error.response.status === 401) { ElMessage.error('登录已过期,请重新登录') setTimeout(() => { - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' }, 1500) } } @@ -786,6 +913,75 @@ export default { loadUserList() }) + // 查看推测记录 + const viewPredictRecords = (user) => { + const path = selectedLotteryType.value === 'dlt' + ? '/cpzsadmin/prediction/dlt' + : '/cpzsadmin/prediction/ssq' + + router.push({ + path: path, + query: { userId: user.id } + }) + } + + // 查看使用统计 + const viewUsageStats = async (user) => { + usageStatsDialogVisible.value = true + usageStatsLoading.value = true + + try { + let response + if (selectedLotteryType.value === 'dlt') { + // 大乐透使用 dlt API + const dltApi = await import('../../api/dlt/index.js') + response = await dltApi.dltLotteryApi.getUserPredictStats(user.id) + } else { + // 双色球使用主 API + response = await lotteryApi.getUserPredictStat(user.id, 'ssq') + } + + if (response && response.success) { + usageStats.value = response.data + } else { + ElMessage.error(response?.message || '获取使用统计失败') + } + } catch (error) { + console.error('获取使用统计失败:', error) + ElMessage.error('获取使用统计失败') + } finally { + usageStatsLoading.value = false + } + } + + // 查看奖金统计 + const viewPrizeStats = (user) => { + const path = selectedLotteryType.value === 'dlt' + ? '/cpzsadmin/prize-statistics/dlt' + : '/cpzsadmin/prize-statistics/ssq' + + router.push({ + path: path, + query: { userId: user.id } + }) + } + + // 格式化命中率 + const formatHitRate = (rate) => { + if (typeof rate !== 'number') return '0.00%' + return (rate * 100).toFixed(2) + '%' + } + + // 获取会员类型标签颜色 + const getVipTypeTag = (vipType) => { + const typeMap = { + '年度会员': 'warning', + '月度会员': 'primary', + '体验会员': 'info' + } + return typeMap[vipType] || 'info' + } + return { searchForm, userList, @@ -808,6 +1004,9 @@ export default { editUser, toggleUserStatus, deleteUser, + showUserDetail, + detailDialogVisible, + currentUserDetail, submitForm, resetForm, getRoleType, @@ -816,7 +1015,16 @@ export default { deleteDialogVisible, userToDelete, confirmDeleteUser, - cancelDeleteUser + cancelDeleteUser, + viewPredictRecords, + viewUsageStats, + viewPrizeStats, + selectedLotteryType, + usageStatsDialogVisible, + usageStatsLoading, + usageStats, + formatHitRate, + getVipTypeTag } } } diff --git a/lottery-app/src/views/admin/VipCodeManagement.vue b/lottery-app/src/views/admin/VipCodeManagement.vue index 154481f..93cdbee 100644 --- a/lottery-app/src/views/admin/VipCodeManagement.vue +++ b/lottery-app/src/views/admin/VipCodeManagement.vue @@ -78,17 +78,13 @@ -
- -
+
@@ -117,17 +113,13 @@ -
- -
+
@@ -166,6 +158,14 @@
+
+ +
@@ -178,6 +178,10 @@ 会员码列表
+ + + 导出 + 刷新 @@ -223,7 +227,18 @@ > + + + + + + + + + + +
@@ -419,6 +434,7 @@ export default { }) const getCodeLoading = ref(false) const availableCode = ref('') + const getCodeError = ref('') const getCodeRules = { vipExpireTime: [ @@ -430,7 +446,7 @@ export default { const searchForm = reactive({ keyword: '', status: '', - expireTime: 1 // 设置默认值为1个月 + expireTime: '' // 设置默认值为全部有效期 }) // 有效期选项 @@ -491,14 +507,14 @@ export default { // 检查管理员登录状态 if (!userStore.isAdminLoggedIn()) { ElMessage.error('请先登录后台管理系统') - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' return } // 初始化表单默认值 generateForm.vipExpireTime = 1 getCodeForm.vipExpireTime = 1 - searchForm.expireTime = 1 + searchForm.expireTime = '' // 搜索筛选默认显示全部有效期 loadStats() loadCodeList() @@ -522,7 +538,7 @@ export default { // 如果是未登录或权限错误,可能需要重新登录 if (response?.code === 40100 || response?.code === 40101) { - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' } } } catch (error) { @@ -530,7 +546,7 @@ export default { // 如果是401错误,可能是登录态过期 if (error.response && error.response.status === 401) { - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' } } } @@ -568,7 +584,7 @@ export default { // 如果是未登录或权限错误,可能需要重新登录 if (response?.code === 40100 || response?.code === 40101) { - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' } } } catch (error) { @@ -580,7 +596,7 @@ export default { // 如果是401错误,可能是登录态过期 if (error.response && error.response.status === 401) { - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' } } finally { generateLoading.value = false @@ -597,11 +613,14 @@ export default { if (response && response.success && response.data) { availableCode.value = response.data + getCodeError.value = '' ElMessage({ type: 'success', message: '获取会员码成功' }) } else { + availableCode.value = '' + getCodeError.value = response?.message || '暂无可用会员码,请先生成' ElMessage({ type: 'warning', message: response?.message || '暂无可用会员码,请先生成' @@ -609,11 +628,13 @@ export default { // 如果是未登录或权限错误,可能需要重新登录 if (response?.code === 40100 || response?.code === 40101) { - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' } } } catch (error) { console.error('获取会员码失败:', error) + availableCode.value = '' + getCodeError.value = error?.response?.data?.message || '获取失败,请重试' ElMessage({ type: 'error', message: error?.response?.data?.message || '获取失败,请重试' @@ -621,7 +642,7 @@ export default { // 如果是401错误,可能是登录态过期 if (error.response && error.response.status === 401) { - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' } } finally { getCodeLoading.value = false @@ -683,7 +704,7 @@ export default { // 如果是未登录或权限错误,可能需要重新登录 if (response?.code === 40100 || response?.code === 40101) { - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' } } } catch (error) { @@ -695,7 +716,7 @@ export default { // 如果是401错误,可能是登录态过期 if (error.response && error.response.status === 401) { - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' } } finally { tableLoading.value = false @@ -738,6 +759,70 @@ export default { return date.toLocaleString('zh-CN') } + // 导出到Excel + const exportToExcel = async () => { + try { + tableLoading.value = true + + // 获取所有数据(不分页) + const params = { + page: 1, + size: 10000, // 获取所有数据 + ...searchForm + } + + const response = await lotteryApi.getVipCodeList(params) + + if (response && response.success) { + const allData = response.data.records || [] + + if (allData.length === 0) { + ElMessage.warning('没有数据可以导出') + return + } + + // 准备CSV数据 + const headers = ['VIP编号', '会员码', '有效期', '状态', '创建人', '创建时间', '使用人ID', '使用时间'] + const csvContent = [ + headers.join(','), + ...allData.map(row => [ + row.vipNumber || 1, + row.code, + `${row.vipExpireTime}个月`, + row.isUse === 0 ? '可用' : '已使用', + row.createdUserName || '-', + formatDate(row.createTime), + row.usedUserName || '-', + row.updateTime ? formatDate(row.updateTime) : '-' + ].join(',')) + ].join('\n') + + // 添加BOM以支持中文 + const BOM = '\uFEFF' + const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' }) + + // 创建下载链接 + const link = document.createElement('a') + const url = URL.createObjectURL(blob) + link.setAttribute('href', url) + link.setAttribute('download', `会员码列表_${new Date().toLocaleDateString('zh-CN').replace(/\//g, '-')}.csv`) + link.style.visibility = 'hidden' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + ElMessage.success(`成功导出 ${allData.length} 条数据`) + } else { + ElMessage.error(response?.message || '导出失败') + } + } catch (error) { + console.error('导出失败:', error) + ElMessage.error('导出失败,请重试') + } finally { + tableLoading.value = false + } + } + // 验证表单 const validateGenerateForm = () => { generateFormRef.value?.validateField('vipExpireTime') @@ -758,6 +843,7 @@ export default { getCodeRules, getCodeLoading, availableCode, + getCodeError, searchForm, expireTimeOptions, codeList, @@ -782,7 +868,8 @@ export default { confirmActionLoading, confirmGenerateVipCodes, confirmGetAvailableCode, - cancelDialog + cancelDialog, + exportToExcel } } } @@ -905,6 +992,10 @@ export default { margin-top: 16px; } +.code-error { + margin-top: 16px; +} + .code-content { display: flex; align-items: center; diff --git a/lottery-app/src/views/admin/layout/AdminLayout.vue b/lottery-app/src/views/admin/layout/AdminLayout.vue index 3cda8a3..f7c062f 100644 --- a/lottery-app/src/views/admin/layout/AdminLayout.vue +++ b/lottery-app/src/views/admin/layout/AdminLayout.vue @@ -5,7 +5,7 @@
- Logo + Logo

后台管理

@@ -20,30 +20,75 @@ active-text-color="#409EFF" class="sidebar-menu" > - + - + - + - - - - + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@@ -64,7 +109,7 @@ - 首页 + 首页 {{ item }} @@ -123,7 +168,10 @@ import { Refresh, Setting, SwitchButton, - Clock + Clock, + Bell, + Aim, + Trophy } from '@element-plus/icons-vue' import { userStore } from '../../../store/user.js' import { lotteryApi } from '../../../api/index.js' @@ -138,6 +186,7 @@ export default { Fold, Expand, ArrowDown, + Bell, Refresh, Setting, SwitchButton, @@ -162,10 +211,36 @@ export default { const map = { 'user-list': ['用户管理'], 'vip-code': ['会员码管理'], - 'excel-import': ['数据导入'], + 'excel-import': { + 'ssq': ['数据导入', '双色球'], + 'dlt': ['数据导入', '大乐透'], + 'default': ['数据导入'] + }, + 'prediction': { + 'ssq': ['推测管理', '双色球'], + 'dlt': ['推测管理', '大乐透'], + 'default': ['推测管理'] + }, + 'prize-statistics': { + 'ssq': ['奖金统计', '双色球'], + 'dlt': ['奖金统计', '大乐透'], + 'default': ['奖金统计'] + }, 'operation-history': ['操作历史'] } + if (path[2] === 'excel-import' && path[3]) { + return map['excel-import'][path[3]] || map['excel-import']['default'] + } + + if (path[2] === 'prediction' && path[3]) { + return map['prediction'][path[3]] || map['prediction']['default'] + } + + if (path[2] === 'prize-statistics' && path[3]) { + return map['prize-statistics'][path[3]] || map['prize-statistics']['default'] + } + return map[path[2]] || [path[2]] }) @@ -185,11 +260,11 @@ export default { switch (command) { case 'profile': console.log('跳转到个人信息页面') - router.push('/admin/profile') + router.push('/cpzsadmin/profile') break case 'settings': console.log('跳转到系统设置页面') - router.push('/admin/settings') + router.push('/cpzsadmin/settings') break case 'logout': console.log('执行注销操作') @@ -222,7 +297,7 @@ export default { // 确保跳转到登录页 setTimeout(() => { - router.push('/admin/login') + router.push('/cpzsadmin/login') }, 100) } catch (error) { console.error('注销过程中出错:', error) @@ -235,7 +310,7 @@ export default { }) setTimeout(() => { - router.push('/admin/login') + router.push('/cpzsadmin/login') }, 100) } }).catch(() => { @@ -265,14 +340,14 @@ export default { }) // 直接跳转到登录页 - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' }) } catch (error) { console.error('注销过程中出错:', error) // 强制清除状态并跳转 userStore.adminLogout() - window.location.href = '/admin/login' + window.location.href = '/cpzsadmin/login' } } @@ -353,7 +428,7 @@ export default { justify-content: center; } -.logo img { +.logo img.logo-icon { width: 32px; height: 32px; margin-right: 12px; diff --git a/lottery-app/src/views/dlt/DltTableAnalysis.vue b/lottery-app/src/views/dlt/DltTableAnalysis.vue new file mode 100644 index 0000000..1ea14ae --- /dev/null +++ b/lottery-app/src/views/dlt/DltTableAnalysis.vue @@ -0,0 +1,431 @@ + + + + + + diff --git a/lottery-app/src/views/dlt/HitAnalysis.vue b/lottery-app/src/views/dlt/HitAnalysis.vue new file mode 100644 index 0000000..abfbdd8 --- /dev/null +++ b/lottery-app/src/views/dlt/HitAnalysis.vue @@ -0,0 +1,831 @@ + + + + + diff --git a/lottery-app/src/views/dlt/Home.vue b/lottery-app/src/views/dlt/Home.vue new file mode 100644 index 0000000..1c8464a --- /dev/null +++ b/lottery-app/src/views/dlt/Home.vue @@ -0,0 +1,3830 @@ + + + + + diff --git a/lottery-app/src/views/dlt/LineAnalysis.vue b/lottery-app/src/views/dlt/LineAnalysis.vue new file mode 100644 index 0000000..536aadf --- /dev/null +++ b/lottery-app/src/views/dlt/LineAnalysis.vue @@ -0,0 +1,813 @@ + + + + + diff --git a/lottery-app/src/views/dlt/Lottery.vue b/lottery-app/src/views/dlt/Lottery.vue new file mode 100644 index 0000000..755e16e --- /dev/null +++ b/lottery-app/src/views/dlt/Lottery.vue @@ -0,0 +1,1369 @@ + + + + + + diff --git a/lottery-app/src/views/dlt/PredictRecords.vue b/lottery-app/src/views/dlt/PredictRecords.vue new file mode 100644 index 0000000..2b08ae6 --- /dev/null +++ b/lottery-app/src/views/dlt/PredictRecords.vue @@ -0,0 +1,1324 @@ + + + + + diff --git a/lottery-app/src/views/dlt/PrizeStatistics.vue b/lottery-app/src/views/dlt/PrizeStatistics.vue new file mode 100644 index 0000000..ad9f656 --- /dev/null +++ b/lottery-app/src/views/dlt/PrizeStatistics.vue @@ -0,0 +1,621 @@ + + + + + + diff --git a/lottery-app/src/views/dlt/SurfaceAnalysis.vue b/lottery-app/src/views/dlt/SurfaceAnalysis.vue new file mode 100644 index 0000000..560f1f6 --- /dev/null +++ b/lottery-app/src/views/dlt/SurfaceAnalysis.vue @@ -0,0 +1,913 @@ + + + + + diff --git a/lottery-app/src/views/dlt/TrendAnalysis.vue b/lottery-app/src/views/dlt/TrendAnalysis.vue new file mode 100644 index 0000000..c687427 --- /dev/null +++ b/lottery-app/src/views/dlt/TrendAnalysis.vue @@ -0,0 +1,817 @@ + + + + + diff --git a/lottery-app/src/views/dlt/UsageStats.vue b/lottery-app/src/views/dlt/UsageStats.vue new file mode 100644 index 0000000..eafb54b --- /dev/null +++ b/lottery-app/src/views/dlt/UsageStats.vue @@ -0,0 +1,719 @@ + + + + + diff --git a/lottery-app/src/views/jt/DltHome.vue b/lottery-app/src/views/jt/DltHome.vue new file mode 100644 index 0000000..bd3fe2d --- /dev/null +++ b/lottery-app/src/views/jt/DltHome.vue @@ -0,0 +1,4098 @@ + + + + + + \ No newline at end of file diff --git a/lottery-app/src/views/jt/SsqHome.vue b/lottery-app/src/views/jt/SsqHome.vue new file mode 100644 index 0000000..07fcfec --- /dev/null +++ b/lottery-app/src/views/jt/SsqHome.vue @@ -0,0 +1,3776 @@ + + + + + \ No newline at end of file diff --git a/lottery-app/src/views/ssq/HitAnalysis.vue b/lottery-app/src/views/ssq/HitAnalysis.vue new file mode 100644 index 0000000..ed881c3 --- /dev/null +++ b/lottery-app/src/views/ssq/HitAnalysis.vue @@ -0,0 +1,791 @@ + + + + + diff --git a/lottery-app/src/views/ssq/Home.vue b/lottery-app/src/views/ssq/Home.vue new file mode 100644 index 0000000..ed575a8 --- /dev/null +++ b/lottery-app/src/views/ssq/Home.vue @@ -0,0 +1,3728 @@ + + + + + \ No newline at end of file diff --git a/lottery-app/src/views/LineAnalysis.vue b/lottery-app/src/views/ssq/LineAnalysis.vue similarity index 96% rename from lottery-app/src/views/LineAnalysis.vue rename to lottery-app/src/views/ssq/LineAnalysis.vue index 304e7fe..c5543ae 100644 --- a/lottery-app/src/views/LineAnalysis.vue +++ b/lottery-app/src/views/ssq/LineAnalysis.vue @@ -3,13 +3,13 @@
- + 返回

接续性分析

-

球号接续性分析,计算接续系数值

+

球号接续分析,把握上依下托

@@ -335,6 +335,16 @@ export default { } }, methods: { + // 返回上一页 + goBack() { + // 获取当前彩票类型,优先使用路由参数,否则使用默认值 'ssq' + const lotteryType = this.$route.query.lotteryType || 'ssq' + this.$router.push({ + path: '/data-analysis', + query: { lotteryType: lotteryType } + }) + }, + selectAnalysisType(type) { this.analysisType = type this.masterBall = null @@ -468,7 +478,7 @@ export default { .header { text-align: center; - margin-bottom: 30px; + margin-bottom: 15px; position: relative; } @@ -481,7 +491,7 @@ export default { .header h2 { color: #2c3e50; font-size: 28px; - margin-bottom: 10px; + margin-bottom: 5px; font-weight: 700; } @@ -494,7 +504,7 @@ export default { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; - margin-bottom: 30px; + margin-bottom: 20px; max-width: 1000px; margin-left: auto; margin-right: auto; @@ -559,9 +569,6 @@ export default { color: #3498db; } -.analysis-container { - margin-bottom: 30px; -} .number-selection { padding: 20px 0; @@ -626,7 +633,7 @@ export default { .result-details { width: 100%; - max-width: 800px; + max-width: 850px; margin: 0 auto; display: flex; flex-direction: column; @@ -721,7 +728,7 @@ export default { .back-button-container { position: static; text-align: left; - margin-bottom: 15px; + margin-bottom: 0px; } .header { @@ -729,7 +736,8 @@ export default { } .analysis-buttons { - grid-template-columns: 1fr; + grid-template-columns: repeat(2, 1fr); + gap: 10px; } .ball-grid { diff --git a/lottery-app/src/views/ssq/Lottery.vue b/lottery-app/src/views/ssq/Lottery.vue new file mode 100644 index 0000000..83b628b --- /dev/null +++ b/lottery-app/src/views/ssq/Lottery.vue @@ -0,0 +1,1359 @@ + + + + + diff --git a/lottery-app/src/views/ssq/PrizeStatistics.vue b/lottery-app/src/views/ssq/PrizeStatistics.vue new file mode 100644 index 0000000..9bea2c5 --- /dev/null +++ b/lottery-app/src/views/ssq/PrizeStatistics.vue @@ -0,0 +1,624 @@ + + + + + \ No newline at end of file diff --git a/lottery-app/src/views/TableAnalysis.vue b/lottery-app/src/views/ssq/SsqTableAnalysis.vue similarity index 96% rename from lottery-app/src/views/TableAnalysis.vue rename to lottery-app/src/views/ssq/SsqTableAnalysis.vue index 4c81e74..cfb7c16 100644 --- a/lottery-app/src/views/TableAnalysis.vue +++ b/lottery-app/src/views/ssq/SsqTableAnalysis.vue @@ -8,8 +8,7 @@
-

表相性分析

-

基于最新100期开奖数据的表相性分析

+

表相查询

@@ -93,7 +92,7 @@ + + diff --git a/lottery-app/start.bat b/lottery-app/start.bat deleted file mode 100644 index 134dbe6..0000000 --- a/lottery-app/start.bat +++ /dev/null @@ -1,5 +0,0 @@ -@echo off -echo 正在启动双色球智能推测系统... -echo. -npm run dev -pause \ No newline at end of file diff --git a/lottery-app/vite.config.js b/lottery-app/vite.config.js index 93d42e9..5151513 100644 --- a/lottery-app/vite.config.js +++ b/lottery-app/vite.config.js @@ -16,5 +16,37 @@ export default defineConfig({ server: { host: '0.0.0.0', // 允许局域网访问 port: 5173 + }, + build: { + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true + } + }, + // 生成带hash的文件名,避免浏览器缓存 + rollupOptions: { + output: { + // 入口文件名 + entryFileNames: 'assets/js/[name].[hash].js', + // 代码分割文件名 + chunkFileNames: 'assets/js/[name].[hash].js', + // 资源文件名 + assetFileNames: (assetInfo) => { + // 根据文件类型分目录存放 + if (assetInfo.name.endsWith('.css')) { + return 'assets/css/[name].[hash].[ext]' + } + if (/\.(png|jpe?g|gif|svg|webp|ico)$/.test(assetInfo.name)) { + return 'assets/images/[name].[hash].[ext]' + } + if (/\.(woff2?|eot|ttf|otf)$/.test(assetInfo.name)) { + return 'assets/fonts/[name].[hash].[ext]' + } + return 'assets/[name].[hash].[ext]' + } + } + } } -}) +}) \ No newline at end of file diff --git a/lottery-app/xx.md b/lottery-app/xx.md new file mode 100644 index 0000000..15b4343 --- /dev/null +++ b/lottery-app/xx.md @@ -0,0 +1,155 @@ +现在大乐透也开放了,预测流程由双色球的5步(推测准备、推测首球、推测随球、推测蓝球、确认结果)变为6步(推测准备、推测前区首球、推测前区随球、推测后区首球、推测前区随球、确认结果)。推测准备(第一步)这一页和双色球的流程一样,只不过是上期红球号码,变为了上期前区号码,上期蓝球号码,变成了上次后区号码,仅10期开奖号码是通过GET http://localhost:8123/api/dlt-draw/recent-10-draw-ids这个接口获取,根据期号获取中奖号码的接口路径是GET http://localhost:8123/api/dlt-draw/draw/{{drawId}}/numbers,响应示例为 + +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": [ + 1, + 7, + 9, + 16, + 30, + 2, + 5 + ] +} + +然后点击继续,调用这个接口POST http://localhost:8123/api/dlt/ball-analysis/predict-first-ball,入参为 + +``` +public class FirstBallPredictionRequest { + private String level; high/middle/low:高位/中位/低位 + private List redBalls; 5个前区号码 + private List blueBalls; 2个后期号码 +} +``` + +响应示例为 + +``` +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": [ + 29, + 20, + 22, + 33, + 35, + 1, + 2, + 3, + 4, + 5, + 11, + 30 + ] +} +``` + +然后进入第二步,解析出12个球号作为推荐的12个前区球号, + +第二步的布局和双色球的的一样,先选择1个球号,在选择2个球号。然后拿上一步选择的位(high/middle/low)+ 3个号码(第二步选择的) + 5个号码(第一步的5个上期前区) + 2个号码(第一步的2个上期后区)去调用接口:POST http://localhost:8123/api/dlt/ball-analysis/predict-follower-ball + +入参为: + +``` +@Data +public class FollowerBallPredictionRequest { + private String level; + private List wellRegardedBalls; + private List previousFrontBalls; + private List previousBackBalls; +} +``` + +响应示例为 + +``` +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": [ + 1, + 2, + 3, + 4, + 5, + 6, + 35, + 29 + ] +} +``` + +然后进入第三步,解析出8个球号作为推荐的8个前区球号, + +先选择4个前区球号,再选择2个后区球号。然后拿第一步选择的位(high/middle/low)+ 5个号码(第二步选择的首球球号+这一步选择的4个球号)+ 5个号码(第一步选择5个上期前区球号) + 2个号码(第一步选择的2个上期后区球号)+ 2个号码(这一步选择的2个下期后区)。去调用接口:POST http://localhost:8123/api/dlt/ball-analysis/predict-back-ball + +入参为 + +``` +{ + "level": "high", + "nextFrontBalls": [1,2,3,4,5], + "previousFrontBalls": [1,2,3,4,5], + "previousBackBalls": [6,7], + "nextBackBalls": [8,9] +} +``` + +响应为 + +``` +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": [ + 1, + 2, + 9, + 7 + ] +} +``` + +然后进入第四步,解析出4个球号作为推荐的4个后区球号, + +选择一个推荐的后区首球。然后拿第一步选择的位(high/middle/low)+ 1个号码(这一步选择的一个后区首球)+ 5个号码(第二步选择的前区首球球号+第三步选择的4个前区随球)+ 5个号码(第一步选择5个上期前区球号) + 2个号码(第一步选择的2个上期后区球号)。去调用接口:POST http://localhost:8123/api/dlt/ball-analysis/predict-follow-back-ball + +入参 + +``` +{ + "level": "high", + "backFirstBall": 6, + "nextFrontBalls": [1,2,3,4,5], + "previousFrontBalls": [1,2,3,4,5], + "previousBackBalls": [6,7] +} +``` + +响应示例为 + +``` +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": [ + 1, + 3, + 5 + ] +} +``` + +然后进入第五步,解析出3个球号作为推荐的3个后区球号, + +选择一个推荐的后区随球,点击继续,进入下一步。 + +第六步:确认推测,这一步跟双色球的第五步一样。 \ No newline at end of file diff --git a/new.md b/new.md new file mode 100644 index 0000000..9d482f0 --- /dev/null +++ b/new.md @@ -0,0 +1,33 @@ +一、精彩猪手,不仅仅是一款彩票数据分析工具 +庖丁解牛,且看彩票数据姿态逻辑分析法 +游刃有余,且用彩票号组分段逻辑推测法 +彩票下注是最懵懂的彷徨和最无奈的焦虑 +但我们让“老虎吃天”,不再“无从下口” + +我们是彩票数据姿态逻辑分析法的原创者 +从婀娜多姿的彩票数据中模型化活跃性、接续性、组合性逻辑 +让彩票号球尽显动态灵性,在四方交错中不再冰冷、呆滞和虚幻 +想得到、看得清、抓得住,彩票数据之海任尔纵横捭阖 + +我们还是彩票号组分段推测逻辑的开路人 +正是基于彩票姿态逻辑数据集,彩票号组分段式推测逻辑方得跃然而出 +这是一股彩票下注方法论的清流,也是一种数据引领、逻辑导向的突破 +车到山前必有“路”,有路先有“开路人” + +我们致力于“精彩”本色,扮演好“猪手”角色,花独放,香尽散 +提供精品优质的彩票数据服务便是我们的本色 +做好彩民的彩票数据助手更是我们的角色 +我们用一首数据加逻辑的彩票推测神曲,唤醒和造福一方追梦的彩民 + +我们的宗旨:助力、再助力,直至彩民们梦想成真 +科技给出了腾飞的翅膀,我们却把它插上彩民的臂膀 +在彩票追梦大戏中,我们永远都甘当配角 +能够创造神奇的主角,只属于那些智慧和运气并驾的彩民 + +愿好花常开,祝好运常在 +二、公司微信客服二维码: + +三、公司微信公众号:精彩猪手 + +西安溢彩数智科技有限公司 +2026年2月 diff --git a/userController b/userController index 29ee56a..148aa22 100644 --- a/userController +++ b/userController @@ -1,119 +1,482 @@ -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 + /** + * 管理员获取所有双色球推测记录(支持分页和用户ID筛选) + * @param userId 用户ID(可选) + * @param current 当前页码,默认为1 + * @param pageSize 每页大小,默认为10 + * @param request HTTP请求 + * @return 分页的双色球预测记录 */ - @PostMapping("/register") - @Operation(summary = "用户注册", description = "用户注册接口") - public ApiResponse userRegister(@RequestBody UserRegisterRequest userRegisterRequest) { - if (userRegisterRequest == null) { - throw new BusinessException(ErrorCode.PARAMS_ERROR); + @GetMapping("/admin/all-records") + @Operation(summary = "管理员获取所有推测记录", description = "管理员获取所有双色球推测记录,支持分页和根据用户ID、中奖等级筛选") + public ApiResponse> getAllRecordsForAdmin( + @Parameter(description = "用户ID(可选),用于筛选指定用户的记录") + @RequestParam(value = "userId", required = false) Long userId, + @Parameter(description = "中奖等级(可选),例如:一等奖、二等奖、未中奖") + @RequestParam(value = "predictResult", required = false) String predictResult, + @Parameter(description = "当前页码,从1开始,默认为1") + @RequestParam(value = "current", defaultValue = "1") Integer current, + @Parameter(description = "每页大小,默认为10") + @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest request) { + + // 验证管理员权限 + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_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); + + if (!userService.isAdmin(loginUser)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限访问,仅管理员可用"); + } + + try { + log.info("管理员获取所有双色球推测记录,userId={},predictResult={},current={},pageSize={}", + userId, predictResult, current, pageSize); + + // 调用Service层方法 + PageResponse result = predictRecordService.getAllRecordsForAdmin(userId, predictResult, current, pageSize); + + log.info("管理员获取双色球推测记录完成,总记录数:{}", result.getTotal()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("管理员获取双色球推测记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取推测记录失败:" + e.getMessage()); } - long result = userService.userRegister(userAccount, userPassword, checkPassword); - return ResultUtils.success(result); } - /** - * 用户登录 - * - * @param userLoginRequest - * @param request - * @return - */ - @PostMapping("/login") - @Operation(summary = "用户登录", description = "用户登录接口") - public ApiResponse userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) { - if (userLoginRequest == null) { - throw new BusinessException(ErrorCode.PARAMS_ERROR); + GET http://localhost:8123/api/ball-analysis/admin/all-records? + userId={{$random.integer(100)}}& + predictResult={{$random.alphanumeric(8)}}& + current={{$random.integer(100)}}& + pageSize={{$random.integer(100)}} + + + 响应示例 + { + "code": 0, + "success": true, + "message": "操作成功", + "data": { + "records": [ + { + "id": 57, + "userId": 1, + "drawId": 2025096, + "drawDate": 1759766400000, + "redBall1": 25, + "redBall2": 17, + "redBall3": 18, + "redBall4": 15, + "redBall5": 21, + "redBall6": 29, + "blueBall": 11, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1759824079000, + "bonus": 0, + "type": "ssq" + }, + { + "id": 56, + "userId": 1, + "drawId": 2025096, + "drawDate": 1759766400000, + "redBall1": 13, + "redBall2": 18, + "redBall3": 2, + "redBall4": 4, + "redBall5": 5, + "redBall6": 1, + "blueBall": 10, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1759818889000, + "bonus": 0, + "type": "ssq" + }, + { + "id": 55, + "userId": 1, + "drawId": 2025096, + "drawDate": 1758384000000, + "redBall1": 22, + "redBall2": 33, + "redBall3": 24, + "redBall4": 25, + "redBall5": 20, + "redBall6": 15, + "blueBall": 14, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1758427874000, + "bonus": 0, + "type": "ssq" + }, + { + "id": 54, + "userId": 1, + "drawId": 2025096, + "drawDate": 1758384000000, + "redBall1": 23, + "redBall2": 25, + "redBall3": 33, + "redBall4": 29, + "redBall5": 24, + "redBall6": 20, + "blueBall": 15, + "predictStatus": "已中奖", + "predictResult": "六等奖", + "predictTime": 1758427335000, + "bonus": 5, + "type": "ssq" + }, + { + "id": 53, + "userId": 1, + "drawId": 2025096, + "drawDate": 1758384000000, + "redBall1": 22, + "redBall2": 27, + "redBall3": 29, + "redBall4": 24, + "redBall5": 30, + "redBall6": 25, + "blueBall": 14, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1758427265000, + "bonus": 0, + "type": "ssq" + }, + { + "id": 52, + "userId": 1, + "drawId": 2025096, + "drawDate": 1758384000000, + "redBall1": 18, + "redBall2": 27, + "redBall3": 32, + "redBall4": 30, + "redBall5": 29, + "redBall6": 28, + "blueBall": 15, + "predictStatus": "已中奖", + "predictResult": "六等奖", + "predictTime": 1758426942000, + "bonus": 5, + "type": "ssq" + }, + { + "id": 51, + "userId": 1, + "drawId": 2025096, + "drawDate": 1758384000000, + "redBall1": 26, + "redBall2": 18, + "redBall3": 23, + "redBall4": 19, + "redBall5": 20, + "redBall6": 25, + "blueBall": 14, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1758426099000, + "bonus": 0, + "type": "ssq" + }, + { + "id": 50, + "userId": 1, + "drawId": 2025096, + "drawDate": 1757520000000, + "redBall1": 22, + "redBall2": 18, + "redBall3": 19, + "redBall4": 23, + "redBall5": 20, + "redBall6": 15, + "blueBall": 13, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1757597385000, + "bonus": 0, + "type": "ssq" + }, + { + "id": 49, + "userId": 1, + "drawId": 2025096, + "drawDate": 1757260800000, + "redBall1": 14, + "redBall2": 20, + "redBall3": 18, + "redBall4": 27, + "redBall5": 33, + "redBall6": 13, + "blueBall": 15, + "predictStatus": "已中奖", + "predictResult": "六等奖", + "predictTime": 1757312366000, + "bonus": 5, + "type": "ssq" + }, + { + "id": 48, + "userId": 1, + "drawId": 2025095, + "drawDate": 1756828800000, + "redBall1": 17, + "redBall2": 22, + "redBall3": 27, + "redBall4": 16, + "redBall5": 6, + "redBall6": 7, + "blueBall": 4, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1756882722000, + "bonus": 0, + "type": "ssq" + } + ], + "total": 35, + "page": 1, + "size": 10, + "totalPages": 4, + "hasNext": true, + "hasPrevious": false + } +} + + @GetMapping("/admin/all-records") + @Operation(summary = "管理员获取所有推测记录", description = "管理员获取所有大乐透推测记录,支持分页和根据用户ID、中奖等级筛选") + public ApiResponse> getAllRecordsForAdmin( + @Parameter(description = "用户ID(可选),用于筛选指定用户的记录") + @RequestParam(value = "userId", required = false) Long userId, + @Parameter(description = "中奖等级(可选),例如:一等奖、二等奖、未中奖") + @RequestParam(value = "predictResult", required = false) String predictResult, + @Parameter(description = "当前页码,从1开始,默认为1") + @RequestParam(value = "current", defaultValue = "1") Integer current, + @Parameter(description = "每页大小,默认为10") + @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest request) { + + // 验证管理员权限 + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录"); } - String userAccount = userLoginRequest.getUserAccount(); - String userPassword = userLoginRequest.getUserPassword(); - if (StringUtils.isAnyBlank(userAccount, userPassword)) { - throw new BusinessException(ErrorCode.PARAMS_ERROR); + + if (!userService.isAdmin(loginUser)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限访问,仅管理员可用"); + } + + try { + log.info("管理员获取所有大乐透推测记录,userId={},predictResult={},current={},pageSize={}", + userId, predictResult, current, pageSize); + + // 调用Service层方法 + PageResponse result = dltPredictRecordService.getAllRecordsForAdmin(userId, predictResult, current, pageSize); + + log.info("管理员获取大乐透推测记录完成,总记录数:{}", result.getTotal()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("管理员获取大乐透推测记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取推测记录失败:" + e.getMessage()); } - 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 userLogout(HttpServletRequest request) { - if (request == null) { - throw new BusinessException(ErrorCode.PARAMS_ERROR); - } - boolean result = userService.userLogout(request); - return ResultUtils.success(result); - } + GET http://localhost:8123/api/dlt-predict/admin/all-records? + userId={{$random.integer(100)}}& + predictResult={{$random.alphanumeric(8)}}& + current={{$random.integer(100)}}& + pageSize={{$random.integer(100)}} - /** - * 获取当前登录用户 - * - * @param request - * @return - */ - @GetMapping("/get/login") - @Operation(summary = "获取当前登录用户", description = "获取当前登录用户信息") - public ApiResponse getLoginUser(HttpServletRequest request) { - User user = userService.getLoginUser(request); - UserVO userVO = new UserVO(); - BeanUtils.copyProperties(user, userVO); - return ResultUtils.success(userVO); - } - // endregion - -} \ No newline at end of file + { + "code": 0, + "success": true, + "message": "操作成功", + "data": { + "records": [ + { + "id": 23, + "userId": 1, + "drawId": 25117, + "drawDate": null, + "frontendBall1": 16, + "frontendBall2": 13, + "frontendBall3": 14, + "frontendBall4": 9, + "frontendBall5": 4, + "backendBall1": 2, + "backendBall2": 5, + "predictStatus": "待开奖", + "predictResult": "待开奖", + "predictTime": 1763967816000, + "bonus": 0 + }, + { + "id": 22, + "userId": 1, + "drawId": 25106, + "drawDate": null, + "frontendBall1": 10, + "frontendBall2": 32, + "frontendBall3": 33, + "frontendBall4": 35, + "frontendBall5": 1, + "backendBall1": 6, + "backendBall2": 8, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1759909486000, + "bonus": 0 + }, + { + "id": 21, + "userId": 1, + "drawId": 25106, + "drawDate": null, + "frontendBall1": 11, + "frontendBall2": 22, + "frontendBall3": 25, + "frontendBall4": 28, + "frontendBall5": 29, + "backendBall1": 11, + "backendBall2": 12, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1759909424000, + "bonus": 0 + }, + { + "id": 20, + "userId": 1, + "drawId": 25106, + "drawDate": null, + "frontendBall1": 5, + "frontendBall2": 22, + "frontendBall3": 28, + "frontendBall4": 30, + "frontendBall5": 35, + "backendBall1": 7, + "backendBall2": 3, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1759909344000, + "bonus": 0 + }, + { + "id": 19, + "userId": 1, + "drawId": 25109, + "drawDate": null, + "frontendBall1": 5, + "frontendBall2": 28, + "frontendBall3": 18, + "frontendBall4": 32, + "frontendBall5": 22, + "backendBall1": 9, + "backendBall2": 12, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1758511498000, + "bonus": 0 + }, + { + "id": 18, + "userId": 1, + "drawId": 25109, + "drawDate": null, + "frontendBall1": 3, + "frontendBall2": 11, + "frontendBall3": 23, + "frontendBall4": 29, + "frontendBall5": 34, + "backendBall1": 2, + "backendBall2": 7, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1758510538000, + "bonus": 0 + }, + { + "id": 17, + "userId": 1, + "drawId": 25109, + "drawDate": null, + "frontendBall1": 32, + "frontendBall2": 1, + "frontendBall3": 2, + "frontendBall4": 5, + "frontendBall5": 23, + "backendBall1": 3, + "backendBall2": 7, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1758510155000, + "bonus": 0 + }, + { + "id": 16, + "userId": 1, + "drawId": 25109, + "drawDate": null, + "frontendBall1": 19, + "frontendBall2": 12, + "frontendBall3": 2, + "frontendBall4": 33, + "frontendBall5": 22, + "backendBall1": 1, + "backendBall2": 7, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1758509942000, + "bonus": 0 + }, + { + "id": 15, + "userId": 1, + "drawId": 25109, + "drawDate": null, + "frontendBall1": 29, + "frontendBall2": 20, + "frontendBall3": 22, + "frontendBall4": 32, + "frontendBall5": 35, + "backendBall1": 10, + "backendBall2": 2, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1758509591000, + "bonus": 0 + }, + { + "id": 14, + "userId": 1, + "drawId": 25109, + "drawDate": null, + "frontendBall1": 14, + "frontendBall2": 28, + "frontendBall3": 30, + "frontendBall4": 2, + "frontendBall5": 20, + "backendBall1": 2, + "backendBall2": 3, + "predictStatus": "未中奖", + "predictResult": "未中奖", + "predictTime": 1758509292000, + "bonus": 0 + } + ], + "total": 14, + "page": 1, + "size": 10, + "totalPages": 2, + "hasNext": true, + "hasPrevious": false + } +} \ No newline at end of file diff --git a/前端多设备登录处理方案.md b/前端多设备登录处理方案.md new file mode 100644 index 0000000..d2f8b6c --- /dev/null +++ b/前端多设备登录处理方案.md @@ -0,0 +1,228 @@ +# 前端多设备登录处理方案 + +## 📋 后端机制概述 + +根据后端实现,采用 **"后登录踢掉先登录"** 的策略: +- 每次登录生成新的 UUID token 并存储到 Redis +- 新 token 会覆盖旧 token +- 每次请求验证时对比 Session token 和 Redis token +- 如果不一致,后端返回错误:`"账号在其他设备登录,当前会话已失效"` + +--- + +## 🎯 前端处理方案 + +### 1. **API 响应拦截器处理** ✅ + +已在 `src/api/index.js` 中实现: + +```javascript +// 防止重复提示的标志 +let isKickedOut = false + +// 响应拦截器 +api.interceptors.response.use( + response => { + const data = response.data + + if (data && data.success === false) { + const message = data.message || '' + + // 🔍 专门检测"其他设备登录"的情况 + if (message.includes('其他设备登录') || message.includes('当前会话已失效')) { + if (!isKickedOut) { + isKickedOut = true + + // 📢 显示友好提示 + ElMessage.warning({ + message: '您的账号已在其他设备登录,请重新登录', + duration: 3000, + showClose: true + }) + + // 🚪 清除状态并跳转 + if (window.location.pathname.startsWith('/cpzsadmin')) { + // 后台管理 + userStore.adminLogout() + setTimeout(() => { + window.location.href = '/cpzsadmin/login' + isKickedOut = false + }, 1500) + } else { + // 前台用户 + userStore.logout() + setTimeout(() => { + isKickedOut = false + }, 3000) + } + } + } + } + return data + } +) +``` + +### 2. **关键处理要点** + +#### ✅ 错误检测 +```javascript +// 匹配后端返回的错误消息 +if (message.includes('其他设备登录') || message.includes('当前会话已失效')) { + // 处理被踢出逻辑 +} +``` + +#### ✅ 防止重复提示 +```javascript +let isKickedOut = false // 全局标志位 + +if (!isKickedOut) { + isKickedOut = true + // 显示提示 + + // 延时后重置标志 + setTimeout(() => { + isKickedOut = false + }, 3000) +} +``` + +#### ✅ 友好提示 +```javascript +ElMessage.warning({ + message: '您的账号已在其他设备登录,请重新登录', + duration: 3000, + showClose: true +}) +``` + +#### ✅ 清除状态 +```javascript +// 清除 localStorage/sessionStorage 中的用户信息 +userStore.logout() // 或 userStore.adminLogout() +``` + +#### ✅ 自动跳转 +```javascript +// 延时跳转,让用户看到提示信息 +setTimeout(() => { + window.location.href = '/cpzsadmin/login' // 或前台登录页 +}, 1500) +``` + +--- + +## 🔄 完整流程 + +``` +用户A在设备1登录 + ├─ ✅ 正常使用 + │ +用户A在设备2登录 + ├─ 🔄 后端生成新token,覆盖Redis + │ +设备1发起请求 + ├─ 🔍 后端验证:Session token ≠ Redis token + ├─ ❌ 返回错误:"账号在其他设备登录,当前会话已失效" + │ +前端响应拦截器捕获 + ├─ 📢 显示提示:"您的账号已在其他设备登录" + ├─ 🗑️ 清除本地登录状态 + ├─ 🚪 跳转到登录页 + └─ ✅ 用户重新登录 +``` + +--- + +## 🛡️ 安全性保障 + +### 1. **即时生效** +- 新登录立即使旧会话失效 +- 无需等待定时任务或轮询 + +### 2. **状态同步** +- Redis 作为唯一的真实状态源 +- 前端 Session 只是临时凭证 + +### 3. **自动清理** +- Token 24小时自动过期 +- Session 失效自动清除 + +### 4. **用户体验** +- 明确的提示信息 +- 自动跳转登录页 +- 防止重复弹窗 + +--- + +## 📝 测试场景 + +### 测试步骤: +1. ✅ 在设备A(浏览器1)登录账号 +2. ✅ 在设备B(浏览器2)用同一账号登录 +3. ✅ 在设备A刷新页面或发起任何API请求 +4. ✅ 验证: + - 是否显示"账号在其他设备登录"提示 + - 是否自动清除登录状态 + - 是否自动跳转到登录页 + - 是否只提示一次(不重复弹窗) + +--- + +## 🎨 优化建议 + +### 1. **更友好的提示** +可以在提示中添加更多信息: +```javascript +ElMessage.warning({ + message: '您的账号于 [时间] 在 [设备/IP] 登录,当前会话已失效', + duration: 5000, + showClose: true +}) +``` + +### 2. **弹窗确认** +对于重要操作,可以使用弹窗代替消息提示: +```javascript +ElMessageBox.alert( + '您的账号已在其他设备登录,为保障账号安全,请重新登录', + '账号安全提醒', + { + confirmButtonText: '重新登录', + type: 'warning', + callback: () => { + // 跳转登录页 + } + } +) +``` + +### 3. **记录日志** +在前端记录被踢出的日志,便于问题排查: +```javascript +console.warn('[安全] 检测到账号在其他设备登录', { + time: new Date().toISOString(), + path: window.location.pathname, + userId: currentUser.id +}) +``` + +--- + +## 🔧 相关文件 + +- `src/api/index.js` - API 拦截器配置 +- `src/store/user.js` - 用户状态管理 +- `src/router/index.js` - 路由守卫(可选增强) + +--- + +## ✅ 实现完成 + +前端已完整实现多设备登录的处理机制,确保: +- ✅ 准确捕获被踢出的情况 +- ✅ 友好的用户提示 +- ✅ 自动清除状态 +- ✅ 防止重复提示 +- ✅ 自动跳转登录页 diff --git a/多设备登录防护实现文档.md b/多设备登录防护实现文档.md new file mode 100644 index 0000000..f00aaa1 --- /dev/null +++ b/多设备登录防护实现文档.md @@ -0,0 +1,237 @@ +# 多设备登录防护实现文档 + +## 功能概述 + +实现 **"后登录踢掉先登录"** 的单设备登录机制,确保同一账号同一时间只能在一个终端登录。 + +--- + +## 后端实现 + +### 核心机制:Redis + Token 验证 + +#### 1. 登录时处理 + +```java +// 位置:UserServiceImpl.java 第130-139行 +String token = UUID.randomUUID().toString().replace("-", ""); +String redisKey = UserConstant.REDIS_USER_LOGIN_TOKEN_PREFIX + user.getId(); + +// 存储到Redis(新token覆盖旧token) +redisTemplate.opsForValue().set(redisKey, token, 86400, TimeUnit.SECONDS); + +// 存储到Session +request.getSession().setAttribute(UserConstant.USER_LOGIN_TOKEN, token); +request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user); +``` + +**关键点:** +- Redis Key 格式:`user:login:token:{userId}` +- 新token覆盖旧token,实现单设备登录 +- Token有效期:24小时 + +#### 2. 请求验证 + +```java +// 位置:UserServiceImpl.java 第160-178行 +String sessionToken = (String) request.getSession() + .getAttribute(UserConstant.USER_LOGIN_TOKEN); +String redisToken = (String) redisTemplate.opsForValue().get(redisKey); + +// 对比token +if (redisToken == null || !redisToken.equals(sessionToken)) { + throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, + "账号在其他设备登录,当前会话已失效"); +} +``` + +**验证流程:** +- Session token ≠ Redis token → 被踢出 +- Session token = Redis token → 验证通过 + +--- + +## 前端实现 + +### 1. API 响应拦截器 + +**位置:** `src/api/index.js` + +```javascript +// 防止重复提示的标志 +let isKickedOut = false + +api.interceptors.response.use( + response => { + const data = response.data + const message = data.message || '' + + // 检测"其他设备登录"错误 + if (message.includes('其他设备登录') || message.includes('当前会话已失效')) { + if (!isKickedOut) { + isKickedOut = true + + // 显示提示 + ElMessage.warning('您的账号已在其他设备登录,请重新登录') + + // 清除状态并跳转 + import('../store/user.js').then(({ userStore }) => { + userStore.logout(true) // 传入true标记为被踢出 + setTimeout(() => { + window.location.href = '/login' + isKickedOut = false + }, 1500) + }) + } + } + return data + } +) +``` + +### 2. Store 状态管理 + +**位置:** `src/store/user.js` + +```javascript +export const userStore = reactive({ + user: null, + isLoggedIn: false, + isKickedOut: false, // 标记是否被踢出 + lastCheckTime: null, // 最后检查时间 + + // 增强的登出方法 + logout(isKickedOut = false) { + if (isKickedOut) { + this.isKickedOut = true + console.log('[安全] 账号在其他设备登录') + } + this.user = null + this.isLoggedIn = false + localStorage.clear() + }, + + // 主动检查登录状态 + async checkLoginStatus() { + if (!this.isLoggedIn) return { valid: false } + + // 10秒内只检查一次 + const now = Date.now() + if (this.lastCheckTime && (now - this.lastCheckTime) < 10000) { + return { valid: true, reason: 'recently_checked' } + } + + const response = await lotteryApi.getLoginUser() + if (response.success) { + this.lastCheckTime = now + return { valid: true } + } else { + this.logout(true) + return { valid: false, reason: 'kicked_out' } + } + } +}) +``` + +--- + +## 完整流程 + +### 场景:用户A先登录,然后在另一设备登录 + +``` +设备1登录 + ├─ Redis: user:login:token:123 = token1 + └─ Session: token1 ✅可用 + +设备2登录(新登录) + ├─ Redis: user:login:token:123 = token2 ⚠️覆盖token1 + └─ Session: token2 ✅可用 + +设备1再请求 + ├─ Session token: token1 + ├─ Redis token: token2 + ├─ token1 ≠ token2 ❌不匹配 + ├─ 后端返回错误:"账号在其他设备登录" + ├─ 前端拦截器捕获 + ├─ 显示提示 + ├─ 清除状态 + └─ 跳转登录页 +``` + +--- + +## 使用示例 + +### 在组件中主动检查 + +```javascript +async function handlePayment() { + const status = await userStore.checkLoginStatus() + if (!status.valid) { + ElMessage.warning('您的账号已在其他设备登录') + return + } + // 继续操作 +} +``` + +### 显示被踢出提示 + +```javascript +if (userStore.isKickedOut) { + ElMessage.warning('您的账号已在其他设备登录') + userStore.resetKickedOutStatus() +} +``` + +--- + +## 测试验证 + +1. ✅ 浏览器A登录 → 正常使用 +2. ✅ 浏览器B登录(同账号) +3. ✅ 浏览器A刷新 → 显示提示 → 跳转登录页 +4. ✅ 只提示一次,不重复弹窗 + +--- + +## 实现检查清单 + +### 后端 +- ✅ 登录时生成token并存储Redis +- ✅ 新token覆盖旧token +- ✅ 请求验证时对比token +- ✅ token不一致返回特定错误 +- ✅ token自动过期(24小时) + +### 前端 +- ✅ 响应拦截器捕获错误 +- ✅ 显示友好提示 +- ✅ 调用logout(true)标记状态 +- ✅ 自动跳转登录页 +- ✅ 防止重复提示 +- ✅ Store增加isKickedOut状态 +- ✅ Store增加checkLoginStatus()方法 +- ✅ 防频繁检查(10秒缓存) + +--- + +## 关键文件 + +- **后端:** `UserServiceImpl.java` +- **前端API:** `src/api/index.js` +- **前端Store:** `src/store/user.js` + +--- + +## 总结 + +实现了完整的多设备登录防护: + +✅ **后端**:Redis + Token 三层验证 +✅ **前端**:被动响应 + 主动检查 +✅ **体验**:友好提示、自动跳转 +✅ **安全**:实时生效、自动过期 + +这套机制确保同一账号只能在一个设备登录,有效防止账号被盗用! diff --git a/头像上传功能实现说明.md b/头像上传功能实现说明.md new file mode 100644 index 0000000..5cb215e --- /dev/null +++ b/头像上传功能实现说明.md @@ -0,0 +1,241 @@ +# 头像上传功能实现说明 + +## 完成时间 +2026-01-27 + +## 功能概述 +将Profile页面的头像上传功能从Base64编码改为调用后端文件上传接口,获取云存储URL。 + +## 实现细节 + +### 1. API接口配置 +在 `lottery-app/src/api/index.js` 中已添加文件上传接口: + +```javascript +// 上传文件 +uploadFile(file) { + const formData = new FormData() + formData.append('file', file) + return api.post('/file/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} +``` + +**接口地址**: `POST /api/file/upload` + +**请求格式**: `multipart/form-data` + +**响应格式**: +```json +{ + "code": 0, + "data": { + "fileName": "a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg", + "fileUrl": "https://yicaishuzhi-1326058838.cos.ap-beijing.myqcloud.com/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg", + "originalFilename": "avatar.jpg" + }, + "message": "ok" +} +``` + +### 2. 数据状态管理 +在 `lottery-app/src/views/Profile.vue` 的 data 中添加了上传状态标志: + +```javascript +data() { + return { + // ... 其他属性 + uploadingAvatar: false, // 新增:头像上传状态 + // ... + } +} +``` + +### 3. 头像上传方法实现 +修改了 `handleAvatarChange` 方法: + +**主要改动**: +- 从读取文件为Base64改为调用 `lotteryApi.uploadFile(file)` 接口 +- 添加上传状态管理 (`uploadingAvatar`) +- 从响应中获取 `fileUrl` 并设置到 `editForm.userAvatar` +- 添加完整的错误处理和用户提示 +- 清空文件输入框,允许重新选择同一文件 + +**代码实现**: +```javascript +async handleAvatarChange(event) { + const file = event.target.files[0] + if (!file) return + + // 验证文件类型 + if (!file.type.startsWith('image/')) { + this.$toast.error('请选择图片文件') + return + } + + // 验证文件大小(限制2MB) + if (file.size > 2 * 1024 * 1024) { + this.$toast.error('图片大小不能超过2MB') + return + } + + this.uploadingAvatar = true + try { + // 显示上传提示 + this.$toast.info('正在上传头像...') + + // 调用上传接口 + const response = await lotteryApi.uploadFile(file) + + if (response.code === 0 && response.data && response.data.fileUrl) { + // 上传成功,设置头像URL + this.editForm.userAvatar = response.data.fileUrl + this.$toast.success('头像上传成功!') + } else { + this.$toast.error(response.message || '头像上传失败') + } + } catch (error) { + console.error('头像上传失败:', error) + this.$toast.error('头像上传失败,请稍后重试') + } finally { + this.uploadingAvatar = false + // 清空文件输入框,允许重新选择同一文件 + event.target.value = '' + } +} +``` + +### 4. UI交互优化 + +#### 4.1 头像预览区域 +添加了上传中的遮罩层和加载动画: + +```vue +
+ 头像 +
{{ editForm.userName ? editForm.userName.charAt(0) : 'U' }}
+
+
+
+
+``` + +#### 4.2 上传按钮 +添加了禁用状态和文字变化: + +```vue + +``` + +#### 4.3 CSS样式 +添加了以下样式: + +```css +/* 禁用状态 */ +.upload-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + background: #f5f5f5; + border-color: #e0e6ed; + color: #999; +} + +/* 上传遮罩层 */ +.upload-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +/* 加载动画 */ +.upload-spinner { + width: 24px; + height: 24px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} +``` + +## 功能特性 + +### ✅ 已实现 +1. **文件验证** + - 验证文件类型(仅允许图片) + - 验证文件大小(限制2MB) + +2. **上传流程** + - 调用后端上传接口 + - 获取云存储URL + - 更新头像预览 + +3. **用户反馈** + - 上传中提示 + - 上传成功提示 + - 上传失败提示 + - 按钮禁用状态 + - 加载动画 + +4. **错误处理** + - 网络错误处理 + - 接口错误处理 + - 文件验证错误处理 + +5. **用户体验** + - 上传中显示遮罩和加载动画 + - 按钮文字动态变化 + - 清空文件输入框,允许重新选择 + +## 测试建议 + +### 1. 功能测试 +- [ ] 选择图片文件,验证上传成功 +- [ ] 选择非图片文件,验证错误提示 +- [ ] 选择超过2MB的图片,验证错误提示 +- [ ] 上传过程中验证按钮禁用 +- [ ] 上传成功后验证头像更新 +- [ ] 保存用户信息后验证头像持久化 + +### 2. UI测试 +- [ ] 验证上传中的遮罩层显示 +- [ ] 验证加载动画正常运行 +- [ ] 验证按钮文字变化 +- [ ] 验证按钮禁用样式 + +### 3. 错误场景测试 +- [ ] 网络断开时上传 +- [ ] 后端接口返回错误 +- [ ] 上传超时处理 + +## 相关文件 +- `lottery-app/src/views/Profile.vue` - 主要实现文件 +- `lottery-app/src/api/index.js` - API接口定义 + +## 开发服务器 +当前运行在: http://localhost:5174/ + +## 注意事项 +1. 确保后端 `/api/file/upload` 接口正常运行 +2. 确保云存储服务配置正确 +3. 上传的图片URL需要支持跨域访问 +4. 建议在生产环境中添加图片压缩功能 diff --git a/新建文本文档 (2).txt b/新建文本文档 (2).txt index 043dd97..9df006e 100644 --- a/新建文本文档 (2).txt +++ b/新建文本文档 (2).txt @@ -1,715 +1,79 @@ -配置流程 -步骤一:发布智能体或 AI 应用 -在智能体或 AI 应用的发布页面,选择 Chat SDK,并单击发布。发布的详细流程可参考: -发布应用为 Chat SDK -发布智能体到 Chat SDK -步骤二:获取安装代码 -进入发布页面复制 SDK 安装代码。 - -步骤三:安装 SDK -你可以直接在页面中通过 script 标签的形式加载 Chat SDK 的 js 代码,将步骤二中复制好的安装代码粘贴到网页的 区域中即可。 -步骤二中复制好的安装代码示例如下: - -示例代码中的版本号 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 参数实现会话隔离,具体请参见如何实现会话隔离。 +欢迎使用《精彩猪手》数据服务! +《精彩猪手》是一款彩票数据姿态逻辑分析推测工具,是各位会员朋友的超级数据助理。在开始使用我们的服务之前,请会员仔细阅读并充分理解本《会员服务协议》(以下简称 “本协议”)的全部内容。 +本协议是会员与我们(西安溢彩数智科技有限公司)之间关于使用本服务的法律协议。一旦会员使用本服务,即表示会员已同意接受本协议的约束。如果会员不同意本协议的任何条款,请不要使用本服务。 +一、定义 +1.本服务:指我们通过网站、应用程序或其他相关平台向会员提供的《精彩猪手》数据服务,包括但不限于彩票开奖信息查询、姿态逻辑数据分析报告、个性化推导推测辅助,以及会员行为统计、经验技巧交流等。 +2.会员:指承认本协议,接受本服务的自然人。具体包括一般会员和付费会员。 +3.个人信息:指以电子或者其他方式记录的与已识别或者可识别的自然人有关的各种信息,不包括匿名化处理后的信息。 +4.知识产权:包括但不限于著作权、专利权、商标权、商业秘密等。 +二、服务内容及门槛 +1.我们将尽力为会员提供稳定、高效的服务。服务内容包括但不限于: +1.1既定彩票开奖信息查询浏览; +1.2既定彩票开奖数据姿态逻辑分析及报告; +1.3按期次更新的号球动态数据,以及通过“本期”已出的开奖号球,进行“下期”开奖号球的个性化推测逻辑辅助; +1.4根据首球与随球的关联线索,启发复式/胆拖的投注参考; +1.5推测行为统计、分析、备份; +1.6进行规律总结、技巧探索、经验交流。 +2.我们会根据实际情况对服务内容进行调整、更新。如有重大变更,我们将通过企业公众号、企业微信、短信等方式通知会员,需要会员及时接收。如会员在服务内容发生变更后继续使用本服务,视为会员接受变更后的协议内容。 +3.会员理解并同意,使用本服务可能需要会员具备相应的设备、软件、网络环境和一定的专业知识,相关费用由会员自行承担。 +三、会员账号 +1.会员可以通过免费注册的方式获取本服务账号。在注册过程中,会员需提供真实、准确、完整的信息,并确保信息的及时更新。 +2.会员应妥善保管账号及密码,不将账号出借、转让、赠予或共享给他人使用。因会员自身原因导致账号泄露或被他人使用的后果,由会员自行承担。 +3.若会员发现账号存在异常或安全问题,请立即通知我们,我们将尽力协助处理,但对于非因我们原因导致的损失,我们不承担责任。 +4.会员在使用账号过程中应遵守法律法规及本协议约定,不得利用账号从事违法、违规,或损害他人权益的行为,否则我们有权暂停或终止会员的账号使用,由此造成的损失由会员自行承担。 +四、会员权利与义务 +1.权利 +1.1有权在遵守本协议的前提下,按照我们提供的方式使用服务。 +1.2对我们提供的服务质量有权提出合理的意见和建议。 +2.义务 +2.1遵守国家法律法规及互联网相关规定,不得利用本服务从事违法犯罪活动。 +2.2不得干扰、破坏本服务的正常运行,不得对服务进行反向工程、反编译、反汇编等行为。 +2.3不得发布、传播任何侵犯他人知识产权、隐私或其他合法权益的信息。 +2.4不得恶意注册账号、发送垃圾信息或进行其他滥用服务的行为。 +2.5如因会员的行为导致我们或第三方遭受损失,责任方会员应承担相应的赔偿责任。 +五、隐私政策 +1.我们十分重视对会员个人信息保护,将谨慎收集、安全存储、妥善使用会员的个人信息。 +2.会员同意我们为提供服务、改进服务,包括遵守法律法规的需要,对会员的个人信息进行合理的调用,过程中我们将采取合理措施确保信息安全。 +六、知识产权 +我们对本服务及相关内容(包括但不限于软件、数据、文字、图片、音频、视频等)享有知识产权。未经我们书面许可,会员不得擅自复制、改编,或创造基于本服务的衍生品。 -扣子应用配置 -调用 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 及以上版本支持该参数。 +七、责任限制与免责 +1.本服务所提供的各项数据分析报告,均为根据彩票历史数据、统计学、数学和数据科学原理,通过计算机技术进行建模分析而得出。我们仅对数据的及时性、客观性承担责任,但对任何经过会员主观干预之后而产生的行为结果,无法承担相应的后果。 +2.会员应当理解并知晓,彩票本身就是概率游戏,彩票开奖也是纯粹的随机事件。因此,本服务仅具备参考和辅助功效,会员必须对自己的判断和选择结果承担最终责任。 +3.我们将尽力确保服务的正常运行,但由于互联网的复杂性和不确定性,可能会出现服务中断、延迟、错误等情况。对于因不可抗力、技术故障、网络攻击等不可预见、不可避免的原因导致的服务问题,我们不承担责任。 +4.我们对会员通过本服务获取的第三方信息的准确性、完整性、可靠性不承担保证责任,会员应自行判断并承担使用风险。 +5.在任何情况下,我们对会员因使用本服务而产生的直接、间接、偶然、特殊或后果性损失(包括但不限于数据丢失、业务中断、收入和利润损失等),无论基于合同、侵权或其他理论,均不承担超过会员实际支付的服务费用的赔偿责任。 +八、收费及其规则 +1.本服务采用会员制运营模式。所有会员分为普通会员与VIP会员两种类型。 +2.本服务对普通会员仅提供一般的基础数据服务。 +3.本服务对VIP会员,在提供一般基础数据服务的基础上,还提供个性化的智推、精推特色辅助服务。 +4.本服务对VIP会员实行付费服务。付费形式分为包月付费和包年付费两种。 +5.包月付费每月10元,不足一个月时,按一个月计算。 +6.包年付费每年100元。若因会员原因中途提出终止协议,需要发生退款时,所退款项均按包月付费标准进行折算。 +7.出现业务退款情形时,所退款项只限原路退回付款账号。 +8.所有会员在服务有效期内使用本服务额定内容的时段和频次均不受限。 +9.会员可以根据自己的实际需要,随时选择成为VIP会员。 +九、协议变更与终止 +1.我们有权根据法律法规变化、业务发展需要等对本协议进行变更。变更后的协议将在相关平台以显著方式公布,自公布之日起生效,公布后即视为已通知会员。若会员在协议变更后继续使用服务,视为会员接受变更后的协议;若会员不同意变更,有权停止使用本服务。 +2.出现以下情况下,我们有权终止本协议及停止会员继续使用服务: +2.1会员严重违反本协议约定。 +2.2法律法规要求我们终止服务。 +2.3因不可抗力等不可预见、不可避免的原因导致服务无法继续提供。 +2.4协议终止后,我们有权根据法律法规要求,对会员的相关信息进行封存处理: +十、争议解决 +1.本协议的签订、履行、解释及争议解决均适用 《中华人民共和国民法典》。 +2.如双方在本协议履行过程中发生争议,应首先通过友好协商解决;协商不成的,任何一方均有权向有管辖权的人民法院提起诉讼。 +十一、其他条款 +1.本协议构成会员与我们之间关于本服务的完整协议。未经我们书面同意,会员不得转让本协议项下的任何权利、利益和义务。 +2.本协议各条款的标题仅为方便阅读而设,不影响条款的具体含义及解释。 +3.若本协议任何条款被认定为无效或不可执行,不影响其他条款的效力及执行。 +4.我们未行使或执行本协议任何权利或条款,不构成对该权利或条款的放弃。 +5.本协议自会员成功注册《精彩猪手》服务相关账号之日即刻生效。 +6.任何有关本协议项下服务的问题,会员可通过本公司企业微信进行咨询。 -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 方法销毁客户端。 - - -属性配置 -智能体或应用配置 -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, - }, - }, -}); - -不展示悬浮球时,你可以通过以下方式显示聊天框或隐藏聊天框。 - - - - - -底部文案 -聊天框底部会展示对话服务的提供方信息,默认为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 调用智能体的代码示例。 - - - - - - -

Hello World

-
- - - - - -相关文档 \ No newline at end of file +西安溢彩数智科技有限公司 +2025年12月31日 \ No newline at end of file diff --git a/用户信息管理API使用说明.md b/用户信息管理API使用说明.md new file mode 100644 index 0000000..f838f29 --- /dev/null +++ b/用户信息管理API使用说明.md @@ -0,0 +1,685 @@ +# 用户信息管理API使用说明 + +## 接口概述 + +本文档提供用户信息获取和编辑的接口说明,包括会员个人信息的查询和更新功能。 + +## 基础信息 + +- **Base URL**: `http://localhost:8123/api` +- **Content-Type**: `application/json` +- **认证方式**: Session(需要先登录) + +--- + +## 1. 获取当前登录用户信息 + +### 接口地址 +``` +GET /user/get/login +``` + +### 接口说明 +获取当前登录用户的详细信息,包括会员ID、等级、昵称、手机号码、注册日期、套餐类别、费用到期时间、性别、所在省市、彩票偏好、获客渠道等。 + +### 请求参数 +无需参数(通过Session获取当前登录用户) + +### 请求示例(使用 curl) +```bash +curl -X GET "http://localhost:8123/api/user/get/login" \ + -H "Content-Type: application/json" \ + -b "JSESSIONID=your_session_id" +``` + +### 请求示例(使用 JavaScript) +```javascript +fetch('http://localhost:8123/api/user/get/login', { + method: 'GET', + credentials: 'include', // 携带cookie + headers: { + 'Content-Type': 'application/json' + } +}) +.then(response => response.json()) +.then(data => { + console.log('用户信息:', data); +}) +.catch(error => { + console.error('获取失败:', error); +}); +``` + +### 响应示例(成功) +```json +{ + "code": 0, + "data": { + "id": "1234567890123456789", + "userName": "张三", + "userAccount": "zhangsan", + "phone": "13800138000", + "userAvatar": "https://example.com/avatar.jpg", + "gender": 1, + "userRole": "user", + "isVip": 1, + "vipExpire": "2026-12-31T23:59:59", + "vipType": "年度会员", + "location": "北京市", + "preference": "双色球", + "channel": "微信推广", + "status": 0, + "createTime": "2025-01-01T10:00:00", + "updateTime": "2026-01-26T15:30:00" + }, + "message": "ok" +} +``` + +### 响应字段说明 + +| 字段名 | 类型 | 说明 | 备注 | +|--------|------|------|------| +| code | Integer | 状态码 | 0表示成功 | +| data | Object | 用户信息对象 | | +| data.id | String | 会员ID | 系统自动生成,不可修改 | +| data.userName | String | 昵称 | 可修改 | +| data.userAccount | String | 账号 | 系统收集,不可修改 | +| data.phone | String | 手机号码 | 可修改 | +| data.userAvatar | String | 头像URL | 可修改 | +| data.gender | Integer | 性别 | 0-女,1-男,2-未知,可修改 | +| data.userRole | String | 用户角色 | user/admin,系统收集 | +| data.isVip | Integer | 是否会员 | 0-非会员,1-会员,系统收集 | +| data.vipExpire | String | 费用到期时间 | ISO 8601格式,系统收集 | +| data.vipType | String | 套餐类别 | 体验会员/月度会员/年度会员,系统收集 | +| data.location | String | 所在省市 | 可由用户填写或客服填写 | +| data.preference | String | 彩票偏好 | 双色球/大乐透等,可由用户填写或客服填写 | +| data.channel | String | 获客渠道 | 可由用户填写或客服填写 | +| data.status | Integer | 状态 | 0-正常,1-封禁,系统收集 | +| data.createTime | String | 注册日期 | ISO 8601格式,系统收集 | +| data.updateTime | String | 更新时间 | ISO 8601格式 | +| message | String | 响应消息 | | + +### 字段权限说明 + +**系统收集字段(黄色标记,会员可查看但不可修改):** +- 会员ID (id) +- 等级 (userRole) +- 注册日期 (createTime) +- 套餐类别 (vipType) +- 费用到期时间 (vipExpire) + +**用户可填写或客服可填写字段(其它四项):** +- 性别 (gender) +- 所在省市 (location) +- 彩票偏好 (preference) +- 获客渠道 (channel) + +### 错误响应示例 +```json +{ + "code": 40100, + "data": null, + "message": "未登录" +} +``` + +--- + +## 2. 编辑用户信息 + +### 接口地址 +``` +POST /user/update +``` + +### 接口说明 +更新当前登录用户的个人信息。用户只能修改自己的信息,管理员可以修改任何用户的信息。 + +**注意:** +- 系统收集的字段(会员ID、等级、注册日期、套餐类别、费用到期时间)不可通过此接口修改 +- 用户可以修改:昵称、手机号码、头像、性别、所在省市、彩票偏好、获客渠道 + +### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 用户ID(当前登录用户的ID) | +| userName | String | 否 | 昵称 | +| phone | String | 否 | 手机号码(11位) | +| userAvatar | String | 否 | 头像URL | +| gender | Integer | 否 | 性别(0-女,1-男,2-未知) | +| location | String | 否 | 所在省市 | +| preference | String | 否 | 彩票偏好(如:双色球、大乐透) | +| channel | String | 否 | 获客渠道 | + +### 请求示例(使用 curl) +```bash +curl -X POST "http://localhost:8123/api/user/update" \ + -H "Content-Type: application/json" \ + -b "JSESSIONID=your_session_id" \ + -d '{ + "id": 1234567890123456789, + "userName": "李四", + "phone": "13900139000", + "gender": 1, + "location": "上海市", + "preference": "大乐透", + "channel": "朋友推荐" + }' +``` + +### 请求示例(使用 JavaScript) +```javascript +const updateUserInfo = async (userInfo) => { + const response = await fetch('http://localhost:8123/api/user/update', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(userInfo) + }); + + const result = await response.json(); + return result; +}; + +// 使用示例 +updateUserInfo({ + id: 1234567890123456789, + userName: "李四", + phone: "13900139000", + gender: 1, + location: "上海市", + preference: "大乐透", + channel: "朋友推荐" +}).then(data => { + console.log('更新成功:', data); +}).catch(error => { + console.error('更新失败:', error); +}); +``` + +### 请求示例(使用 Vue 3) +```vue + + + +``` + +### 响应示例(成功) +```json +{ + "code": 0, + "data": true, + "message": "ok" +} +``` + +### 响应字段说明 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| code | Integer | 状态码,0表示成功 | +| data | Boolean | 是否更新成功 | +| message | String | 响应消息 | + +### 错误响应示例 + +#### 1. 未登录 +```json +{ + "code": 40100, + "data": null, + "message": "未登录" +} +``` + +#### 2. 参数错误 +```json +{ + "code": 40000, + "data": null, + "message": "手机号格式不正确" +} +``` + +#### 3. 无权限 +```json +{ + "code": 40101, + "data": null, + "message": "无权限修改其他用户信息" +} +``` + +--- + +## 3. 根据ID获取用户信息(管理员) + +### 接口地址 +``` +GET /user/get?id={userId} +``` + +### 接口说明 +管理员根据用户ID获取指定用户的详细信息。 + +### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 用户ID | + +### 请求示例 +```bash +curl -X GET "http://localhost:8123/api/user/get?id=1234567890123456789" \ + -H "Content-Type: application/json" \ + -b "JSESSIONID=your_session_id" +``` + +### 响应示例 +与"获取当前登录用户信息"接口响应格式相同。 + +--- + +## 错误码说明 + +| 错误码 | 说明 | +|--------|------| +| 0 | 成功 | +| 40000 | 参数错误 | +| 40100 | 未登录 | +| 40101 | 无权限 | +| 40400 | 用户不存在 | +| 50000 | 系统错误 | + +--- + +## 字段约束说明 + +### 1. 手机号码 (phone) +- 长度:11位 +- 格式:纯数字 +- 示例:13800138000 + +### 2. 性别 (gender) +- 0:女 +- 1:男 +- 2:未知 + +### 3. 所在省市 (location) +- 最大长度:100字符 +- 格式:省份+城市,如"北京市"、"广东省广州市" +- 可为空 + +### 4. 彩票偏好 (preference) +- 最大长度:100字符 +- 常见值:双色球、大乐透、双色球和大乐透 +- 可为空 + +### 5. 获客渠道 (channel) +- 最大长度:100字符 +- 常见值:微信推广、朋友推荐、搜索引擎、广告投放等 +- 可为空 + +--- + +## 完整的前端表单示例(HTML + JavaScript) + +```html + + + + + + 用户信息编辑 + + + +
+

会员个人信息

+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +

注:黄色标记为系统收集,会员可查看但不可修改

+ +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +

注:其它四项可由用户填写或客服填写

+ + + +
+ + + + +``` + +--- + +## 注意事项 + +1. **权限控制** + - 用户只能查看和修改自己的信息 + - 管理员可以查看和修改任何用户的信息 + - 系统收集的字段(会员ID、等级、注册日期、套餐类别、费用到期时间)任何人都不能通过此接口修改 + +2. **数据验证** + - 手机号必须是11位数字 + - 性别只能是0、1、2 + - 所有字符串字段都有长度限制 + +3. **Session管理** + - 所有接口都需要先登录 + - 请求时需要携带Session Cookie + - 前端使用`credentials: 'include'`来携带Cookie + +4. **错误处理** + - 前端应该处理所有可能的错误情况 + - 显示友好的错误提示给用户 + +5. **数据更新** + - 只需要传递需要更新的字段 + - 未传递的字段不会被更新 + - 建议每次都传递完整的可编辑字段数据