Initial public release
25
.eslintrc.js
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
es2022: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'warn',
|
||||
'no-debugger': 'warn',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'no-unused-vars': 'warn'
|
||||
},
|
||||
globals: {
|
||||
globalThis: 'readonly'
|
||||
}
|
||||
};
|
||||
64
.gitignore
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Production builds
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Local cache
|
||||
.cache/
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
70
CHANGELOG.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 更新日志
|
||||
|
||||
## [2.0.0] - 2024-01-20
|
||||
|
||||
### 🎉 重大更新 - Token管理系统重构
|
||||
|
||||
#### ✨ 新增功能
|
||||
- **Base64 Token导入**: 支持多种格式的Base64编码Token解析
|
||||
- **可视化Token管理**: 名称-Token列表形式管理多个游戏角色
|
||||
- **WebSocket连接管理**: 自动建立和监控WebSocket连接状态
|
||||
- **批量操作功能**: 导入/导出、清理过期Token等批量功能
|
||||
- **响应式Token界面**: 完美适配桌面和移动设备的Token管理界面
|
||||
|
||||
#### 🗑️ 移除功能
|
||||
- **登录注册系统**: 完全移除传统的用户认证流程
|
||||
- **用户管理**: 不再需要用户账户系统
|
||||
- **API依赖**: 移除所有后端接口依赖
|
||||
|
||||
#### 🔄 重大变更
|
||||
- **入口页面**: 从登录页面改为Token导入页面 (`/tokens`)
|
||||
- **路由结构**: 重构路由,旧路由自动重定向到Token管理
|
||||
- **数据结构**: 全新的Token数据结构,支持自定义名称和完整信息
|
||||
- **访问控制**: 基于Token存在性而非用户认证状态
|
||||
|
||||
#### 🛠️ 技术改进
|
||||
- **本地存储**: 所有数据完全本地化存储
|
||||
- **智能解析**: 自动识别和解析各种Base64格式
|
||||
- **连接监控**: 实时WebSocket连接状态显示
|
||||
- **容错处理**: 完善的错误处理和用户提示
|
||||
|
||||
#### 📱 用户体验
|
||||
- **简化流程**: 无需注册登录,直接导入Token使用
|
||||
- **直观管理**: 卡片式Token列表,状态一目了然
|
||||
- **快速操作**: 一键选择、连接、管理Token
|
||||
- **数据安全**: 本地存储,Token信息脱敏显示
|
||||
|
||||
---
|
||||
|
||||
## [1.x.x] - 历史版本
|
||||
|
||||
### 特性
|
||||
- 基于用户认证的传统系统
|
||||
- API接口依赖的数据管理
|
||||
- 游戏角色CRUD操作
|
||||
- 日常任务管理功能
|
||||
|
||||
---
|
||||
|
||||
## 升级指南
|
||||
|
||||
### 从1.x版本升级到2.0
|
||||
1. **数据迁移**:
|
||||
- 导出现有游戏角色数据
|
||||
- 获取每个角色对应的Token
|
||||
- 将Token转换为Base64格式后导入新系统
|
||||
|
||||
2. **使用变更**:
|
||||
- 不再需要注册登录
|
||||
- 直接访问 `/tokens` 页面导入Token
|
||||
- 通过Token名称管理多个游戏角色
|
||||
|
||||
3. **功能对照**:
|
||||
- 游戏角色管理 → Token管理
|
||||
- 用户认证 → Token导入
|
||||
- 角色选择 → Token选择
|
||||
|
||||
### 兼容性说明
|
||||
- 旧版本路由会自动重定向到新系统
|
||||
- 本地存储数据需要手动迁移
|
||||
- WebSocket连接方式保持兼容
|
||||
218
CLAUDE.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a Vue 3 Token Manager application for XYZW game automation. The application manages game tokens via Base64 decoding, establishes WebSocket connections, and provides a visual interface for token management and game automation.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Core Commands
|
||||
```bash
|
||||
# Development server (port 3000)
|
||||
npm run dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Lint Vue, JS, TS files with auto-fix
|
||||
npm run lint
|
||||
|
||||
# Format code (Prettier)
|
||||
npm run format
|
||||
```
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Core System Design
|
||||
The application is built around a **token-centric architecture** that replaces traditional user authentication:
|
||||
|
||||
1. **Token Management System**: Base64-encoded tokens are imported, decoded, and stored locally
|
||||
2. **WebSocket Connection Layer**: Automatic WebSocket connections using BON protocol for game communication
|
||||
3. **Local-First Storage**: All data stored in browser localStorage, no backend dependencies
|
||||
4. **Protocol Layer**: Custom BON (Binary Object Notation) protocol for game message encoding/decoding
|
||||
|
||||
### Key Architectural Components
|
||||
|
||||
#### 1. Token Store (`src/stores/tokenStore.js`)
|
||||
Central state management for token operations:
|
||||
- **Token Lifecycle**: Import → Parse → Store → Select → Connect
|
||||
- **Base64 Parsing**: Supports multiple formats (JSON, plain text, prefixed)
|
||||
- **WebSocket Management**: Automatic connection establishment and status tracking
|
||||
- **Data Persistence**: localStorage with cross-session state recovery
|
||||
|
||||
#### 2. BON Protocol Implementation (`src/utils/bonProtocol.js`)
|
||||
Custom binary protocol for game communication:
|
||||
- **Message Encoding/Decoding**: Binary serialization with type safety
|
||||
- **Game Message Templates**: Predefined message structures for common operations
|
||||
- **Encryption Layer**: Multi-channel encryption with XOR-based security
|
||||
- **WebSocket Message Handling**: Structured message parsing and creation
|
||||
|
||||
#### 3. WebSocket Client (`src/utils/xyzwWebSocket.js`)
|
||||
Enhanced WebSocket client based on reference implementation:
|
||||
- **Command Registry**: Pre-registered game commands with default parameters
|
||||
- **Queue Management**: Automatic message queuing and batch processing
|
||||
- **Connection Management**: Auto-reconnection, heartbeat, and status monitoring
|
||||
- **Promise Support**: Both fire-and-forget and request-response patterns
|
||||
|
||||
#### 4. Router Architecture (`src/router/index.js`)
|
||||
Token-aware navigation system:
|
||||
- **Access Control**: Route guards based on token availability
|
||||
- **Smart Redirects**: Automatic routing based on token state
|
||||
- **Legacy Compatibility**: Redirects from old authentication routes
|
||||
|
||||
### Data Flow Architecture
|
||||
|
||||
```
|
||||
Token Import → Base64 Decode → Local Storage → Token Selection → WebSocket Connection → Game Communication
|
||||
↑ ↓ ↓ ↓ ↓ ↓
|
||||
User Input JSON/String Token Store Router Guards BON Protocol Game Messages
|
||||
```
|
||||
|
||||
### State Management Pattern
|
||||
|
||||
**Pinia Store Structure**:
|
||||
- `tokenStore`: Primary token management and WebSocket connections
|
||||
- `auth`: Simplified authentication state (legacy compatibility)
|
||||
- `gameRoles`: Role-specific game data management
|
||||
- `localTokenManager`: Low-level token persistence utilities
|
||||
|
||||
## Key Framework Features
|
||||
|
||||
### Token Data Structure
|
||||
```javascript
|
||||
{
|
||||
id: "token_xxx", // Unique identifier
|
||||
name: "主号战士", // User-defined name
|
||||
token: "base64_token", // Actual token string
|
||||
wsUrl: "wss://...", // WebSocket endpoint
|
||||
server: "风云服", // Game server
|
||||
level: 85, // Character level
|
||||
profession: "战士", // Character class
|
||||
createdAt: "2024-...", // Creation timestamp
|
||||
lastUsed: "2024-...", // Last usage timestamp
|
||||
isActive: true // Activation status
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket Connection Flow
|
||||
1. **Token Selection**: User selects token from management interface
|
||||
2. **Base64 Parsing**: Extract actual game token from Base64 string
|
||||
3. **URL Construction**: Build WebSocket URL with token parameter
|
||||
4. **Client Creation**: Create `XyzwWebSocketClient` instance with game utilities
|
||||
5. **Connection Establishment**: Automatic connection with heartbeat and queue setup
|
||||
6. **Message Handling**: Bi-directional communication using command registry
|
||||
|
||||
### BON Protocol Message Format
|
||||
```javascript
|
||||
{
|
||||
cmd: "command_name", // Command identifier
|
||||
body: encodedData, // BON-encoded message body
|
||||
ack: 0, // Acknowledgment number
|
||||
seq: 12345, // Sequence number
|
||||
time: 1234567890 // Timestamp
|
||||
}
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── TokenManager.vue # Primary token management interface
|
||||
│ ├── DailyTaskCard.vue # Game task visualization
|
||||
│ ├── MessageTester.vue # Protocol debugging tool
|
||||
│ └── WebSocketTester.vue # Connection testing utility
|
||||
├── stores/
|
||||
│ ├── tokenStore.js # Core token management state
|
||||
│ ├── auth.js # Legacy authentication compatibility
|
||||
│ ├── gameRoles.js # Role-specific game data
|
||||
│ └── localTokenManager.js # Token persistence utilities
|
||||
├── utils/
|
||||
│ ├── bonProtocol.js # BON protocol implementation
|
||||
│ ├── gameCommands.js # Game-specific command helpers
|
||||
│ └── wsAgent.js # WebSocket connection management
|
||||
├── views/
|
||||
│ ├── TokenImport.vue # Token import/management page
|
||||
│ ├── Dashboard.vue # Main game control interface
|
||||
│ ├── DailyTasks.vue # Task management interface
|
||||
│ └── Profile.vue # User preferences and settings
|
||||
└── router/index.js # Token-aware routing configuration
|
||||
```
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Working with Tokens
|
||||
- Always use the `tokenStore` for token operations
|
||||
- Test Base64 parsing with various input formats
|
||||
- Verify WebSocket connections after token operations
|
||||
- Handle token validation errors gracefully
|
||||
|
||||
### WebSocket Development
|
||||
- Use the new `XyzwWebSocketClient` class for WebSocket connections
|
||||
- Send messages with `client.send(cmd, params)` or `client.sendWithPromise(cmd, params)`
|
||||
- Monitor connection status via `tokenStore.getWebSocketStatus(tokenId)`
|
||||
- WebSocket client includes automatic reconnection, queued sending, and heartbeat management
|
||||
- Built-in command registry supports game-specific message formats
|
||||
|
||||
### State Management
|
||||
- Access token data through computed properties (`selectedToken`, `hasTokens`)
|
||||
- Use reactive WebSocket status via `getWebSocketStatus(tokenId)`
|
||||
- Persist critical state changes to localStorage automatically
|
||||
- Handle cross-session state recovery on application startup
|
||||
|
||||
### Protocol Implementation
|
||||
- Follow BON encoding/decoding patterns for message handling
|
||||
- Use predefined `GameMessages` templates for common operations
|
||||
- Implement proper type checking for message validation
|
||||
- Handle protocol errors with fallback to JSON parsing
|
||||
|
||||
## Configuration Notes
|
||||
|
||||
### Vite Configuration
|
||||
- Path aliases configured for clean imports (`@/`, `@components/`, etc.)
|
||||
- Development server runs on port 3000
|
||||
- Proxy configured for `/api` routes to `http://xyzw.my`
|
||||
- SCSS preprocessing with global variables
|
||||
|
||||
### Browser Compatibility
|
||||
- Requires modern browser with WebSocket support
|
||||
- localStorage required for token persistence
|
||||
- Base64 decoding and TextEncoder/TextDecoder APIs used
|
||||
|
||||
### Security Considerations
|
||||
- All tokens stored locally in browser storage
|
||||
- WebSocket connections use WSS encryption
|
||||
- BON protocol includes basic XOR encryption
|
||||
- Token display masked (shows only first/last 4 characters)
|
||||
|
||||
## Testing and Debugging
|
||||
|
||||
### Built-in Testing Tools
|
||||
- **MessageTester.vue**: Test BON protocol message encoding/decoding
|
||||
- **WebSocketTester.vue**: Debug WebSocket connections and message flow
|
||||
- Browser DevTools WebSocket monitoring for connection debugging
|
||||
|
||||
### Common Development Tasks
|
||||
- Test token import with various Base64 formats
|
||||
- Verify WebSocket connection establishment with new client architecture
|
||||
- Debug game command sending using command registry
|
||||
- Test Promise-based message responses
|
||||
- Validate route guards and navigation flow
|
||||
- Test localStorage persistence across sessions
|
||||
|
||||
### Key API Changes
|
||||
- `tokenStore.sendMessage(tokenId, cmd, params)` - Send game commands
|
||||
- `tokenStore.sendMessageWithPromise(tokenId, cmd, params)` - Send with response
|
||||
- `tokenStore.getWebSocketClient(tokenId)` - Get client instance
|
||||
- WebSocket client provides `send()`, `sendWithPromise()`, and game-specific methods
|
||||
- Built-in commands: `getRoleInfo()`, `signIn()`, `claimDailyReward()`, etc.
|
||||
42
LICENSE
Normal file
@@ -0,0 +1,42 @@
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
|
||||
|
||||
Copyright (c) 2024 XYZW Team
|
||||
|
||||
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
|
||||
You are free to:
|
||||
- Share — copy and redistribute the material in any medium or format
|
||||
- Adapt — remix, transform, and build upon the material
|
||||
|
||||
The licensor cannot revoke these freedoms as long as you follow the license terms.
|
||||
|
||||
Under the following terms:
|
||||
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
||||
- NonCommercial — You may not use the material for commercial purposes.
|
||||
- ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
|
||||
- No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
|
||||
|
||||
Notices:
|
||||
You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation.
|
||||
|
||||
No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.
|
||||
|
||||
For the full legal text of this license, please visit:
|
||||
https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
|
||||
|
||||
---
|
||||
|
||||
ADDITIONAL TERMS FOR THIS SOFTWARE:
|
||||
|
||||
This software is specifically designed for educational and personal use only.
|
||||
Commercial use, including but not limited to:
|
||||
- Selling this software or derivative works
|
||||
- Using this software in commercial gaming operations
|
||||
- Integrating this software into commercial products or services
|
||||
- Using this software to generate revenue in any form
|
||||
|
||||
is strictly prohibited without explicit written permission from the copyright holders.
|
||||
|
||||
The software is provided "AS IS", without warranty of any kind, express or implied,
|
||||
including but not limited to the warranties of merchantability, fitness for a
|
||||
particular purpose and noninfringement.
|
||||
143
LOCAL_TOKEN_CHANGES.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# 本地Token存储重构说明
|
||||
|
||||
本次重构完全移除了所有API接口请求,改为使用本地存储管理用户认证和游戏角色token。
|
||||
|
||||
## 主要变更
|
||||
|
||||
### 1. 新增文件
|
||||
|
||||
#### `/src/stores/localTokenManager.js`
|
||||
- 完整的本地token管理系统
|
||||
- 支持用户认证token和游戏角色token管理
|
||||
- 内置WebSocket连接管理
|
||||
- 支持token导入/导出、过期清理等功能
|
||||
|
||||
#### `/src/components/TokenManager.vue`
|
||||
- Token管理界面组件
|
||||
- 可视化显示所有token状态
|
||||
- 支持WebSocket连接控制
|
||||
- 提供批量操作功能
|
||||
|
||||
### 2. 修改的文件
|
||||
|
||||
#### `/src/stores/auth.js`
|
||||
- **移除**: 所有`api.auth.*`调用
|
||||
- **新增**: 本地认证逻辑,模拟用户登录
|
||||
- **集成**: localTokenStore进行token管理
|
||||
|
||||
#### `/src/stores/gameRoles.js`
|
||||
- **移除**: 所有`api.gameRoles.*`调用
|
||||
- **新增**: 本地角色管理,自动生成游戏token
|
||||
- **集成**: 角色选择时自动建立WebSocket连接
|
||||
|
||||
#### `/src/views/DailyTasks.vue`
|
||||
- **移除**: `api.dailyTasks.*`调用
|
||||
- **新增**: 本地模拟任务数据生成
|
||||
- **集成**: 通过WebSocket执行任务(模拟)
|
||||
|
||||
#### `/src/views/Profile.vue`
|
||||
- **新增**: TokenManager组件,提供token管理界面
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 用户认证
|
||||
```javascript
|
||||
// 本地认证,无需API调用
|
||||
const result = await authStore.login({ username, password })
|
||||
```
|
||||
|
||||
### 游戏角色管理
|
||||
```javascript
|
||||
// 添加角色时自动生成游戏token
|
||||
const result = await gameRolesStore.addGameRole(roleData)
|
||||
// 自动生成: roleId, gameToken, wsUrl
|
||||
```
|
||||
|
||||
### WebSocket连接
|
||||
```javascript
|
||||
// 选择角色时自动建立WebSocket连接
|
||||
gameRolesStore.selectRole(role)
|
||||
// 使用本地存储的token建立连接
|
||||
|
||||
// 手动控制连接
|
||||
localTokenStore.createWebSocketConnection(roleId, token, wsUrl)
|
||||
localTokenStore.closeWebSocketConnection(roleId)
|
||||
```
|
||||
|
||||
### Token管理
|
||||
```javascript
|
||||
// 添加游戏token
|
||||
localTokenStore.addGameToken(roleId, tokenData)
|
||||
|
||||
// 获取token
|
||||
const tokenData = localTokenStore.getGameToken(roleId)
|
||||
|
||||
// 导出所有token
|
||||
const backup = localTokenStore.exportTokens()
|
||||
|
||||
// 导入token
|
||||
localTokenStore.importTokens(backupData)
|
||||
```
|
||||
|
||||
## 数据结构
|
||||
|
||||
### 游戏Token数据结构
|
||||
```javascript
|
||||
{
|
||||
token: "game_token_xxx", // 游戏token
|
||||
roleId: "role_xxx", // 角色ID
|
||||
roleName: "角色名称", // 角色名称
|
||||
server: "服务器名", // 服务器
|
||||
wsUrl: "wss://game.xxx/ws", // WebSocket URL
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
lastUsed: "2024-01-01T00:00:00Z",
|
||||
isActive: true
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket连接状态
|
||||
```javascript
|
||||
{
|
||||
connection: WebSocket, // WebSocket连接对象
|
||||
status: "connected", // 连接状态
|
||||
roleId: "role_xxx", // 关联角色ID
|
||||
connectedAt: "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 1. 登录
|
||||
- 用户名/密码任意输入即可本地认证
|
||||
- 自动生成用户token并保存
|
||||
|
||||
### 2. 添加游戏角色
|
||||
- 填写角色信息后自动生成:
|
||||
- 角色ID
|
||||
- 游戏token
|
||||
- WebSocket连接URL
|
||||
|
||||
### 3. 管理Token
|
||||
- 访问"个人设置"页面查看Token管理器
|
||||
- 可以查看、编辑、删除、导出/导入token
|
||||
- 可以手动控制WebSocket连接
|
||||
|
||||
### 4. 执行任务
|
||||
- 选择角色后自动建立WebSocket连接
|
||||
- 执行任务通过WebSocket发送指令(模拟)
|
||||
- 所有操作记录保存在本地
|
||||
|
||||
## 优势
|
||||
|
||||
1. **完全离线**: 无需任何服务器接口
|
||||
2. **数据安全**: 所有数据存储在本地
|
||||
3. **功能完整**: 保留原有所有功能
|
||||
4. **易于扩展**: 模块化设计,便于添加新功能
|
||||
5. **WebSocket支持**: 内置完整的WebSocket连接管理
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有数据存储在浏览器localStorage中
|
||||
2. 清除浏览器数据会丢失所有token
|
||||
3. 建议定期使用导出功能备份token数据
|
||||
4. WebSocket连接使用模拟URL,需要根据实际情况修改
|
||||
477
README.md
Normal file
@@ -0,0 +1,477 @@
|
||||
# XYZW Web Helper
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
**🎮 咸鱼自动化web平台**
|
||||
|
||||
[](https://vuejs.org/)
|
||||
[](https://vitejs.dev/)
|
||||
[](https://www.naiveui.com/)
|
||||
[](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
||||
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
||||
|
||||
基于Vue 3 + Vite的现代化XYZW游戏辅助工具,支持Token管理、WebSocket通信、游戏自动化等功能。
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
### 🔐 Token管理系统
|
||||
- **Base64解码支持**:自动识别和解析多种Base64格式的游戏Token
|
||||
- **多角色管理**:同时管理多个游戏账号,支持角色信息展示
|
||||
- **本地存储**:安全的本地数据存储,无需后端服务器
|
||||
- **Token验证**:自动验证Token有效性和格式完整性
|
||||
|
||||
### 🌐 WebSocket通信
|
||||
- **BON协议支持**:内置Binary Object Notation协议编解码
|
||||
- **多重加密**:支持LX、X、XTM等多种加密方式
|
||||
- **自动重连**:智能断线重连机制,确保连接稳定
|
||||
- **消息队列**:内置消息队列系统,支持批量发送和响应处理
|
||||
|
||||
### 🎮 游戏功能
|
||||
- **日常任务管理**:自动化日常任务执行和奖励领取
|
||||
- **角色状态监控**:实时显示角色等级、职业、服务器等信息
|
||||
- **团队管理**:队伍状态查看和管理功能
|
||||
- **爬塔进度**:爬塔状态追踪和数据分析
|
||||
|
||||
### 🛠️ 开发工具
|
||||
- **消息测试器**:BON协议消息编码/解码测试工具
|
||||
- **WebSocket调试**:实时WebSocket连接和消息调试
|
||||
- **协议验证**:游戏协议消息格式验证工具
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 前端技术栈
|
||||
```
|
||||
Vue 3.4+ # 渐进式JavaScript框架
|
||||
├── Composition API # Vue 3组合式API
|
||||
├── <script setup> # 单文件组件语法糖
|
||||
└── Reactive System # 响应式数据系统
|
||||
|
||||
Vite 5.0+ # 现代化构建工具
|
||||
├── HMR # 热模块替换
|
||||
├── ES6+ # 现代JavaScript支持
|
||||
└── SCSS # CSS预处理器
|
||||
|
||||
Naive UI 2.38+ # Vue 3组件库
|
||||
├── Theme System # 主题系统
|
||||
├── Icon Library # 图标库
|
||||
└── Responsive # 响应式设计
|
||||
|
||||
Pinia 2.1+ # 状态管理
|
||||
├── Store Pattern # 存储模式
|
||||
├── DevTools # 开发工具
|
||||
└── Composition API # 组合式API支持
|
||||
```
|
||||
|
||||
### 核心架构设计
|
||||
|
||||
```
|
||||
src/
|
||||
├── 🎯 stores/ # Pinia状态管理
|
||||
│ ├── tokenStore.js # Token管理核心
|
||||
│ ├── gameRoles.js # 游戏角色数据
|
||||
│ └── localTokenManager.js # 本地存储管理
|
||||
│
|
||||
├── 🌐 utils/ # 核心工具库
|
||||
│ ├── bonProtocol.js # BON协议实现
|
||||
│ ├── xyzwWebSocket.js # WebSocket客户端
|
||||
│ ├── gameCommands.js # 游戏命令封装
|
||||
│ └── wsAgent.js # 连接代理
|
||||
│
|
||||
├── 📱 views/ # 主要页面
|
||||
│ ├── TokenImport.vue # Token导入管理
|
||||
│ ├── Dashboard.vue # 主控制台
|
||||
│ ├── DailyTasks.vue # 日常任务
|
||||
│ ├── GameFeatures.vue # 游戏功能
|
||||
│ └── Profile.vue # 用户设置
|
||||
│
|
||||
└── 🧩 components/ # 可复用组件
|
||||
├── TokenManager.vue # Token管理器
|
||||
├── GameStatus.vue # 游戏状态组件
|
||||
├── DailyTaskCard.vue # 任务卡片
|
||||
├── MessageTester.vue # 消息测试器
|
||||
└── WebSocketTester.vue # WebSocket调试器
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
```bash
|
||||
Node.js >= 16.0.0
|
||||
npm >= 7.0.0
|
||||
```
|
||||
|
||||
### 安装与运行
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/your-repo/xyzw-web-helper.git
|
||||
cd xyzw-web-helper
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 预览生产构建
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### 开发命令
|
||||
|
||||
```bash
|
||||
npm run dev # 启动开发服务器 (端口3000)
|
||||
npm run build # 构建生产版本
|
||||
npm run preview # 预览生产构建
|
||||
npm run lint # 代码检查和修复
|
||||
npm run format # 代码格式化
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 使用指南
|
||||
|
||||
### 1. Token导入与管理
|
||||
|
||||
#### 支持的Token格式
|
||||
```javascript
|
||||
|
||||
|
||||
// 纯Base64格式
|
||||
"eyJ0b2tlbiI6ImFiY2QxMjM0In0="
|
||||
|
||||
// 带前缀格式
|
||||
"token:eyJ0b2tlbiI6ImFiY2QxMjM0In0="
|
||||
```
|
||||
|
||||
#### 导入步骤
|
||||
1. 进入 **Token管理** 页面
|
||||
2. 选择导入方式(文件上传/文本粘贴/手动输入)
|
||||
3. 系统自动解析和验证Token格式
|
||||
4. 设置角色名称和基本信息
|
||||
5. 保存到本地存储
|
||||
|
||||
### 2. WebSocket连接配置
|
||||
|
||||
纯本地连接存储,不用担心封号及账号泄漏风险
|
||||
|
||||
### 3. BON协议消息处理
|
||||
|
||||
```javascript
|
||||
import { bon, GameMessages } from '@/utils/bonProtocol.js';
|
||||
|
||||
// 编码消息
|
||||
const message = GameMessages.getRoleInfo(0, 12345);
|
||||
const encoded = bon.encode(message.body);
|
||||
|
||||
// 解码消息
|
||||
const decoded = bon.decode(receivedData);
|
||||
```
|
||||
|
||||
### 4. 游戏功能使用
|
||||
|
||||
#### 日常任务自动化
|
||||
- 自动签到奖励领取
|
||||
- 日常任务完成状态检查
|
||||
- 奖励自动领取
|
||||
- 任务进度实时追踪
|
||||
|
||||
#### 角色状态监控
|
||||
- 实时等级和经验显示
|
||||
- 职业和技能信息
|
||||
- 服务器状态监控
|
||||
- 在线时长统计
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### Vite配置 (vite.config.js)
|
||||
|
||||
```javascript
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'@components': path.resolve(__dirname, 'src/components'),
|
||||
'@utils': path.resolve(__dirname, 'src/utils'),
|
||||
'@stores': path.resolve(__dirname, 'src/stores')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://xyzw.my',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
```bash
|
||||
# .env.development
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
VITE_WS_URL=wss://game.xyzw.my/ws
|
||||
|
||||
# .env.production
|
||||
VITE_API_BASE_URL=https://api.xyzw.my
|
||||
VITE_WS_URL=wss://game.xyzw.my/ws
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试与调试
|
||||
|
||||
### 内置测试工具
|
||||
|
||||
#### 1. 消息测试器 (MessageTester.vue)
|
||||
- BON协议编码/解码测试
|
||||
- 消息格式验证
|
||||
- 加密/解密功能测试
|
||||
|
||||
#### 2. WebSocket调试器 (WebSocketTester.vue)
|
||||
- 实时连接状态监控
|
||||
- 消息发送和接收测试
|
||||
- 连接参数配置
|
||||
|
||||
#### 3. 协议验证工具
|
||||
```javascript
|
||||
// 测试BON编码
|
||||
const testData = { cmd: "test", data: { id: 123 } };
|
||||
const encoded = bon.encode(testData);
|
||||
const decoded = bon.decode(encoded);
|
||||
console.log('编码测试:', decoded);
|
||||
|
||||
// 测试加密
|
||||
const encrypted = getEnc('x').encrypt(encoded);
|
||||
const decrypted = getEnc('x').decrypt(encrypted);
|
||||
```
|
||||
|
||||
### 调试技巧
|
||||
|
||||
1. **浏览器开发工具**:使用Vue DevTools监控组件状态
|
||||
2. **网络面板**:监控WebSocket消息传输
|
||||
3. **控制台日志**:查看详细的协议处理日志
|
||||
4. **本地存储检查**:验证Token和配置存储
|
||||
|
||||
---
|
||||
|
||||
## 📦 项目结构详解
|
||||
|
||||
### 状态管理架构
|
||||
|
||||
```javascript
|
||||
// tokenStore.js - 核心Token管理
|
||||
const useTokenStore = defineStore('tokens', () => {
|
||||
const gameTokens = ref([]); // Token列表
|
||||
const selectedTokenId = ref(null); // 当前选中Token
|
||||
const wsConnections = ref({}); // WebSocket连接池
|
||||
|
||||
// Token管理方法
|
||||
const addToken = (tokenData) => { /* ... */ };
|
||||
const updateToken = (id, updates) => { /* ... */ };
|
||||
const removeToken = (id) => { /* ... */ };
|
||||
|
||||
// WebSocket管理
|
||||
const connectWebSocket = (tokenId) => { /* ... */ };
|
||||
const disconnectWebSocket = (tokenId) => { /* ... */ };
|
||||
|
||||
return {
|
||||
gameTokens, selectedTokenId, wsConnections,
|
||||
addToken, updateToken, removeToken,
|
||||
connectWebSocket, disconnectWebSocket
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 路由守卫系统
|
||||
|
||||
```javascript
|
||||
// router/index.js
|
||||
router.beforeEach((to, from, next) => {
|
||||
const tokenStore = useTokenStore();
|
||||
|
||||
if (to.meta.requiresToken && !tokenStore.hasTokens) {
|
||||
next('/tokens'); // 重定向到Token管理页
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 组件通信模式
|
||||
|
||||
```javascript
|
||||
// 父子组件通信
|
||||
// Parent.vue
|
||||
<TokenManager
|
||||
:tokens="tokens"
|
||||
@token-selected="handleTokenSelect"
|
||||
@token-updated="handleTokenUpdate" />
|
||||
|
||||
// 兄弟组件通信(通过Store)
|
||||
const tokenStore = useTokenStore();
|
||||
const gameData = computed(() => tokenStore.gameData);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 安全特性
|
||||
|
||||
### 数据安全
|
||||
- **本地存储**:所有敏感数据仅存储在浏览器本地
|
||||
- **Token掩码**:界面显示时自动掩码处理(显示首尾4位)
|
||||
- **加密传输**:WebSocket消息使用多重加密协议
|
||||
- **会话隔离**:每个Tab页面独立的连接会话
|
||||
|
||||
### 协议安全
|
||||
```javascript
|
||||
// 多重加密支持
|
||||
const encryptors = {
|
||||
lx: lzCompressionWithMask, // LZ4压缩+头部掩码
|
||||
x: randomHeaderWithXOR, // 随机头部+XOR加密
|
||||
xtm: xxteaEncryption // XXTEA加密算法
|
||||
};
|
||||
|
||||
// 自动加密检测和解密
|
||||
const autoDecrypt = (data) => {
|
||||
if (isLXFormat(data)) return lx.decrypt(data);
|
||||
if (isXFormat(data)) return x.decrypt(data);
|
||||
if (isXTMFormat(data)) return xtm.decrypt(data);
|
||||
return data;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 性能优化
|
||||
|
||||
### 前端优化
|
||||
- **代码分割**:路由级别的懒加载
|
||||
- **Tree Shaking**:自动删除未使用代码
|
||||
- **资源压缩**:Gzip压缩和资源优化
|
||||
- **缓存策略**:智能缓存Token和游戏数据
|
||||
|
||||
### WebSocket优化
|
||||
- **连接池**:复用WebSocket连接
|
||||
- **消息队列**:批量处理和发送优化
|
||||
- **心跳机制**:智能心跳保持连接活跃
|
||||
- **断线重连**:指数退避重连算法
|
||||
|
||||
### 内存优化
|
||||
```javascript
|
||||
// 响应式数据优化
|
||||
const gameData = computed(() => {
|
||||
return tokenStore.gameData || {};
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (wsClient.value) {
|
||||
wsClient.value.disconnect();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
### 开发规范
|
||||
1. **代码风格**:使用ESLint + Prettier统一代码风格
|
||||
2. **组件命名**:使用PascalCase命名Vue组件
|
||||
3. **提交规范**:遵循Conventional Commits规范
|
||||
4. **文档注释**:关键功能使用JSDoc注释
|
||||
|
||||
### 提交流程
|
||||
```bash
|
||||
# 1. Fork项目并克隆到本地
|
||||
git clone https://github.com/your-username/xyzw-web-helper.git
|
||||
|
||||
# 2. 创建功能分支
|
||||
git checkout -b feature/new-feature
|
||||
|
||||
# 3. 提交更改
|
||||
git commit -m "feat: 添加新功能描述"
|
||||
|
||||
# 4. 推送分支
|
||||
git push origin feature/new-feature
|
||||
|
||||
# 5. 创建Pull Request
|
||||
```
|
||||
|
||||
### Issue反馈
|
||||
- 🐛 **Bug报告**:描述问题复现步骤和环境信息
|
||||
- 💡 **功能建议**:详细说明需求场景和预期效果
|
||||
- 📖 **文档改进**:指出文档不准确或缺失的部分
|
||||
- ❓ **使用问题**:提供详细的使用场景和问题描述
|
||||
|
||||
---
|
||||
|
||||
## 📋 更新日志
|
||||
|
||||
### v2.0.0 (Current)
|
||||
- 🎉 重构Token管理系统,支持多角色管理
|
||||
- 🔧 升级WebSocket客户端,支持更多游戏协议
|
||||
- 🎨 全新UI设计,基于Naive UI组件库
|
||||
- ⚡ 优化BON协议处理,提升消息编解码性能
|
||||
- 🛡️ 增强安全性,支持多种加密方式
|
||||
- 🧪 添加完整的测试和调试工具
|
||||
|
||||
### v1.x.x (Legacy)
|
||||
- 基础Token管理功能
|
||||
- 简单WebSocket连接
|
||||
- 基础游戏功能支持
|
||||
|
||||
---
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目基于 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](LICENSE) 许可证。
|
||||
|
||||
**⚠️ 重要声明:**
|
||||
- ✅ **允许**:个人学习、研究、修改和分享
|
||||
- ❌ **禁止**:商业用途、销售、商业化运营
|
||||
- 📝 **要求**:署名、相同许可证分享、标注修改
|
||||
|
||||
详细许可条款请查看 [LICENSE](LICENSE) 文件。
|
||||
|
||||
---
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
- **项目主页**:[GitHub Repository](https://github.com/w1249178256/xyzw_web_helper)
|
||||
- **问题反馈**:[GitHub Issues](https://github.com/w1249178256/xyzw_web_helper/issues)
|
||||
- **联系邮箱**:[MAIL](stevefeng59@gmail.com)
|
||||
|
||||
---
|
||||
|
||||
## 👏 赞赏
|
||||
<img src="https://github.com/w1249178256/xyzw_web_helper/blob/main/public/IMG_8007.JPG" width="200" height="200">z
|
||||
|
||||
<div align="center">
|
||||
|
||||
**⭐ 如果这个项目对你有帮助,请给它一个星标!**
|
||||
|
||||
Made with ❤️ by FF Team
|
||||
|
||||
</div>
|
||||
147
TOKEN_MANAGEMENT_UPDATE.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Token管理系统重构完成
|
||||
|
||||
已完成从登录注册模式到Token管理模式的完整重构。
|
||||
|
||||
## 重大变更
|
||||
|
||||
### 🗑️ 已移除
|
||||
- **登录/注册界面** - 完全移除认证流程
|
||||
- **用户管理系统** - 不再需要用户账户
|
||||
- **API依赖** - 无需任何后端接口
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
#### 1. Base64 Token导入
|
||||
- **自动解析**: 支持Base64编码的Token字符串
|
||||
- **智能识别**: 自动检测JSON格式或纯Token字符串
|
||||
- **容错处理**: 自动移除base64前缀和空格
|
||||
|
||||
#### 2. Token管理界面 (`/tokens`)
|
||||
- **名称-Token列表**: 每个Token可自定义名称
|
||||
- **WebSocket连接**: 实时显示连接状态
|
||||
- **批量操作**: 导入/导出、清理过期Token
|
||||
- **Token编辑**: 可修改名称、服务器等信息
|
||||
|
||||
#### 3. 新的数据结构
|
||||
```javascript
|
||||
{
|
||||
id: "token_xxx", // 唯一标识
|
||||
name: "主号战士", // 自定义名称
|
||||
token: "base64_token", // 实际Token
|
||||
wsUrl: "wss://...", // WebSocket地址
|
||||
server: "风云服", // 服务器
|
||||
level: 85, // 等级
|
||||
profession: "战士", // 职业
|
||||
createdAt: "2024-...", // 创建时间
|
||||
lastUsed: "2024-...", // 最后使用
|
||||
isActive: true // 是否激活
|
||||
}
|
||||
```
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 1. 导入Token
|
||||
1. 访问 `/tokens` 页面
|
||||
2. 输入Token名称(如"主号战士")
|
||||
3. 粘贴Base64编码的Token字符串
|
||||
4. 可选填写服务器、等级、职业等信息
|
||||
5. 点击"导入Token"
|
||||
|
||||
### 2. 管理Token
|
||||
- **选择Token**: 点击Token卡片选择当前使用的Token
|
||||
- **WebSocket连接**: 选择Token后自动建立连接
|
||||
- **编辑信息**: 修改名称、服务器等基本信息
|
||||
- **连接控制**: 手动断开/重连WebSocket
|
||||
|
||||
### 3. 批量操作
|
||||
- **导出Token**: 备份所有Token到JSON文件
|
||||
- **导入Token**: 从备份文件批量导入
|
||||
- **清理过期**: 自动清理24小时未使用的Token
|
||||
- **断开连接**: 断开所有WebSocket连接
|
||||
|
||||
## 路由变更
|
||||
|
||||
### 新路由结构
|
||||
```
|
||||
/ → 首页(有Token时重定向到控制台)
|
||||
/tokens → Token管理页面
|
||||
/dashboard → 控制台(需要Token)
|
||||
/daily-tasks → 日常任务(需要Token)
|
||||
/profile → 个人设置(需要Token)
|
||||
```
|
||||
|
||||
### 重定向规则
|
||||
```
|
||||
/login → /tokens
|
||||
/register → /tokens
|
||||
/game-roles → /tokens
|
||||
```
|
||||
|
||||
### 访问控制
|
||||
- **无Token**: 自动重定向到 `/tokens`
|
||||
- **有Token但未选择**: 重定向到 `/tokens`
|
||||
- **已选择Token**: 可正常访问所有功能页面
|
||||
|
||||
## 核心功能
|
||||
|
||||
### Base64解析器
|
||||
```javascript
|
||||
// 支持多种格式
|
||||
parseBase64Token("eyJ0b2tlbiI6Imp...") // JSON格式
|
||||
parseBase64Token("game_token_12345") // 纯字符串
|
||||
parseBase64Token("data:text/plain;base64,eyJ...") // 带前缀
|
||||
```
|
||||
|
||||
### WebSocket管理
|
||||
```javascript
|
||||
// 自动连接
|
||||
tokenStore.selectToken(tokenId)
|
||||
|
||||
// 手动控制
|
||||
tokenStore.createWebSocketConnection(tokenId, token, wsUrl)
|
||||
tokenStore.closeWebSocketConnection(tokenId)
|
||||
tokenStore.getWebSocketStatus(tokenId) // 'connected'|'disconnected'|'error'
|
||||
```
|
||||
|
||||
### 数据持久化
|
||||
- **localStorage**: 所有Token数据保存在本地
|
||||
- **实时同步**: 修改后自动保存
|
||||
- **跨会话**: 重新打开浏览器数据仍在
|
||||
|
||||
## 界面特性
|
||||
|
||||
### Token卡片显示
|
||||
- **连接状态**: 绿色圆点表示已连接
|
||||
- **选中状态**: 蓝色边框表示当前选中
|
||||
- **Token预览**: 显示前4位和后4位,中间用***隐藏
|
||||
- **时间戳**: 显示创建时间和最后使用时间
|
||||
|
||||
### 响应式设计
|
||||
- **移动端适配**: 完全响应式布局
|
||||
- **触摸友好**: 大按钮,易于操作
|
||||
- **自适应网格**: Token卡片自动排列
|
||||
|
||||
## 优势
|
||||
|
||||
1. **简化流程**: 无需注册登录,直接导入Token使用
|
||||
2. **Base64支持**: 兼容各种Token格式
|
||||
3. **可视化管理**: 直观的Token列表和状态显示
|
||||
4. **批量操作**: 高效的导入/导出功能
|
||||
5. **实时连接**: WebSocket状态实时显示
|
||||
6. **完全本地**: 无需任何后端服务
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 基本功能测试
|
||||
1. 导入各种格式的Base64 Token
|
||||
2. 测试Token选择和WebSocket连接
|
||||
3. 验证编辑Token信息功能
|
||||
4. 测试批量导出/导入
|
||||
|
||||
### 边界情况测试
|
||||
1. 无效的Base64字符串
|
||||
2. 空Token名称
|
||||
3. 重复导入相同Token
|
||||
4. WebSocket连接失败情况
|
||||
|
||||
现在整个系统以Token为中心,提供了完整的导入、管理、使用流程!
|
||||
41
index.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>XYZW 游戏管理系统</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="loading">正在加载应用...</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
3586
package-lock.json
generated
Normal file
44
package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "xyzw-token-manager",
|
||||
"version": "2.0.0",
|
||||
"description": "XYZW游戏Token管理器 - 支持Base64导入和WebSocket连接管理",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --ext .vue,.js,.ts --fix",
|
||||
"format": "prettier --write \"src/**/*.{js,vue,ts,css,scss}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@vicons/ionicons5": "^0.12.0",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"axios": "^1.6.0",
|
||||
"lz4js": "^0.2.0",
|
||||
"naive-ui": "^2.38.0",
|
||||
"pinia": "^2.1.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"sass": "^1.69.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"token-management",
|
||||
"websocket",
|
||||
"base64",
|
||||
"game-automation",
|
||||
"xyzw",
|
||||
"frontend"
|
||||
],
|
||||
"author": "XYZW Team",
|
||||
"license": "CC-BY-NC-SA-4.0"
|
||||
}
|
||||
BIN
public/1733492491706148.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/1733492491706152.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/1736425783912140.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/173746572831736.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/174023274867420.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/174061875626614.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/IMG_8007.JPG
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
public/Ob7pyorzmHiJcbab2c25af264d0758b527bc1b61cc3b.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icons/1733492491706148.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/icons/1733492491706152.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/icons/1736425783912140.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/icons/173746572831736.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/icons/174023274867420.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/icons/174061875626614.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/icons/Ob7pyorzmHiJcbab2c25af264d0758b527bc1b61cc3b.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icons/ta.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/icons/xiaoyugan.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
public/ta.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/xiaoyugan.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
78
src/App.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<n-config-provider :theme="theme">
|
||||
<n-message-provider>
|
||||
<n-loading-bar-provider>
|
||||
<n-notification-provider>
|
||||
<n-dialog-provider>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</n-dialog-provider>
|
||||
</n-notification-provider>
|
||||
</n-loading-bar-provider>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
|
||||
// 主题控制
|
||||
const theme = ref(null)
|
||||
|
||||
// 检查用户偏好的主题
|
||||
const checkThemePreference = () => {
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
||||
theme.value = darkTheme
|
||||
} else {
|
||||
theme.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkThemePreference()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
background: var(--app-background, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
|
||||
}
|
||||
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
134
src/api/index.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import axios from 'axios'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// 创建axios实例
|
||||
const request = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.token) {
|
||||
config.headers.Authorization = `Bearer ${authStore.token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data
|
||||
|
||||
// 统一处理响应格式
|
||||
if (data.success !== undefined) {
|
||||
return data
|
||||
}
|
||||
|
||||
// 兼容不同的响应格式
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
message: 'success'
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 处理HTTP错误
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
// 未授权,清除登录状态
|
||||
authStore.logout()
|
||||
window.location.href = '/login'
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: '登录已过期,请重新登录'
|
||||
})
|
||||
case 403:
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: '没有权限访问'
|
||||
})
|
||||
case 404:
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: '请求的资源不存在'
|
||||
})
|
||||
case 500:
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
})
|
||||
default:
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: data?.message || '请求失败'
|
||||
})
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 网络错误
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: '网络连接失败,请检查网络'
|
||||
})
|
||||
} else {
|
||||
// 其他错误
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: error.message || '未知错误'
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// API接口定义
|
||||
const api = {
|
||||
// 认证相关
|
||||
auth: {
|
||||
login: (credentials) => request.post('/auth/login', credentials),
|
||||
register: (userInfo) => request.post('/auth/register', userInfo),
|
||||
logout: () => request.post('/auth/logout'),
|
||||
getUserInfo: () => request.get('/auth/user'),
|
||||
refreshToken: () => request.post('/auth/refresh')
|
||||
},
|
||||
|
||||
// 游戏角色相关
|
||||
gameRoles: {
|
||||
getList: () => request.get('/gamerole_list'),
|
||||
add: (roleData) => request.post('/gameroles', roleData),
|
||||
update: (roleId, roleData) => request.put(`/gameroles/${roleId}`, roleData),
|
||||
delete: (roleId) => request.delete(`/gameroles/${roleId}`),
|
||||
getDetail: (roleId) => request.get(`/gameroles/${roleId}`)
|
||||
},
|
||||
|
||||
// 日常任务相关
|
||||
dailyTasks: {
|
||||
getList: (roleId) => request.get(`/daily-tasks?roleId=${roleId}`),
|
||||
getStatus: (roleId) => request.get(`/daily-tasks/status?roleId=${roleId}`),
|
||||
complete: (taskId, roleId) => request.post(`/daily-tasks/${taskId}/complete`, { roleId }),
|
||||
getHistory: (roleId, page = 1, limit = 20) => request.get(`/daily-tasks/history?roleId=${roleId}&page=${page}&limit=${limit}`)
|
||||
},
|
||||
|
||||
// 用户相关
|
||||
user: {
|
||||
getProfile: () => request.get('/user/profile'),
|
||||
updateProfile: (profileData) => request.put('/user/profile', profileData),
|
||||
changePassword: (passwordData) => request.put('/user/password', passwordData),
|
||||
getStats: () => request.get('/user/stats')
|
||||
}
|
||||
}
|
||||
|
||||
export default api
|
||||
314
src/assets/styles/global.scss
Normal file
@@ -0,0 +1,314 @@
|
||||
// 全局样式重置
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: var(--font-size-md);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// 链接样式
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮重置
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
// 输入框重置
|
||||
input, textarea, select {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// 列表重置
|
||||
ul, ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
// 图片
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// 工具类
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// 间距工具类
|
||||
.m-0 { margin: 0; }
|
||||
.m-1 { margin: var(--spacing-xs); }
|
||||
.m-2 { margin: var(--spacing-sm); }
|
||||
.m-3 { margin: var(--spacing-md); }
|
||||
.m-4 { margin: var(--spacing-lg); }
|
||||
.m-5 { margin: var(--spacing-xl); }
|
||||
|
||||
.mt-0 { margin-top: 0; }
|
||||
.mt-1 { margin-top: var(--spacing-xs); }
|
||||
.mt-2 { margin-top: var(--spacing-sm); }
|
||||
.mt-3 { margin-top: var(--spacing-md); }
|
||||
.mt-4 { margin-top: var(--spacing-lg); }
|
||||
.mt-5 { margin-top: var(--spacing-xl); }
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-1 { margin-bottom: var(--spacing-xs); }
|
||||
.mb-2 { margin-bottom: var(--spacing-sm); }
|
||||
.mb-3 { margin-bottom: var(--spacing-md); }
|
||||
.mb-4 { margin-bottom: var(--spacing-lg); }
|
||||
.mb-5 { margin-bottom: var(--spacing-xl); }
|
||||
|
||||
.ml-0 { margin-left: 0; }
|
||||
.ml-1 { margin-left: var(--spacing-xs); }
|
||||
.ml-2 { margin-left: var(--spacing-sm); }
|
||||
.ml-3 { margin-left: var(--spacing-md); }
|
||||
.ml-4 { margin-left: var(--spacing-lg); }
|
||||
.ml-5 { margin-left: var(--spacing-xl); }
|
||||
|
||||
.mr-0 { margin-right: 0; }
|
||||
.mr-1 { margin-right: var(--spacing-xs); }
|
||||
.mr-2 { margin-right: var(--spacing-sm); }
|
||||
.mr-3 { margin-right: var(--spacing-md); }
|
||||
.mr-4 { margin-right: var(--spacing-lg); }
|
||||
.mr-5 { margin-right: var(--spacing-xl); }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-1 { padding: var(--spacing-xs); }
|
||||
.p-2 { padding: var(--spacing-sm); }
|
||||
.p-3 { padding: var(--spacing-md); }
|
||||
.p-4 { padding: var(--spacing-lg); }
|
||||
.p-5 { padding: var(--spacing-xl); }
|
||||
|
||||
.pt-0 { padding-top: 0; }
|
||||
.pt-1 { padding-top: var(--spacing-xs); }
|
||||
.pt-2 { padding-top: var(--spacing-sm); }
|
||||
.pt-3 { padding-top: var(--spacing-md); }
|
||||
.pt-4 { padding-top: var(--spacing-lg); }
|
||||
.pt-5 { padding-top: var(--spacing-xl); }
|
||||
|
||||
.pb-0 { padding-bottom: 0; }
|
||||
.pb-1 { padding-bottom: var(--spacing-xs); }
|
||||
.pb-2 { padding-bottom: var(--spacing-sm); }
|
||||
.pb-3 { padding-bottom: var(--spacing-md); }
|
||||
.pb-4 { padding-bottom: var(--spacing-lg); }
|
||||
.pb-5 { padding-bottom: var(--spacing-xl); }
|
||||
|
||||
.pl-0 { padding-left: 0; }
|
||||
.pl-1 { padding-left: var(--spacing-xs); }
|
||||
.pl-2 { padding-left: var(--spacing-sm); }
|
||||
.pl-3 { padding-left: var(--spacing-md); }
|
||||
.pl-4 { padding-left: var(--spacing-lg); }
|
||||
.pl-5 { padding-left: var(--spacing-xl); }
|
||||
|
||||
.pr-0 { padding-right: 0; }
|
||||
.pr-1 { padding-right: var(--spacing-xs); }
|
||||
.pr-2 { padding-right: var(--spacing-sm); }
|
||||
.pr-3 { padding-right: var(--spacing-md); }
|
||||
.pr-4 { padding-right: var(--spacing-lg); }
|
||||
.pr-5 { padding-right: var(--spacing-xl); }
|
||||
|
||||
// 文字大小
|
||||
.text-xs { font-size: var(--font-size-xs); }
|
||||
.text-sm { font-size: var(--font-size-sm); }
|
||||
.text-md { font-size: var(--font-size-md); }
|
||||
.text-lg { font-size: var(--font-size-lg); }
|
||||
.text-xl { font-size: var(--font-size-xl); }
|
||||
.text-2xl { font-size: var(--font-size-2xl); }
|
||||
.text-3xl { font-size: var(--font-size-3xl); }
|
||||
|
||||
// 文字颜色
|
||||
.text-primary { color: var(--text-primary); }
|
||||
.text-secondary { color: var(--text-secondary); }
|
||||
.text-tertiary { color: var(--text-tertiary); }
|
||||
.text-success { color: var(--success-color); }
|
||||
.text-warning { color: var(--warning-color); }
|
||||
.text-error { color: var(--error-color); }
|
||||
.text-info { color: var(--info-color); }
|
||||
|
||||
// 字重
|
||||
.font-light { font-weight: var(--font-weight-light); }
|
||||
.font-normal { font-weight: var(--font-weight-normal); }
|
||||
.font-medium { font-weight: var(--font-weight-medium); }
|
||||
.font-semibold { font-weight: var(--font-weight-semibold); }
|
||||
.font-bold { font-weight: var(--font-weight-bold); }
|
||||
|
||||
// 圆角
|
||||
.rounded-sm { border-radius: var(--border-radius-small); }
|
||||
.rounded { border-radius: var(--border-radius-medium); }
|
||||
.rounded-lg { border-radius: var(--border-radius-large); }
|
||||
.rounded-xl { border-radius: var(--border-radius-xl); }
|
||||
.rounded-full { border-radius: 50%; }
|
||||
|
||||
// 阴影
|
||||
.shadow-sm { box-shadow: var(--shadow-light); }
|
||||
.shadow { box-shadow: var(--shadow-medium); }
|
||||
.shadow-lg { box-shadow: var(--shadow-heavy); }
|
||||
|
||||
// 动画
|
||||
.transition {
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.transition-fast {
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.transition-slow {
|
||||
transition: all var(--transition-slow);
|
||||
}
|
||||
|
||||
// 布局
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.container-sm {
|
||||
max-width: 768px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.container-lg {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
// 玻璃效果
|
||||
.glass {
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
// 悬停效果
|
||||
.hover-scale {
|
||||
transition: transform var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式
|
||||
@media (max-width: 768px) {
|
||||
.container,
|
||||
.container-sm,
|
||||
.container-lg {
|
||||
padding: 0 var(--spacing-sm);
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
}
|
||||
102
src/assets/styles/variables.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
// 颜色变量
|
||||
:root {
|
||||
// 主题色
|
||||
--primary-color: #667eea;
|
||||
--primary-color-hover: #5a67d8;
|
||||
--primary-color-light: #e6f7ff;
|
||||
|
||||
// 辅助色
|
||||
--secondary-color: #764ba2;
|
||||
--success-color: #18a058;
|
||||
--warning-color: #f5a623;
|
||||
--error-color: #d03050;
|
||||
--info-color: #2080f0;
|
||||
|
||||
// 中性色
|
||||
--text-primary: #333333;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #999999;
|
||||
--text-disabled: #cccccc;
|
||||
|
||||
// 背景色
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f5f7fa;
|
||||
--bg-tertiary: #f0f2f5;
|
||||
--bg-overlay: rgba(0, 0, 0, 0.5);
|
||||
|
||||
// 边框色
|
||||
--border-light: #e5e7eb;
|
||||
--border-medium: #d1d5db;
|
||||
--border-dark: #9ca3af;
|
||||
|
||||
// 阴影
|
||||
--shadow-light: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-heavy: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
|
||||
// 圆角
|
||||
--border-radius-small: 4px;
|
||||
--border-radius-medium: 8px;
|
||||
--border-radius-large: 12px;
|
||||
--border-radius-xl: 16px;
|
||||
|
||||
// 间距
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
--spacing-2xl: 48px;
|
||||
|
||||
// 字体
|
||||
--font-size-xs: 12px;
|
||||
--font-size-sm: 14px;
|
||||
--font-size-md: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-size-xl: 20px;
|
||||
--font-size-2xl: 24px;
|
||||
--font-size-3xl: 32px;
|
||||
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
// 行高
|
||||
--line-height-tight: 1.2;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.75;
|
||||
|
||||
// 动画
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-normal: 0.3s ease;
|
||||
--transition-slow: 0.5s ease;
|
||||
|
||||
// Z-index
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1020;
|
||||
--z-fixed: 1030;
|
||||
--z-modal-backdrop: 1040;
|
||||
--z-modal: 1050;
|
||||
--z-popover: 1060;
|
||||
--z-tooltip: 1070;
|
||||
--z-toast: 1080;
|
||||
}
|
||||
|
||||
// 暗色主题
|
||||
[data-theme="dark"] {
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-tertiary: #9ca3af;
|
||||
--text-disabled: #6b7280;
|
||||
|
||||
--bg-primary: #1f2937;
|
||||
--bg-secondary: #374151;
|
||||
--bg-tertiary: #4b5563;
|
||||
--bg-overlay: rgba(0, 0, 0, 0.7);
|
||||
|
||||
--border-light: #4b5563;
|
||||
--border-medium: #6b7280;
|
||||
--border-dark: #9ca3af;
|
||||
}
|
||||
582
src/components/DailyTaskCard.vue
Normal file
@@ -0,0 +1,582 @@
|
||||
<template>
|
||||
<div
|
||||
class="daily-task-card"
|
||||
:class="{ completed: task.completed }"
|
||||
>
|
||||
<!-- 卡片头部 -->
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<img
|
||||
:src="task.icon || '/icons/ta.png'"
|
||||
:alt="task.title"
|
||||
class="task-icon"
|
||||
>
|
||||
<div class="title-container">
|
||||
<h3 class="title">
|
||||
{{ task.title }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="task.subtitle"
|
||||
class="subtitle"
|
||||
>
|
||||
{{ task.subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="{ completed: task.completed }"
|
||||
@click="toggleStatus"
|
||||
>
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="{ completed: task.completed }"
|
||||
/>
|
||||
<span>{{ task.completed ? '已完成' : '待完成' }}</span>
|
||||
</div>
|
||||
|
||||
<n-button
|
||||
text
|
||||
class="settings-button"
|
||||
@click="showSettings = true"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon class="settings-icon">
|
||||
<Settings />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度信息 -->
|
||||
<div
|
||||
v-if="task.progress"
|
||||
class="progress-container"
|
||||
>
|
||||
<div class="info-container">
|
||||
<div class="info-item">
|
||||
<span class="info-label">当前进度</span>
|
||||
<span class="info-value">{{ task.progress.current }}/{{ task.progress.total }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.reward"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">奖励</span>
|
||||
<span class="info-value">{{ task.reward }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.nextReset"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">重置时间</span>
|
||||
<span class="info-value">{{ formatResetTime(task.nextReset) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div
|
||||
v-if="!task.completed"
|
||||
class="actions-container"
|
||||
>
|
||||
<n-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="isExecuting"
|
||||
:disabled="!task.canExecute"
|
||||
class="complete-button"
|
||||
@click="executeTask"
|
||||
>
|
||||
{{ getButtonText() }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 设置模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showSettings"
|
||||
preset="card"
|
||||
title="任务设置"
|
||||
style="width: 480px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon class="modal-icon">
|
||||
<Settings />
|
||||
</n-icon>
|
||||
<span>{{ task.title }} - 设置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="settings-content">
|
||||
<div class="settings-grid">
|
||||
<div class="setting-item">
|
||||
<n-checkbox
|
||||
v-model:checked="taskSettings.autoExecute"
|
||||
@update:checked="updateSetting('autoExecute', $event)"
|
||||
>
|
||||
自动执行
|
||||
</n-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">执行延迟 (秒)</label>
|
||||
<n-input-number
|
||||
v-model:value="taskSettings.delay"
|
||||
:min="0"
|
||||
:max="300"
|
||||
@update:value="updateSetting('delay', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<n-checkbox
|
||||
v-model:checked="taskSettings.notification"
|
||||
@update:checked="updateSetting('notification', $event)"
|
||||
>
|
||||
完成通知
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务详情 -->
|
||||
<div
|
||||
v-if="task.details"
|
||||
class="task-details"
|
||||
>
|
||||
<h4>任务详情</h4>
|
||||
<div class="task-list">
|
||||
<div
|
||||
v-for="detail in task.details"
|
||||
:key="detail.id"
|
||||
class="task-item"
|
||||
>
|
||||
<div class="task-item-left">
|
||||
<n-icon
|
||||
class="task-status-icon"
|
||||
:class="{ completed: detail.completed }"
|
||||
>
|
||||
<CheckCircle v-if="detail.completed" />
|
||||
<Clock v-else />
|
||||
</n-icon>
|
||||
<span class="task-name">{{ detail.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 执行日志 -->
|
||||
<div
|
||||
v-if="task.logs && task.logs.length"
|
||||
class="execution-log"
|
||||
>
|
||||
<h4>执行日志</h4>
|
||||
<div class="log-container">
|
||||
<div
|
||||
v-for="log in task.logs.slice(-5)"
|
||||
:key="log.id"
|
||||
class="log-item"
|
||||
>
|
||||
<span class="log-time">{{ formatLogTime(log.timestamp) }}</span>
|
||||
<span
|
||||
class="log-message"
|
||||
:class="{
|
||||
error: log.type === 'error',
|
||||
success: log.type === 'success'
|
||||
}"
|
||||
>
|
||||
{{ log.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { Settings, Checkmark as CheckCircle, Time as Clock } from '@vicons/ionicons5'
|
||||
|
||||
const props = defineProps({
|
||||
task: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:task', 'execute', 'toggle-status'])
|
||||
|
||||
const message = useMessage()
|
||||
const showSettings = ref(false)
|
||||
const isExecuting = ref(false)
|
||||
|
||||
// 任务设置
|
||||
const taskSettings = ref({
|
||||
autoExecute: props.task.settings?.autoExecute || false,
|
||||
delay: props.task.settings?.delay || 0,
|
||||
notification: props.task.settings?.notification || true
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const getButtonText = () => {
|
||||
if (isExecuting.value) return '执行中...'
|
||||
if (!props.task.canExecute) return '不可执行'
|
||||
return '立即执行'
|
||||
}
|
||||
|
||||
// 方法
|
||||
const toggleStatus = () => {
|
||||
emit('toggle-status', props.task.id)
|
||||
}
|
||||
|
||||
const executeTask = async () => {
|
||||
if (isExecuting.value || !props.task.canExecute) return
|
||||
|
||||
try {
|
||||
isExecuting.value = true
|
||||
await emit('execute', props.task.id)
|
||||
|
||||
if (taskSettings.value.notification) {
|
||||
message.success(`任务 "${props.task.title}" 执行成功`)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(`任务执行失败: ${error.message}`)
|
||||
} finally {
|
||||
isExecuting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateSetting = (key, value) => {
|
||||
taskSettings.value[key] = value
|
||||
|
||||
// 发出设置更新事件
|
||||
emit('update:task', {
|
||||
...props.task,
|
||||
settings: { ...taskSettings.value }
|
||||
})
|
||||
}
|
||||
|
||||
const formatResetTime = (timestamp) => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const formatLogTime = (timestamp) => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 监听任务设置变化
|
||||
watch(() => props.task.settings, (newSettings) => {
|
||||
if (newSettings) {
|
||||
taskSettings.value = { ...taskSettings.value, ...newSettings }
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.daily-task-card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--border-radius-large);
|
||||
box-shadow: var(--shadow-light);
|
||||
min-height: 184px;
|
||||
padding: var(--spacing-lg);
|
||||
position: relative;
|
||||
transition: all var(--transition-normal);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
border-left-color: var(--success-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: 9999px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background-color: rgba(24, 160, 88, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--text-secondary);
|
||||
border-radius: 50%;
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
&.completed {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
padding: var(--spacing-xs);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
color: var(--text-secondary);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.complete-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
&.n-button--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// 模态框样式
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-details,
|
||||
.execution-log {
|
||||
margin-top: var(--spacing-xl);
|
||||
|
||||
h4 {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.task-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-status-icon {
|
||||
color: var(--text-tertiary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&.completed {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.task-name {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
font-size: var(--font-size-sm);
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 640px) {
|
||||
.daily-task-card {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.info-container {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
751
src/components/DailyTaskStatus.vue
Normal file
@@ -0,0 +1,751 @@
|
||||
<template>
|
||||
<div class="daily-task-container">
|
||||
<div class="task-header">
|
||||
<div class="header-left">
|
||||
<img src="/icons/174023274867420.png" alt="每日任务" class="task-icon">
|
||||
<div class="title-container">
|
||||
<h3>每日任务</h3>
|
||||
<p>当前进度</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="{ completed: isAllCompleted }"
|
||||
@click="showTaskDetails = true"
|
||||
>
|
||||
<div class="status-dot" :class="{ completed: isAllCompleted }"></div>
|
||||
<span>任务详情</span>
|
||||
</div>
|
||||
|
||||
<button class="settings-button" @click="showSettings = true">
|
||||
<n-icon><Settings /></n-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-container">
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="progressPercentage"
|
||||
:height="8"
|
||||
:border-radius="4"
|
||||
:color="progressColor"
|
||||
rail-color="#f3f4f6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="info-container">
|
||||
右上角小齿轮有惊喜
|
||||
</div>
|
||||
|
||||
<!-- 一键执行按钮 -->
|
||||
<button
|
||||
class="execute-button"
|
||||
:disabled="isExecuting"
|
||||
@click="executeAllTasks"
|
||||
>
|
||||
<span v-if="isExecuting" class="loading-text">
|
||||
<svg class="loading-icon" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 22c5.421 0 10-4.579 10-10h-2c0 4.337-3.663 8-8 8s-8-3.663-8-8c0-4.336 3.663-8 8-8V2C6.579 2 2 6.58 2 12c0 5.421 4.579 10 10 10z"/>
|
||||
</svg>
|
||||
执行中...
|
||||
</span>
|
||||
<span v-else>一键补差</span>
|
||||
</button>
|
||||
|
||||
<!-- 任务设置模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showSettings"
|
||||
preset="card"
|
||||
title="任务设置"
|
||||
style="width: 400px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon><Settings /></n-icon>
|
||||
<span>任务设置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="settings-content">
|
||||
<div class="settings-grid">
|
||||
<!-- 竞技场设置 -->
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">竞技场阵容</label>
|
||||
<n-select
|
||||
v-model:value="taskSettings.arenaFormation"
|
||||
:options="formationOptions"
|
||||
size="small"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- BOSS设置 -->
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">BOSS阵容</label>
|
||||
<n-select
|
||||
v-model:value="taskSettings.bossFormation"
|
||||
:options="formationOptions"
|
||||
size="small"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- BOSS次数 -->
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">BOSS次数</label>
|
||||
<n-select
|
||||
v-model:value="taskSettings.bossTimes"
|
||||
:options="bossTimesOptions"
|
||||
size="small"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 功能开关 -->
|
||||
<div class="setting-switches">
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">领罐子</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.claimBottle"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">领挂机</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.claimHangUp"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">竞技场</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.arenaEnable"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">开宝箱</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.openBox"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">领取邮件奖励</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.claimEmail"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">付费招募</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.payRecruit"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<!-- 任务详情模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showTaskDetails"
|
||||
preset="card"
|
||||
title="每日任务详情"
|
||||
style="width: 400px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon><Calendar /></n-icon>
|
||||
<span>每日任务详情</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="task-list">
|
||||
<div
|
||||
v-for="task in dailyTasks"
|
||||
:key="task.id"
|
||||
class="task-item"
|
||||
>
|
||||
<div class="task-item-left">
|
||||
<n-icon
|
||||
class="task-status-icon"
|
||||
:class="{ completed: task.completed }"
|
||||
>
|
||||
<CheckmarkCircle v-if="task.completed" />
|
||||
<EllipseOutline v-else />
|
||||
</n-icon>
|
||||
<span class="task-name">{{ task.name }}</span>
|
||||
</div>
|
||||
<n-tag
|
||||
:type="task.completed ? 'success' : 'default'"
|
||||
size="small"
|
||||
>
|
||||
{{ task.completed ? '已完成' : '未完成' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<!-- 执行日志模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showLog"
|
||||
preset="card"
|
||||
title="任务执行日志"
|
||||
style="width: 500px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon><DocumentText /></n-icon>
|
||||
<span>任务执行日志</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="log-container" ref="logContainer">
|
||||
<div
|
||||
v-for="log in executionLogs"
|
||||
:key="log.id"
|
||||
class="log-item"
|
||||
>
|
||||
<span class="log-time">{{ formatTime(log.time) }}</span>
|
||||
<span
|
||||
class="log-message"
|
||||
:class="{
|
||||
error: log.type === 'error',
|
||||
success: log.type === 'success'
|
||||
}"
|
||||
>
|
||||
{{ log.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import {
|
||||
Settings,
|
||||
Calendar,
|
||||
CheckmarkCircle,
|
||||
EllipseOutline,
|
||||
DocumentText
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const showSettings = ref(false)
|
||||
const showTaskDetails = ref(false)
|
||||
const showLog = ref(false)
|
||||
const isExecuting = ref(false)
|
||||
const logContainer = ref(null)
|
||||
const executionLogs = ref([])
|
||||
|
||||
// 任务设置
|
||||
const taskSettings = ref({
|
||||
arenaFormation: 1,
|
||||
bossFormation: 1,
|
||||
bossTimes: 4,
|
||||
claimBottle: true,
|
||||
payRecruit: true,
|
||||
openBox: true,
|
||||
arenaEnable: true,
|
||||
claimHangUp: true,
|
||||
claimEmail: true
|
||||
})
|
||||
|
||||
// 每日任务列表
|
||||
const dailyTasks = ref([
|
||||
{ id: 1, name: '登录一次游戏', completed: false, loading: false },
|
||||
{ id: 2, name: '分享一次游戏', completed: false, loading: false },
|
||||
{ id: 3, name: '赠送好友3次金币', completed: false, loading: false },
|
||||
{ id: 4, name: '进行2次招募', completed: false, loading: false },
|
||||
{ id: 5, name: '领取5次挂机奖励', completed: false, loading: false },
|
||||
{ id: 6, name: '进行3次点金', completed: false, loading: false },
|
||||
{ id: 7, name: '开启3次宝箱', completed: false, loading: false },
|
||||
{ id: 12, name: '黑市购买1次物品(请设置采购清单)', completed: false, loading: false },
|
||||
{ id: 13, name: '进行1场竞技场战斗', completed: false, loading: false },
|
||||
{ id: 14, name: '收获1个任意盐罐', completed: false, loading: false }
|
||||
])
|
||||
|
||||
// 选项配置
|
||||
const formationOptions = [
|
||||
{ label: '阵容1', value: 1 },
|
||||
{ label: '阵容2', value: 2 },
|
||||
{ label: '阵容3', value: 3 },
|
||||
{ label: '阵容4', value: 4 }
|
||||
]
|
||||
|
||||
const bossTimesOptions = [
|
||||
{ label: '0次', value: 0 },
|
||||
{ label: '1次', value: 1 },
|
||||
{ label: '2次', value: 2 },
|
||||
{ label: '3次', value: 3 },
|
||||
{ label: '4次', value: 4 }
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const roleInfo = computed(() => {
|
||||
return tokenStore.gameData?.roleInfo
|
||||
})
|
||||
|
||||
const dailyTaskData = computed(() => {
|
||||
return roleInfo.value?.role?.dailyTask
|
||||
})
|
||||
|
||||
const progressPercentage = computed(() => {
|
||||
const current = dailyTaskData.value?.dailyPoint || 0
|
||||
return current > 100 ? 100 : current
|
||||
})
|
||||
|
||||
const isAllCompleted = computed(() => {
|
||||
return progressPercentage.value === 100
|
||||
})
|
||||
|
||||
const progressColor = computed(() => {
|
||||
return progressPercentage.value === 100 ? '#10b981' : '#3b82f6'
|
||||
})
|
||||
|
||||
// 更新任务完成状态
|
||||
const updateTaskStatus = () => {
|
||||
if (!dailyTaskData.value?.complete) return
|
||||
|
||||
const completed = dailyTaskData.value.complete
|
||||
dailyTasks.value.forEach(task => {
|
||||
const taskStatus = completed[task.id.toString()]
|
||||
task.completed = taskStatus === -1 // -1 表示已完成
|
||||
})
|
||||
}
|
||||
|
||||
// 添加日志
|
||||
const addLog = (message, type = 'info') => {
|
||||
const log = {
|
||||
id: Date.now() + Math.random(),
|
||||
time: new Date().toLocaleTimeString(),
|
||||
message,
|
||||
type
|
||||
}
|
||||
executionLogs.value.push(log)
|
||||
|
||||
// 自动滚动到底部
|
||||
nextTick(() => {
|
||||
if (logContainer.value) {
|
||||
logContainer.value.scrollTop = logContainer.value.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const saveSettings = () => {
|
||||
// 这里可以保存到 localStorage 或发送到服务器
|
||||
localStorage.setItem('taskSettings', JSON.stringify(taskSettings.value))
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeString) => {
|
||||
return timeString
|
||||
}
|
||||
|
||||
// 执行所有任务
|
||||
const executeAllTasks = async () => {
|
||||
if (!tokenStore.selectedToken || isExecuting.value) return
|
||||
|
||||
isExecuting.value = true
|
||||
showLog.value = true
|
||||
executionLogs.value = []
|
||||
|
||||
addLog('开始执行任务...')
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 获取角色信息
|
||||
addLog('获取角色信息...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'role_getroleinfo')
|
||||
addLog('获取角色信息成功', 'success')
|
||||
|
||||
// 执行各种任务
|
||||
if (taskSettings.value.claimHangUp) {
|
||||
addLog('领取挂机奖励...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'system_claimhangupreward')
|
||||
}
|
||||
|
||||
if (taskSettings.value.claimBottle) {
|
||||
addLog('领取盐罐奖励...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'bottlehelper_claim')
|
||||
}
|
||||
|
||||
if (taskSettings.value.payRecruit) {
|
||||
addLog('进行招募...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'hero_recruit', {
|
||||
byClub: false,
|
||||
recruitNumber: 1,
|
||||
recruitType: 3
|
||||
})
|
||||
}
|
||||
|
||||
if (taskSettings.value.openBox) {
|
||||
addLog('开启宝箱...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'item_openbox', {
|
||||
itemId: 2001,
|
||||
number: 3
|
||||
})
|
||||
}
|
||||
|
||||
if (taskSettings.value.arenaEnable) {
|
||||
addLog('进行竞技场战斗...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'fight_startareaarena', {
|
||||
targetId: 530479307
|
||||
})
|
||||
}
|
||||
|
||||
if (taskSettings.value.claimEmail) {
|
||||
addLog('领取邮件奖励...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'mail_claimallattachment', {
|
||||
category: 0
|
||||
})
|
||||
}
|
||||
|
||||
// 最后再次获取角色信息更新状态
|
||||
addLog('更新角色信息...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'role_getroleinfo')
|
||||
|
||||
addLog('任务执行完成!', 'success')
|
||||
message.success('任务处理完成')
|
||||
|
||||
} catch (error) {
|
||||
addLog(`任务执行失败: ${error.message}`, 'error')
|
||||
message.error('任务执行失败')
|
||||
} finally {
|
||||
isExecuting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听角色信息变化
|
||||
watch(dailyTaskData, () => {
|
||||
updateTaskStatus()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 初始化设置
|
||||
const initSettings = () => {
|
||||
const saved = localStorage.getItem('taskSettings')
|
||||
if (saved) {
|
||||
try {
|
||||
taskSettings.value = { ...taskSettings.value, ...JSON.parse(saved) }
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initSettings()
|
||||
updateTaskStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.daily-task-container {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-full);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
padding: var(--spacing-xs);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-medium);
|
||||
transition: background var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-container {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.execute-button {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-medium);
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 模态框样式
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.setting-switches {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.switch-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.task-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-status-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
&.completed {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) 0;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--text-tertiary);
|
||||
min-width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.task-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
673
src/components/GameStatus.vue
Normal file
@@ -0,0 +1,673 @@
|
||||
<template>
|
||||
<div class="game-status-container">
|
||||
<!-- 队伍状态 -->
|
||||
<TeamStatus />
|
||||
|
||||
<!-- 每日任务状态 -->
|
||||
<DailyTaskStatus />
|
||||
|
||||
<!-- 咸将塔状态 -->
|
||||
<TowerStatus />
|
||||
|
||||
<!-- 其他游戏状态卡片 -->
|
||||
<!-- 盐罐机器人状态 -->
|
||||
<div class="status-card bottle-helper">
|
||||
<div class="card-header">
|
||||
<img src="/icons/173746572831736.png" alt="盐罐图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>盐罐机器人</h3>
|
||||
<p>剩余时间</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: bottleHelper.isRunning }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ bottleHelper.isRunning ? '运行中' : '已停止' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="time-display">
|
||||
{{ formatTime(bottleHelper.remainingTime) }}
|
||||
</div>
|
||||
<button
|
||||
class="action-button"
|
||||
:class="{ active: bottleHelper.isRunning }"
|
||||
@click="handleBottleHelper"
|
||||
>
|
||||
{{ bottleHelper.isRunning ? '重启服务' : '启动服务' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 挂机状态 -->
|
||||
<div class="status-card hang-up">
|
||||
<div class="card-header">
|
||||
<img src="/icons/174061875626614.png" alt="挂机图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>挂机时间</h3>
|
||||
<p>已挂机:{{ formatTime(hangUp.elapsedTime) }}</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: hangUp.isActive }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ hangUp.isActive ? '挂机中' : '已完成' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="time-display">
|
||||
{{ formatTime(hangUp.remainingTime) }}
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button class="action-button secondary" @click="extendHangUp">
|
||||
加钟
|
||||
</button>
|
||||
<button class="action-button primary" @click="claimHangUpReward">
|
||||
领取奖励
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 俱乐部排位 -->
|
||||
<div class="status-card legion-match">
|
||||
<div class="card-header">
|
||||
<img src="/icons/1733492491706152.png" alt="俱乐部图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>俱乐部排位</h3>
|
||||
<p>赛事状态</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: legionMatch.isRegistered }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ legionMatch.isRegistered ? '已报名' : '未报名' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="description">
|
||||
每逢周三周四周五有比赛<br>
|
||||
立即报名参与精彩对决!
|
||||
</p>
|
||||
<button
|
||||
class="action-button"
|
||||
:disabled="legionMatch.isRegistered"
|
||||
@click="registerLegionMatch"
|
||||
>
|
||||
{{ legionMatch.isRegistered ? '已报名' : '立即报名' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 俱乐部签到 -->
|
||||
<div class="status-card legion-signin">
|
||||
<div class="card-header">
|
||||
<img src="/icons/1733492491706148.png" alt="签到图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>俱乐部签到</h3>
|
||||
<p>每日签到状态</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: legionSignin.isSignedIn }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ legionSignin.isSignedIn ? '已签到' : '待签到' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="club-name" v-if="legionSignin.clubName">
|
||||
当前俱乐部<br>
|
||||
<strong>{{ legionSignin.clubName }}</strong>
|
||||
</p>
|
||||
<p class="description" v-else>
|
||||
尚未加入任何俱乐部
|
||||
</p>
|
||||
<button
|
||||
class="action-button"
|
||||
:disabled="legionSignin.isSignedIn"
|
||||
@click="signInLegion"
|
||||
>
|
||||
{{ legionSignin.isSignedIn ? '已签到' : '立即签到' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 咸鱼大冲关 -->
|
||||
<div class="status-card study">
|
||||
<div class="card-header">
|
||||
<img src="/icons/1736425783912140.png" alt="学习图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>咸鱼大冲关</h3>
|
||||
<p>每日知识挑战</p>
|
||||
</div>
|
||||
<div class="status-badge weekly">
|
||||
<div class="status-dot"></div>
|
||||
<span>每周任务</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="description">
|
||||
没有什么可以阻挡我求知的欲望!
|
||||
</p>
|
||||
<button
|
||||
class="action-button"
|
||||
:disabled="study.isAnswering"
|
||||
@click="startStudy"
|
||||
>
|
||||
<span v-if="study.isAnswering" class="loading-text">
|
||||
<svg class="loading-icon" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 22c5.421 0 10-4.579 10-10h-2c0 4.337-3.663 8-8 8s-8-3.663-8-8c0-4.336 3.663-8 8-8V2C6.579 2 2 6.58 2 12c0 5.421 4.579 10 10 10z"/>
|
||||
</svg>
|
||||
答题中...
|
||||
</span>
|
||||
<span v-else>一键答题</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import TeamStatus from './TeamStatus.vue'
|
||||
import DailyTaskStatus from './DailyTaskStatus.vue'
|
||||
import TowerStatus from './TowerStatus.vue'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const bottleHelper = ref({
|
||||
isRunning: false,
|
||||
remainingTime: 0,
|
||||
stopTime: 0
|
||||
})
|
||||
|
||||
const hangUp = ref({
|
||||
isActive: false,
|
||||
remainingTime: 0,
|
||||
elapsedTime: 0,
|
||||
lastTime: 0,
|
||||
hangUpTime: 0
|
||||
})
|
||||
|
||||
const legionMatch = ref({
|
||||
isRegistered: false
|
||||
})
|
||||
|
||||
const legionSignin = ref({
|
||||
isSignedIn: false,
|
||||
clubName: ''
|
||||
})
|
||||
|
||||
const study = ref({
|
||||
isAnswering: false
|
||||
})
|
||||
|
||||
|
||||
// 计算属性
|
||||
const roleInfo = computed(() => {
|
||||
return tokenStore.gameData?.roleInfo || null
|
||||
})
|
||||
|
||||
// 格式化时间 - 确保显示到秒
|
||||
const formatTime = (seconds) => {
|
||||
// 确保传入值为数字,并向下取整到秒
|
||||
const totalSeconds = Math.floor(Number(seconds) || 0)
|
||||
|
||||
if (totalSeconds <= 0) return '00:00:00'
|
||||
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const secs = totalSeconds % 60
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
const updateGameStatus = () => {
|
||||
if (!roleInfo.value) return
|
||||
|
||||
const role = roleInfo.value.role
|
||||
|
||||
// 更新盐罐机器人状态
|
||||
if (role.bottleHelpers) {
|
||||
const now = Date.now() / 1000
|
||||
bottleHelper.value.stopTime = role.bottleHelpers.helperStopTime
|
||||
bottleHelper.value.isRunning = role.bottleHelpers.helperStopTime > now
|
||||
// 确保剩余时间为整数秒
|
||||
bottleHelper.value.remainingTime = Math.max(0, Math.floor(role.bottleHelpers.helperStopTime - now))
|
||||
console.log('🤖 盐罐机器人状态更新:', {
|
||||
stopTime: role.bottleHelpers.helperStopTime,
|
||||
now: now,
|
||||
remainingTime: bottleHelper.value.remainingTime,
|
||||
isRunning: bottleHelper.value.isRunning
|
||||
})
|
||||
}
|
||||
|
||||
// 更新挂机状态
|
||||
if (role.hangUp) {
|
||||
const now = Date.now() / 1000
|
||||
hangUp.value.lastTime = role.hangUp.lastTime
|
||||
hangUp.value.hangUpTime = role.hangUp.hangUpTime
|
||||
|
||||
const elapsed = now - hangUp.value.lastTime
|
||||
if (elapsed <= hangUp.value.hangUpTime) {
|
||||
// 确保剩余时间为整数秒
|
||||
hangUp.value.remainingTime = Math.floor(hangUp.value.hangUpTime - elapsed)
|
||||
hangUp.value.isActive = true
|
||||
} else {
|
||||
hangUp.value.remainingTime = 0
|
||||
hangUp.value.isActive = false
|
||||
}
|
||||
// 确保已挂机时间为整数秒
|
||||
hangUp.value.elapsedTime = Math.floor(hangUp.value.hangUpTime - hangUp.value.remainingTime)
|
||||
|
||||
console.log('⏰ 挂机状态更新:', {
|
||||
lastTime: hangUp.value.lastTime,
|
||||
hangUpTime: hangUp.value.hangUpTime,
|
||||
elapsed: elapsed,
|
||||
remainingTime: hangUp.value.remainingTime,
|
||||
elapsedTime: hangUp.value.elapsedTime,
|
||||
isActive: hangUp.value.isActive
|
||||
})
|
||||
}
|
||||
|
||||
// 更新俱乐部排位状态
|
||||
if (role.statistics) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const todayTimestamp = today.getTime() / 1000
|
||||
|
||||
legionMatch.value.isRegistered =
|
||||
Number(role.statistics['last:legion:match:sign:up:time']) > todayTimestamp
|
||||
}
|
||||
|
||||
// 更新俱乐部签到状态
|
||||
if (role.statisticsTime) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const todayTimestamp = today.getTime() / 1000
|
||||
|
||||
legionSignin.value.isSignedIn =
|
||||
role.statisticsTime['legion:sign:in'] > todayTimestamp
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 定时器更新
|
||||
let timer = null
|
||||
const startTimer = () => {
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
// 更新盐罐机器人剩余时间
|
||||
if (bottleHelper.value.isRunning && bottleHelper.value.remainingTime > 0) {
|
||||
bottleHelper.value.remainingTime = Math.max(0, bottleHelper.value.remainingTime - 1)
|
||||
if (bottleHelper.value.remainingTime <= 0) {
|
||||
bottleHelper.value.isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新挂机剩余时间
|
||||
if (hangUp.value.isActive && hangUp.value.remainingTime > 0) {
|
||||
hangUp.value.remainingTime = Math.max(0, hangUp.value.remainingTime - 1)
|
||||
hangUp.value.elapsedTime = hangUp.value.elapsedTime + 1
|
||||
if (hangUp.value.remainingTime <= 0) {
|
||||
hangUp.value.isActive = false
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 盐罐机器人操作
|
||||
const handleBottleHelper = () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 停止后重启
|
||||
tokenStore.sendMessage(tokenId, 'bottlehelper_stop')
|
||||
setTimeout(() => {
|
||||
tokenStore.sendMessage(tokenId, 'bottlehelper_start')
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
}, 500)
|
||||
|
||||
message.info(bottleHelper.value.isRunning ? '重启盐罐机器人' : '启动盐罐机器人')
|
||||
}
|
||||
|
||||
// 挂机操作
|
||||
const extendHangUp = () => {
|
||||
if (!tokenStore.selectedToken) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 发送4次分享回调请求来加钟
|
||||
for (let i = 0; i < 4; i++) {
|
||||
setTimeout(() => {
|
||||
tokenStore.sendMessage(tokenId, 'system_mysharecallback', {
|
||||
isSkipShareCard: true,
|
||||
type: 2
|
||||
})
|
||||
}, i * 200)
|
||||
}
|
||||
|
||||
message.info('正在加钟...')
|
||||
}
|
||||
|
||||
const claimHangUpReward = () => {
|
||||
if (!tokenStore.selectedToken) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 领取挂机奖励
|
||||
tokenStore.sendMessage(tokenId, 'system_mysharecallback')
|
||||
tokenStore.sendMessage(tokenId, 'system_claimhangupreward')
|
||||
tokenStore.sendMessage(tokenId, 'system_mysharecallback', {
|
||||
isSkipShareCard: true,
|
||||
type: 2
|
||||
})
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
|
||||
message.info('领取挂机奖励')
|
||||
}
|
||||
|
||||
// 俱乐部排位报名
|
||||
const registerLegionMatch = () => {
|
||||
if (!tokenStore.selectedToken || legionMatch.value.isRegistered) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'legionmatch_rolesignup')
|
||||
|
||||
message.info('报名俱乐部排位')
|
||||
}
|
||||
|
||||
// 俱乐部签到
|
||||
const signInLegion = () => {
|
||||
if (!tokenStore.selectedToken || legionSignin.value.isSignedIn) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'legion_signin')
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
|
||||
message.info('俱乐部签到')
|
||||
}
|
||||
|
||||
// 学习答题
|
||||
const startStudy = () => {
|
||||
if (!tokenStore.selectedToken || study.value.isAnswering) return
|
||||
|
||||
study.value.isAnswering = true
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'study_startgame')
|
||||
|
||||
setTimeout(() => {
|
||||
study.value.isAnswering = false
|
||||
}, 3000)
|
||||
|
||||
message.info('开始答题')
|
||||
}
|
||||
|
||||
|
||||
// 监听角色信息变化
|
||||
watch(roleInfo, (newValue) => {
|
||||
if (newValue) {
|
||||
updateGameStatus()
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
updateGameStatus()
|
||||
startTimer()
|
||||
|
||||
// 获取俱乐部信息
|
||||
if (tokenStore.selectedToken) {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'legion_getinfo')
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.game-status-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
min-height: 200px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-full);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: rgba(156, 163, 175, 0.1);
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.active {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.weekly {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
&.energy {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.energy-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
.time-display {
|
||||
font-size: 1.5rem; /* text-2xl */
|
||||
font-weight: 700; /* font-bold */
|
||||
color: #111827; /* text-gray-900 */
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', monospace;
|
||||
letter-spacing: 0.1em;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.club-name {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.tower-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
.label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.tower-level {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-medium);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--primary-color-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: var(--secondary-color);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--secondary-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--info-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--info-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.game-status-container {
|
||||
grid-template-columns: 1fr;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-card {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
319
src/components/MessageTester.vue
Normal file
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div class="message-tester">
|
||||
<n-card
|
||||
title="消息加解密测试"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<!-- 选择Token -->
|
||||
<div>
|
||||
<n-select
|
||||
v-model:value="selectedTokenId"
|
||||
:options="tokenOptions"
|
||||
placeholder="选择要测试的Token"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket连接状态 -->
|
||||
<div v-if="selectedTokenId">
|
||||
<n-tag :type="wsStatusType">
|
||||
{{ wsStatusText }}
|
||||
</n-tag>
|
||||
<n-button
|
||||
v-if="wsStatus !== 'connected'"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
@click="connectWebSocket"
|
||||
>
|
||||
连接WebSocket
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 预设消息测试 -->
|
||||
<n-divider title-placement="left">
|
||||
预设消息测试
|
||||
</n-divider>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendHeartbeat"
|
||||
>
|
||||
💗 发送心跳
|
||||
</n-button>
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendGetRoleInfo"
|
||||
>
|
||||
👤 获取角色信息
|
||||
</n-button>
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendGetDataVersion"
|
||||
>
|
||||
📦 获取数据版本
|
||||
</n-button>
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendSignIn"
|
||||
>
|
||||
📅 签到
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 自定义消息发送 -->
|
||||
<n-divider title-placement="left">
|
||||
自定义消息
|
||||
</n-divider>
|
||||
<div class="space-y-2">
|
||||
<n-input
|
||||
v-model:value="customCmd"
|
||||
placeholder="命令 (例如: role_getroleinfo)"
|
||||
class="w-full"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="customBody"
|
||||
type="textarea"
|
||||
placeholder="消息体 JSON (例如: {"clientVersion": "1.65.3-wx"})"
|
||||
:rows="3"
|
||||
class="w-full"
|
||||
/>
|
||||
<n-button
|
||||
:disabled="!canSendMessage || !customCmd"
|
||||
type="primary"
|
||||
@click="sendCustomMessage"
|
||||
>
|
||||
🚀 发送自定义消息
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 消息历史 -->
|
||||
<n-divider title-placement="left">
|
||||
消息历史
|
||||
</n-divider>
|
||||
<div class="message-history max-h-96 overflow-y-auto">
|
||||
<div
|
||||
v-for="(message, index) in messageHistory"
|
||||
:key="index"
|
||||
class="message-item p-3 mb-2 rounded border"
|
||||
:class="message.type === 'sent' ? 'bg-blue-50 border-blue-200' : 'bg-green-50 border-green-200'"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-semibold">
|
||||
{{ message.type === 'sent' ? '📤 发送' : '📨 接收' }}
|
||||
<span class="text-sm text-gray-500 ml-2">{{ formatTime(message.timestamp) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.cmd"
|
||||
class="text-sm"
|
||||
>
|
||||
<strong>命令:</strong> {{ message.cmd }}
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<n-collapse>
|
||||
<n-collapse-item
|
||||
title="查看详细数据"
|
||||
name="detail"
|
||||
>
|
||||
<pre class="text-xs bg-gray-100 p-2 rounded overflow-x-auto">{{ formatJSON(message.data) }}</pre>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="messageHistory.length === 0"
|
||||
class="text-center text-gray-500"
|
||||
>
|
||||
暂无消息历史
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useTokenStore } from '../stores/tokenStore'
|
||||
import { useGameRolesStore } from '../stores/gameRoles'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const gameRolesStore = useGameRolesStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const selectedTokenId = ref(null)
|
||||
const customCmd = ref('')
|
||||
const customBody = ref('{}')
|
||||
const messageHistory = ref([])
|
||||
|
||||
// 计算属性
|
||||
const tokenOptions = computed(() => {
|
||||
return gameRolesStore.gameRoles.map(role => ({
|
||||
label: role.name,
|
||||
value: role.id
|
||||
}))
|
||||
})
|
||||
|
||||
const wsStatus = computed(() => {
|
||||
return selectedTokenId.value ? tokenStore.getWebSocketStatus(selectedTokenId.value) : 'disconnected'
|
||||
})
|
||||
|
||||
const wsStatusType = computed(() => {
|
||||
switch (wsStatus.value) {
|
||||
case 'connected': return 'success'
|
||||
case 'connecting': return 'warning'
|
||||
case 'error': return 'error'
|
||||
default: return 'default'
|
||||
}
|
||||
})
|
||||
|
||||
const wsStatusText = computed(() => {
|
||||
switch (wsStatus.value) {
|
||||
case 'connected': return '🟢 已连接'
|
||||
case 'connecting': return '🟡 连接中'
|
||||
case 'error': return '🔴 连接错误'
|
||||
default: return '⚪ 未连接'
|
||||
}
|
||||
})
|
||||
|
||||
const canSendMessage = computed(() => {
|
||||
return selectedTokenId.value && wsStatus.value === 'connected'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const connectWebSocket = () => {
|
||||
if (!selectedTokenId.value) return
|
||||
|
||||
const role = gameRolesStore.gameRoles.find(r => r.id === selectedTokenId.value)
|
||||
if (role) {
|
||||
gameRolesStore.selectRole(role)
|
||||
message.success('正在建立WebSocket连接...')
|
||||
}
|
||||
}
|
||||
|
||||
const addToHistory = (type, data, cmd = null) => {
|
||||
messageHistory.value.unshift({
|
||||
type,
|
||||
timestamp: new Date().toISOString(),
|
||||
cmd,
|
||||
data
|
||||
})
|
||||
|
||||
// 保持历史记录在合理范围内
|
||||
if (messageHistory.value.length > 50) {
|
||||
messageHistory.value = messageHistory.value.slice(0, 50)
|
||||
}
|
||||
}
|
||||
|
||||
const sendHeartbeat = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendHeartbeat(selectedTokenId.value)
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: '_sys/ack' }, '_sys/ack')
|
||||
message.success('心跳消息已发送')
|
||||
} else {
|
||||
message.error('心跳消息发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendGetRoleInfo = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendGetRoleInfo(selectedTokenId.value)
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: 'role_getroleinfo' }, 'role_getroleinfo')
|
||||
message.success('角色信息请求已发送')
|
||||
} else {
|
||||
message.error('角色信息请求发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendGetDataVersion = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendGameMessage(selectedTokenId.value, 'system_getdatabundlever', { isAudit: false })
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: 'system_getdatabundlever' }, 'system_getdatabundlever')
|
||||
message.success('数据版本请求已发送')
|
||||
} else {
|
||||
message.error('数据版本请求发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendSignIn = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendGameMessage(selectedTokenId.value, 'system_signinreward', {})
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: 'system_signinreward' }, 'system_signinreward')
|
||||
message.success('签到请求已发送')
|
||||
} else {
|
||||
message.error('签到请求发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendCustomMessage = () => {
|
||||
if (!canSendMessage.value || !customCmd.value) return
|
||||
|
||||
try {
|
||||
const body = JSON.parse(customBody.value || '{}')
|
||||
const success = tokenStore.sendGameMessage(selectedTokenId.value, customCmd.value, body)
|
||||
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: customCmd.value, body }, customCmd.value)
|
||||
message.success(`自定义消息 ${customCmd.value} 已发送`)
|
||||
|
||||
// 清空输入
|
||||
customCmd.value = ''
|
||||
customBody.value = '{}'
|
||||
} else {
|
||||
message.error('自定义消息发送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('消息体JSON格式错误: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleTimeString()
|
||||
}
|
||||
|
||||
const formatJSON = (data) => {
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
|
||||
// 监听WebSocket消息(模拟,实际需要在tokenStore中触发事件)
|
||||
watch(() => tokenStore.wsConnections, (connections) => {
|
||||
if (!selectedTokenId.value || !connections[selectedTokenId.value]) return
|
||||
|
||||
const connection = connections[selectedTokenId.value]
|
||||
if (connection.lastMessage) {
|
||||
const lastMessage = connection.lastMessage
|
||||
addToHistory('received', lastMessage.parsed, lastMessage.parsed?.cmd)
|
||||
}
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-tester {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.message-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
454
src/components/TeamStatus.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div class="team-status-card">
|
||||
<div class="card-header">
|
||||
<img src="/icons/Ob7pyorzmHiJcbab2c25af264d0758b527bc1b61cc3b.png" alt="队伍图标" class="team-icon">
|
||||
<div class="team-info">
|
||||
<h3>队伍阵容</h3>
|
||||
<p>当前使用的战斗阵容</p>
|
||||
</div>
|
||||
<div class="team-selector">
|
||||
<button
|
||||
v-for="teamId in availableTeams"
|
||||
:key="teamId"
|
||||
@click="selectTeam(teamId)"
|
||||
:class="[
|
||||
'team-button',
|
||||
{ active: currentTeam === teamId }
|
||||
]"
|
||||
>
|
||||
{{ teamId }}
|
||||
</button>
|
||||
<button
|
||||
@click="refreshTeamData"
|
||||
class="team-button refresh-button"
|
||||
title="刷新队伍数据"
|
||||
>
|
||||
🔄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="team-display">
|
||||
<div class="current-team-info">
|
||||
<span class="label">当前阵容</span>
|
||||
<span class="team-number">阵容 {{ currentTeam }}</span>
|
||||
</div>
|
||||
|
||||
<div class="heroes-container">
|
||||
<div class="heroes-grid">
|
||||
<div
|
||||
v-for="hero in currentTeamHeroes"
|
||||
:key="hero.id || hero.name"
|
||||
class="hero-card"
|
||||
>
|
||||
<img
|
||||
v-if="hero.avatar"
|
||||
:src="hero.avatar"
|
||||
:alt="hero.name"
|
||||
class="hero-avatar"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="hero-placeholder"
|
||||
>
|
||||
{{ hero.name?.substring(0, 2) || '?' }}
|
||||
</div>
|
||||
<span class="hero-name">{{ hero.name || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!currentTeamHeroes.length" class="empty-team">
|
||||
<p>暂无队伍信息</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const currentTeam = ref(1)
|
||||
const availableTeams = ref([1, 2, 3, 4])
|
||||
|
||||
// 计算属性
|
||||
const presetTeamInfo = computed(() => {
|
||||
return tokenStore.gameData?.presetTeam || null
|
||||
})
|
||||
|
||||
const currentTeamHeroes = computed(() => {
|
||||
if (!presetTeamInfo.value) {
|
||||
console.log('👥 TeamStatus: presetTeamInfo 为空')
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('👥 TeamStatus: 当前队伍信息结构:', {
|
||||
presetTeamInfo: presetTeamInfo.value,
|
||||
currentTeam: currentTeam.value,
|
||||
hasPresetTeamInfo: !!presetTeamInfo.value.presetTeamInfo,
|
||||
presetTeamInfoKeys: presetTeamInfo.value.presetTeamInfo ? Object.keys(presetTeamInfo.value.presetTeamInfo) : []
|
||||
})
|
||||
|
||||
// 尝试多种可能的数据结构
|
||||
let teamData = null
|
||||
|
||||
// 方式1: 标准结构 presetTeamInfo[teamId].teamInfo
|
||||
if (presetTeamInfo.value.presetTeamInfo?.[currentTeam.value]?.teamInfo) {
|
||||
teamData = presetTeamInfo.value.presetTeamInfo[currentTeam.value].teamInfo
|
||||
console.log('👥 TeamStatus: 使用标准结构获取队伍数据')
|
||||
}
|
||||
// 方式2: 直接在presetTeamInfo[teamId]下
|
||||
else if (presetTeamInfo.value.presetTeamInfo?.[currentTeam.value]) {
|
||||
const teamInfo = presetTeamInfo.value.presetTeamInfo[currentTeam.value]
|
||||
if (typeof teamInfo === 'object' && !Array.isArray(teamInfo)) {
|
||||
teamData = teamInfo
|
||||
console.log('👥 TeamStatus: 使用直接结构获取队伍数据')
|
||||
}
|
||||
}
|
||||
// 方式3: 查找任何包含英雄数据的结构
|
||||
else if (presetTeamInfo.value.presetTeamInfo) {
|
||||
for (const [key, value] of Object.entries(presetTeamInfo.value.presetTeamInfo)) {
|
||||
if (value && typeof value === 'object') {
|
||||
// 查找包含heroId或类似字段的数据
|
||||
if (value.teamInfo || value.heroes || value.formation ||
|
||||
Object.values(value).some(v => v && v.heroId)) {
|
||||
teamData = value.teamInfo || value.heroes || value.formation || value
|
||||
console.log(`👥 TeamStatus: 在 ${key} 中找到队伍数据`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!teamData) {
|
||||
console.log('👥 TeamStatus: 未找到队伍数据')
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('👥 TeamStatus: 解析队伍数据:', teamData)
|
||||
|
||||
// 转换队伍信息为英雄数组
|
||||
const heroes = []
|
||||
|
||||
// 处理不同的数据格式
|
||||
if (Array.isArray(teamData)) {
|
||||
// 数组格式
|
||||
teamData.forEach((hero, index) => {
|
||||
if (hero && (hero.heroId || hero.id)) {
|
||||
heroes.push({
|
||||
id: hero.heroId || hero.id,
|
||||
name: getHeroName(hero.heroId || hero.id),
|
||||
position: index + 1,
|
||||
level: hero.level || 1
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (typeof teamData === 'object') {
|
||||
// 对象格式(position => hero)
|
||||
for (const [position, hero] of Object.entries(teamData)) {
|
||||
if (hero && (hero.heroId || hero.id)) {
|
||||
heroes.push({
|
||||
id: hero.heroId || hero.id,
|
||||
name: getHeroName(hero.heroId || hero.id),
|
||||
position: position,
|
||||
level: hero.level || 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('👥 TeamStatus: 解析出的英雄列表:', heroes)
|
||||
return heroes
|
||||
})
|
||||
|
||||
// 从presetTeamInfo获取可用队伍数量
|
||||
const updateAvailableTeams = () => {
|
||||
if (!presetTeamInfo.value?.presetTeamInfo) return
|
||||
|
||||
const teams = Object.keys(presetTeamInfo.value.presetTeamInfo)
|
||||
.map(Number)
|
||||
.filter(num => !isNaN(num))
|
||||
.sort((a, b) => a - b)
|
||||
|
||||
if (teams.length > 0) {
|
||||
availableTeams.value = teams
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前队伍
|
||||
const updateCurrentTeam = () => {
|
||||
if (presetTeamInfo.value?.presetTeamInfo?.useTeamId) {
|
||||
currentTeam.value = presetTeamInfo.value.presetTeamInfo.useTeamId
|
||||
}
|
||||
}
|
||||
|
||||
// 获取英雄名称(这里需要英雄数据字典)
|
||||
const getHeroName = (heroId) => {
|
||||
// 暂时返回英雄ID,后续可以添加英雄名称映射
|
||||
const heroNames = {
|
||||
1: '剑士',
|
||||
2: '法师',
|
||||
3: '弓手',
|
||||
4: '盗贼',
|
||||
5: '牧师'
|
||||
// 更多英雄映射...
|
||||
}
|
||||
|
||||
return heroNames[heroId] || `英雄${heroId}`
|
||||
}
|
||||
|
||||
// 选择队伍
|
||||
const selectTeam = (teamId) => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
currentTeam.value = teamId
|
||||
|
||||
// 发送切换队伍的消息
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'presetteam_saveteam', { teamId })
|
||||
|
||||
message.info(`切换到阵容 ${teamId}`)
|
||||
}
|
||||
|
||||
// 监听预设队伍信息变化
|
||||
watch(presetTeamInfo, (newValue) => {
|
||||
if (newValue) {
|
||||
updateAvailableTeams()
|
||||
updateCurrentTeam()
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 刷新队伍数据
|
||||
const refreshTeamData = () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
console.log('👥 手动刷新队伍数据')
|
||||
|
||||
// 发送多个可能的队伍相关命令
|
||||
const commands = [
|
||||
'presetteam_getteam',
|
||||
'role_gettargetteam',
|
||||
'role_getroleinfo' // 角色信息中可能包含队伍数据
|
||||
]
|
||||
|
||||
commands.forEach(cmd => {
|
||||
tokenStore.sendMessage(tokenId, cmd, {})
|
||||
console.log(`👥 发送命令: ${cmd}`)
|
||||
})
|
||||
|
||||
message.info('正在刷新队伍数据...')
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 获取队伍信息
|
||||
refreshTeamData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.team-status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.team-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-info {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.team-selector {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.team-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.refresh-button {
|
||||
background: var(--success-color, #10b981);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: var(--success-color-dark, #059669);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
.current-team-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.team-number {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.heroes-container {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.heroes-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--border-radius-medium);
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-fast);
|
||||
min-width: 80px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hero-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.empty-team {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.team-selector {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.heroes-grid {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
507
src/components/TokenManager.vue
Normal file
@@ -0,0 +1,507 @@
|
||||
<template>
|
||||
<div class="token-manager">
|
||||
<div class="header">
|
||||
<h3>Token管理器</h3>
|
||||
<div class="header-actions">
|
||||
<n-button
|
||||
size="small"
|
||||
@click="refreshTokens"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><Refresh /></n-icon>
|
||||
</template>
|
||||
刷新
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="warning"
|
||||
@click="exportTokens"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><Download /></n-icon>
|
||||
</template>
|
||||
导出
|
||||
</n-button>
|
||||
<n-upload
|
||||
:show-file-list="false"
|
||||
accept=".json"
|
||||
@change="importTokens"
|
||||
>
|
||||
<n-button
|
||||
size="small"
|
||||
type="info"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><CloudUpload /></n-icon>
|
||||
</template>
|
||||
导入
|
||||
</n-button>
|
||||
</n-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户Token -->
|
||||
<div class="token-section">
|
||||
<h4>用户认证Token</h4>
|
||||
<div
|
||||
v-if="localTokenStore.userToken"
|
||||
class="token-item"
|
||||
>
|
||||
<div class="token-info">
|
||||
<span class="token-label">Token:</span>
|
||||
<span class="token-value">{{ maskToken(localTokenStore.userToken) }}</span>
|
||||
</div>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="error"
|
||||
@click="clearUserToken"
|
||||
>
|
||||
清除
|
||||
</n-button>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="empty-token"
|
||||
>
|
||||
<span>未设置用户Token</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏Token列表 -->
|
||||
<div class="token-section">
|
||||
<h4>游戏角色Token ({{ Object.keys(localTokenStore.gameTokens).length }}个)</h4>
|
||||
<div class="game-tokens-list">
|
||||
<div
|
||||
v-for="(tokenData, roleId) in localTokenStore.gameTokens"
|
||||
:key="roleId"
|
||||
class="game-token-item"
|
||||
>
|
||||
<div class="token-header">
|
||||
<div class="role-info">
|
||||
<span class="role-name">{{ tokenData.roleName }}</span>
|
||||
<span class="role-server">{{ tokenData.server }}</span>
|
||||
</div>
|
||||
<div class="token-actions">
|
||||
<n-button
|
||||
size="tiny"
|
||||
:type="getWSStatus(roleId) === 'connected' ? 'success' : 'default'"
|
||||
@click="toggleWebSocket(roleId, tokenData)"
|
||||
>
|
||||
{{ getWSStatus(roleId) === 'connected' ? '断开WS' : '连接WS' }}
|
||||
</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="warning"
|
||||
@click="regenerateToken(roleId)"
|
||||
>
|
||||
重生成
|
||||
</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="error"
|
||||
@click="removeToken(roleId)"
|
||||
>
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="token-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Token:</span>
|
||||
<span class="detail-value">{{ maskToken(tokenData.token) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">WebSocket URL:</span>
|
||||
<span class="detail-value">{{ tokenData.wsUrl }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">创建时间:</span>
|
||||
<span class="detail-value">{{ formatTime(tokenData.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">最后使用:</span>
|
||||
<span class="detail-value">{{ formatTime(tokenData.lastUsed) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">连接状态:</span>
|
||||
<n-tag
|
||||
size="small"
|
||||
:type="getWSStatusType(getWSStatus(roleId))"
|
||||
>
|
||||
{{ getWSStatusText(getWSStatus(roleId)) }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作 -->
|
||||
<div class="bulk-actions">
|
||||
<n-button
|
||||
type="warning"
|
||||
@click="cleanExpiredTokens"
|
||||
>
|
||||
清理过期Token
|
||||
</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="clearAllTokens"
|
||||
>
|
||||
清除所有Token
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useMessage, useDialog } from 'naive-ui'
|
||||
import { useLocalTokenStore } from '@/stores/localTokenManager'
|
||||
import { useGameRolesStore } from '@/stores/gameRoles'
|
||||
import {
|
||||
Refresh,
|
||||
Download,
|
||||
CloudUpload
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const localTokenStore = useLocalTokenStore()
|
||||
const gameRolesStore = useGameRolesStore()
|
||||
|
||||
// 方法
|
||||
const maskToken = (token) => {
|
||||
if (!token) return ''
|
||||
const len = token.length
|
||||
if (len <= 8) return token
|
||||
return token.substring(0, 4) + '***' + token.substring(len - 4)
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const getWSStatus = (roleId) => {
|
||||
return localTokenStore.getWebSocketStatus(roleId)
|
||||
}
|
||||
|
||||
const getWSStatusType = (status) => {
|
||||
switch (status) {
|
||||
case 'connected': return 'success'
|
||||
case 'error': return 'error'
|
||||
case 'connecting': return 'warning'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const getWSStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'connected': return '已连接'
|
||||
case 'error': return '连接错误'
|
||||
case 'connecting': return '连接中'
|
||||
default: return '未连接'
|
||||
}
|
||||
}
|
||||
|
||||
const refreshTokens = () => {
|
||||
localTokenStore.initTokenManager()
|
||||
message.success('Token数据已刷新')
|
||||
}
|
||||
|
||||
const clearUserToken = () => {
|
||||
dialog.warning({
|
||||
title: '清除用户Token',
|
||||
content: '确定要清除用户认证Token吗?这将会退出登录。',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
localTokenStore.clearUserToken()
|
||||
message.success('用户Token已清除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toggleWebSocket = (roleId, tokenData) => {
|
||||
const status = getWSStatus(roleId)
|
||||
|
||||
if (status === 'connected') {
|
||||
localTokenStore.closeWebSocketConnection(roleId)
|
||||
message.info('WebSocket连接已断开')
|
||||
} else {
|
||||
try {
|
||||
localTokenStore.createWebSocketConnection(roleId, tokenData.token, tokenData.wsUrl)
|
||||
message.success('正在建立WebSocket连接...')
|
||||
} catch (error) {
|
||||
message.error('建立WebSocket连接失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const regenerateToken = (roleId) => {
|
||||
dialog.info({
|
||||
title: '重新生成Token',
|
||||
content: '确定要为此角色重新生成游戏Token吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
const oldTokenData = localTokenStore.getGameToken(roleId)
|
||||
if (oldTokenData) {
|
||||
const newToken = 'game_token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 16)
|
||||
localTokenStore.updateGameToken(roleId, {
|
||||
token: newToken,
|
||||
regeneratedAt: new Date().toISOString()
|
||||
})
|
||||
message.success('Token已重新生成')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const removeToken = (roleId) => {
|
||||
dialog.warning({
|
||||
title: '删除Token',
|
||||
content: '确定要删除此角色的游戏Token吗?这将断开相关的WebSocket连接。',
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
localTokenStore.removeGameToken(roleId)
|
||||
message.success('Token已删除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const exportTokens = () => {
|
||||
try {
|
||||
const tokenData = localTokenStore.exportTokens()
|
||||
const dataStr = JSON.stringify(tokenData, null, 2)
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(dataBlob)
|
||||
link.download = `tokens_backup_${new Date().toISOString().split('T')[0]}.json`
|
||||
link.click()
|
||||
|
||||
message.success('Token数据已导出')
|
||||
} catch (error) {
|
||||
message.error('导出失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const importTokens = ({ file }) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const tokenData = JSON.parse(e.target.result)
|
||||
const result = localTokenStore.importTokens(tokenData)
|
||||
|
||||
if (result.success) {
|
||||
message.success(result.message)
|
||||
// 刷新游戏角色数据
|
||||
gameRolesStore.fetchGameRoles()
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('导入失败:文件格式错误')
|
||||
}
|
||||
}
|
||||
reader.readAsText(file.file)
|
||||
}
|
||||
|
||||
const cleanExpiredTokens = () => {
|
||||
dialog.info({
|
||||
title: '清理过期Token',
|
||||
content: '确定要清理超过24小时未使用的Token吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
const cleanedCount = localTokenStore.cleanExpiredTokens()
|
||||
message.success(`已清理 ${cleanedCount} 个过期Token`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clearAllTokens = () => {
|
||||
dialog.error({
|
||||
title: '清除所有Token',
|
||||
content: '确定要清除所有游戏Token吗?这将断开所有WebSocket连接。此操作不可恢复!',
|
||||
positiveText: '确定清除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
localTokenStore.clearAllGameTokens()
|
||||
message.success('所有游戏Token已清除')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.token-manager {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
margin: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.token-section {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
h4 {
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
}
|
||||
|
||||
.token-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.token-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.token-label {
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.token-value {
|
||||
font-family: monospace;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.empty-token {
|
||||
padding: var(--spacing-md);
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.game-tokens-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.game-token-item {
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.token-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.role-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.role-server {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.token-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.token-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-primary);
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.token-item {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.token-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.token-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
404
src/components/TowerStatus.vue
Normal file
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<div class="tower-status-card">
|
||||
<div class="card-header">
|
||||
<div class="header-info">
|
||||
<img src="/icons/1733492491706148.png" alt="爬塔图标" class="tower-icon">
|
||||
<div class="tower-info">
|
||||
<h3>咸将塔</h3>
|
||||
<p>一个不小心就过了</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="energy-display">
|
||||
<img src="/icons/xiaoyugan.png" alt="小鱼干" class="energy-icon">
|
||||
<span class="energy-count">{{ towerEnergy }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="tower-floor">
|
||||
<span class="label">当前层数</span>
|
||||
<span class="floor-number">{{ currentFloor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button
|
||||
:class="[
|
||||
'climb-button',
|
||||
{
|
||||
'active': canClimb,
|
||||
'disabled': !canClimb
|
||||
}
|
||||
]"
|
||||
:disabled="!canClimb"
|
||||
@click="startTowerClimb"
|
||||
>
|
||||
开始爬塔
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const isClimbing = ref(false)
|
||||
|
||||
// 计算属性 - 从gameData中获取塔相关信息
|
||||
const roleInfo = computed(() => {
|
||||
const data = tokenStore.gameData?.roleInfo || null
|
||||
console.log('🗼 TowerStatus roleInfo 计算属性更新:', data)
|
||||
if (data?.role?.tower) {
|
||||
console.log('🗼 TowerStatus 发现tower数据:', data.role.tower)
|
||||
} else {
|
||||
console.log('🗼 TowerStatus 没有找到tower数据, gameData:', tokenStore.gameData)
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const currentFloor = computed(() => {
|
||||
const tower = roleInfo.value?.role?.tower
|
||||
console.log('🗼 TowerStatus currentFloor 计算属性更新')
|
||||
console.log('🗼 TowerStatus 输入的tower数据:', tower)
|
||||
console.log('🗼 TowerStatus 完整的roleInfo:', roleInfo.value)
|
||||
|
||||
if (!tower) {
|
||||
console.log('🗼 没有tower对象,显示默认值')
|
||||
return "0 - 0"
|
||||
}
|
||||
|
||||
if (!tower.id && tower.id !== 0) {
|
||||
console.log('🗼 没有塔ID或ID无效,显示默认值, tower.id:', tower.id)
|
||||
return "0 - 0"
|
||||
}
|
||||
|
||||
const towerId = tower.id
|
||||
const floor = Math.floor(towerId / 10) + 1
|
||||
const layer = towerId % 10 + 1
|
||||
const result = `${floor} - ${layer}`
|
||||
console.log(`🗼 计算层数: towerId=${towerId} -> floor=${floor}, layer=${layer} -> ${result}`)
|
||||
return result
|
||||
})
|
||||
|
||||
const towerEnergy = computed(() => {
|
||||
const tower = roleInfo.value?.role?.tower
|
||||
console.log('🗼 TowerStatus towerEnergy 计算属性更新')
|
||||
console.log('🗼 TowerStatus tower对象:', tower)
|
||||
|
||||
const energy = tower?.energy || 0
|
||||
console.log('🗼 TowerStatus 计算出的energy:', energy)
|
||||
return energy
|
||||
})
|
||||
|
||||
const canClimb = computed(() => {
|
||||
return towerEnergy.value > 0 && !isClimbing.value
|
||||
})
|
||||
|
||||
// 方法
|
||||
const startTowerClimb = async () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
if (!canClimb.value) {
|
||||
message.warning('体力不足或正在爬塔中')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isClimbing.value = true
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
message.info('开始爬塔挑战...')
|
||||
|
||||
// 发送爬塔命令
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'fight_starttower', {}, 10000)
|
||||
|
||||
message.success('爬塔命令已发送')
|
||||
|
||||
// 立即查询塔信息以获取最新状态
|
||||
console.log('🗼 爬塔完成,立即查询塔信息')
|
||||
await getTowerInfo()
|
||||
|
||||
// 再延迟查询一次确保数据同步
|
||||
setTimeout(async () => {
|
||||
console.log('🗼 延迟查询塔信息')
|
||||
await getTowerInfo()
|
||||
}, 3000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('爬塔失败:', error)
|
||||
message.error('爬塔失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
isClimbing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getTowerInfo = async () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
console.warn('🗼 getTowerInfo: 没有选中的Token')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
console.log('🗼 getTowerInfo: 开始获取塔信息, tokenId:', tokenId)
|
||||
|
||||
// 检查WebSocket连接状态
|
||||
const wsStatus = tokenStore.getWebSocketStatus(tokenId)
|
||||
console.log('🗼 getTowerInfo: WebSocket状态:', wsStatus)
|
||||
|
||||
if (wsStatus !== 'connected') {
|
||||
console.warn('🗼 getTowerInfo: WebSocket未连接,无法获取数据')
|
||||
return
|
||||
}
|
||||
|
||||
// 首先获取角色信息,这包含了塔的数据
|
||||
console.log('🗼 getTowerInfo: 正在请求角色信息...')
|
||||
const roleResult = tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
console.log('🗼 getTowerInfo: 角色信息请求结果:', roleResult)
|
||||
|
||||
// 直接请求塔信息
|
||||
console.log('🗼 getTowerInfo: 正在请求塔信息...')
|
||||
const towerResult = tokenStore.sendMessage(tokenId, 'tower_getinfo')
|
||||
console.log('🗼 getTowerInfo: 塔信息请求结果:', towerResult)
|
||||
|
||||
// 检查当前gameData状态
|
||||
console.log('🗼 getTowerInfo: 当前gameData:', tokenStore.gameData)
|
||||
console.log('🗼 getTowerInfo: 当前roleInfo:', tokenStore.gameData?.roleInfo)
|
||||
console.log('🗼 getTowerInfo: 当前tower数据:', tokenStore.gameData?.roleInfo?.role?.tower)
|
||||
|
||||
if (!roleResult && !towerResult) {
|
||||
console.error('🗼 getTowerInfo: 所有请求都失败了')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('🗼 getTowerInfo: 获取塔信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 监听WebSocket连接状态变化
|
||||
const wsStatus = computed(() => {
|
||||
if (!tokenStore.selectedToken) return 'disconnected'
|
||||
return tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
})
|
||||
|
||||
// 监听WebSocket连接状态,连接成功后自动获取塔信息
|
||||
watch(wsStatus, (newStatus, oldStatus) => {
|
||||
console.log(`🗼 WebSocket状态变化: ${oldStatus} -> ${newStatus}`)
|
||||
|
||||
if (newStatus === 'connected' && oldStatus !== 'connected') {
|
||||
console.log('🗼 WebSocket已连接,自动获取塔信息')
|
||||
// 延迟一点时间让WebSocket完全就绪
|
||||
setTimeout(() => {
|
||||
getTowerInfo()
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听选中Token变化
|
||||
watch(() => tokenStore.selectedToken, (newToken, oldToken) => {
|
||||
if (newToken && newToken.id !== oldToken?.id) {
|
||||
console.log('🗼 Token已切换,获取新的塔信息')
|
||||
// 检查WebSocket是否已连接
|
||||
const status = tokenStore.getWebSocketStatus(newToken.id)
|
||||
if (status === 'connected') {
|
||||
getTowerInfo()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
console.log('🗼 TowerStatus 组件已挂载')
|
||||
console.log('🗼 当前选中Token:', tokenStore.selectedToken?.name)
|
||||
console.log('🗼 当前选中Token ID:', tokenStore.selectedToken?.id)
|
||||
console.log('🗼 当前WebSocket状态:', wsStatus.value)
|
||||
console.log('🗼 当前游戏数据:', tokenStore.gameData)
|
||||
console.log('🗼 当前roleInfo:', tokenStore.gameData?.roleInfo)
|
||||
console.log('🗼 当前tower数据:', tokenStore.gameData?.roleInfo?.role?.tower)
|
||||
|
||||
// 检查WebSocket客户端
|
||||
if (tokenStore.selectedToken) {
|
||||
const client = tokenStore.getWebSocketClient(tokenStore.selectedToken.id)
|
||||
console.log('🗼 WebSocket客户端:', client)
|
||||
console.log('🗼 WebSocket客户端状态:', client ? 'exists' : 'null')
|
||||
}
|
||||
|
||||
// 组件挂载时获取塔信息
|
||||
if (tokenStore.selectedToken && wsStatus.value === 'connected') {
|
||||
console.log('🗼 条件满足,开始获取塔信息')
|
||||
getTowerInfo()
|
||||
} else if (!tokenStore.selectedToken) {
|
||||
console.log('🗼 没有选中的Token,无法获取塔信息')
|
||||
} else {
|
||||
console.log('🗼 WebSocket未连接,等待连接后自动获取塔信息')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.tower-status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
border-left: 4px solid #6366f1;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.tower-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tower-info {
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.energy-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
background: var(--bg-tertiary);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.energy-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.energy-count {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.tower-floor {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.floor-number {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
|
||||
.climb-button {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&.active {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #5855eb;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
margin-top: var(--spacing-sm);
|
||||
padding: var(--spacing-xs);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-small);
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
|
||||
small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.energy-display {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
496
src/components/WebSocketTester.vue
Normal file
@@ -0,0 +1,496 @@
|
||||
<template>
|
||||
<div class="websocket-tester">
|
||||
<n-card
|
||||
title="WebSocket连接测试"
|
||||
class="mb-4"
|
||||
>
|
||||
<n-space
|
||||
direction="vertical"
|
||||
size="large"
|
||||
>
|
||||
<!-- 连接状态 -->
|
||||
<n-card
|
||||
title="连接状态"
|
||||
size="small"
|
||||
>
|
||||
<n-space align="center">
|
||||
<n-tag
|
||||
:type="getStatusType(status)"
|
||||
size="large"
|
||||
>
|
||||
{{ getStatusText(status) }}
|
||||
</n-tag>
|
||||
<n-button
|
||||
v-if="selectedRoleId && status !== 'connected'"
|
||||
type="primary"
|
||||
:loading="status === 'connecting'"
|
||||
@click="connectWebSocket"
|
||||
>
|
||||
连接WebSocket
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="status === 'connected'"
|
||||
type="error"
|
||||
@click="disconnectWebSocket"
|
||||
>
|
||||
断开连接
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<!-- 角色选择 -->
|
||||
<n-form-item label="选择角色">
|
||||
<n-select
|
||||
v-model:value="selectedRoleId"
|
||||
placeholder="请选择要测试的角色"
|
||||
:options="roleOptions"
|
||||
@update:value="onRoleChange"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<!-- 连接详情 -->
|
||||
<n-card
|
||||
v-if="connectionDetails"
|
||||
title="连接详情"
|
||||
size="small"
|
||||
>
|
||||
<n-descriptions
|
||||
:column="2"
|
||||
bordered
|
||||
size="small"
|
||||
>
|
||||
<n-descriptions-item label="角色ID">
|
||||
{{ connectionDetails.roleId }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="状态">
|
||||
<n-tag :type="getStatusType(connectionDetails.status)">
|
||||
{{ getStatusText(connectionDetails.status) }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="WebSocket URL">
|
||||
<n-text
|
||||
code
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
{{ connectionDetails.wsUrl }}
|
||||
</n-text>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="连接时间">
|
||||
{{ formatTime(connectionDetails.connectedAt) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item
|
||||
v-if="connectionDetails.lastError"
|
||||
label="最后错误"
|
||||
>
|
||||
<n-text type="error">
|
||||
{{ connectionDetails.lastError }}
|
||||
</n-text>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item
|
||||
v-if="connectionDetails.reconnectAttempt > 0"
|
||||
label="重连次数"
|
||||
>
|
||||
{{ connectionDetails.reconnectAttempt }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
|
||||
<!-- 游戏命令测试 -->
|
||||
<n-card
|
||||
v-if="status === 'connected'"
|
||||
title="游戏命令测试"
|
||||
size="small"
|
||||
>
|
||||
<n-space direction="vertical">
|
||||
<n-form-item label="选择命令">
|
||||
<n-select
|
||||
v-model:value="selectedCommand"
|
||||
placeholder="请选择要测试的命令"
|
||||
:options="commandOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item
|
||||
v-if="selectedCommand"
|
||||
label="命令参数 (JSON)"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="commandParams"
|
||||
type="textarea"
|
||||
placeholder="例如: {"roleId": 123456}"
|
||||
:rows="3"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-space>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!selectedCommand"
|
||||
:loading="sendingCommand"
|
||||
@click="sendCommand"
|
||||
>
|
||||
发送命令
|
||||
</n-button>
|
||||
<n-button
|
||||
type="success"
|
||||
:disabled="!selectedCommand"
|
||||
:loading="waitingResponse"
|
||||
@click="sendCommandWithPromise"
|
||||
>
|
||||
发送并等待响应
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<!-- 消息日志 -->
|
||||
<n-card
|
||||
title="消息日志"
|
||||
size="small"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="clearLog"
|
||||
>
|
||||
清空日志
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<div class="message-log">
|
||||
<div
|
||||
v-for="(msg, index) in messageLog"
|
||||
:key="index"
|
||||
class="message-item"
|
||||
:class="`message-${msg.type}`"
|
||||
>
|
||||
<div class="message-header">
|
||||
<n-tag
|
||||
:type="msg.type === 'sent' ? 'info' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
{{ msg.type === 'sent' ? '发送' : '接收' }}
|
||||
</n-tag>
|
||||
<n-text
|
||||
depth="3"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
{{ formatTime(msg.timestamp) }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<pre>{{ JSON.stringify(msg.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="messageLog.length === 0"
|
||||
class="no-messages"
|
||||
>
|
||||
暂无消息日志
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useTokenStore } from '../stores/tokenStore.js'
|
||||
|
||||
const message = useMessage()
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 响应式数据
|
||||
const selectedRoleId = ref(null)
|
||||
const status = ref('disconnected')
|
||||
const connectionDetails = ref(null)
|
||||
const selectedCommand = ref(null)
|
||||
const commandParams = ref('{}')
|
||||
const sendingCommand = ref(false)
|
||||
const waitingResponse = ref(false)
|
||||
const messageLog = ref([])
|
||||
|
||||
// Token选项
|
||||
const roleOptions = computed(() => {
|
||||
return tokenStore.gameTokens.map(token => ({
|
||||
label: `${token.name} (${token.server})`,
|
||||
value: token.id
|
||||
}))
|
||||
})
|
||||
|
||||
// 命令选项
|
||||
const commandOptions = [
|
||||
{ label: '获取角色信息', value: 'role_getroleinfo' },
|
||||
{ label: '获取数据包版本', value: 'system_getdatabundlever' },
|
||||
{ label: '签到奖励', value: 'system_signinreward' },
|
||||
{ label: '领取每日任务奖励', value: 'task_claimdailyreward' },
|
||||
{ label: '获取邮件列表', value: 'mail_getlist' },
|
||||
{ label: '领取所有邮件附件', value: 'mail_claimallattachment' },
|
||||
{ label: '获取军团信息', value: 'legion_getinfo' },
|
||||
{ label: '英雄招募', value: 'hero_recruit' },
|
||||
{ label: '领取挂机奖励', value: 'system_claimhangupreward' }
|
||||
]
|
||||
|
||||
// 方法
|
||||
const getStatusType = (statusValue) => {
|
||||
const typeMap = {
|
||||
connected: 'success',
|
||||
connecting: 'warning',
|
||||
disconnected: 'default',
|
||||
reconnecting: 'info',
|
||||
error: 'error'
|
||||
}
|
||||
return typeMap[statusValue] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (statusValue) => {
|
||||
const textMap = {
|
||||
connected: '已连接',
|
||||
connecting: '连接中',
|
||||
disconnected: '已断开',
|
||||
reconnecting: '重连中',
|
||||
error: '连接错误'
|
||||
}
|
||||
return textMap[statusValue] || '未知状态'
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return '-'
|
||||
return new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const onRoleChange = () => {
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
const updateStatus = () => {
|
||||
if (!selectedRoleId.value) {
|
||||
status.value = 'disconnected'
|
||||
connectionDetails.value = null
|
||||
return
|
||||
}
|
||||
|
||||
status.value = tokenStore.getWebSocketStatus(selectedRoleId.value)
|
||||
const connection = tokenStore.wsConnections[selectedRoleId.value]
|
||||
if (connection) {
|
||||
connectionDetails.value = {
|
||||
roleId: selectedRoleId.value,
|
||||
status: connection.status,
|
||||
connectedAt: connection.connectedAt,
|
||||
wsUrl: connection.wsUrl
|
||||
}
|
||||
} else {
|
||||
connectionDetails.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const connectWebSocket = async () => {
|
||||
if (!selectedRoleId.value) {
|
||||
message.error('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
status.value = 'connecting'
|
||||
const token = tokenStore.gameTokens.find(t => t.id === selectedRoleId.value)
|
||||
|
||||
if (!token) {
|
||||
message.error('未找到Token数据')
|
||||
return
|
||||
}
|
||||
|
||||
tokenStore.createWebSocketConnection(
|
||||
selectedRoleId.value,
|
||||
token.token,
|
||||
token.wsUrl
|
||||
)
|
||||
|
||||
// 监听消息
|
||||
startMessageListener()
|
||||
|
||||
message.success('WebSocket连接已启动')
|
||||
} catch (error) {
|
||||
console.error('WebSocket连接失败:', error)
|
||||
message.error('WebSocket连接失败: ' + error.message)
|
||||
} finally {
|
||||
setTimeout(updateStatus, 1000) // 延迟更新状态以等待连接建立
|
||||
}
|
||||
}
|
||||
|
||||
const disconnectWebSocket = () => {
|
||||
if (!selectedRoleId.value) return
|
||||
|
||||
tokenStore.closeWebSocketConnection(selectedRoleId.value)
|
||||
status.value = 'disconnected'
|
||||
connectionDetails.value = null
|
||||
message.info('WebSocket连接已断开')
|
||||
}
|
||||
|
||||
const sendCommand = async () => {
|
||||
if (!selectedCommand.value) {
|
||||
message.error('请选择要发送的命令')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
sendingCommand.value = true
|
||||
|
||||
let params = {}
|
||||
if (commandParams.value.trim()) {
|
||||
params = JSON.parse(commandParams.value)
|
||||
}
|
||||
|
||||
const success = tokenStore.sendMessage(
|
||||
selectedRoleId.value,
|
||||
selectedCommand.value,
|
||||
params
|
||||
)
|
||||
|
||||
if (success) {
|
||||
addToLog('sent', {
|
||||
command: selectedCommand.value,
|
||||
params
|
||||
})
|
||||
message.success('命令发送成功')
|
||||
} else {
|
||||
message.error('命令发送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送命令失败:', error)
|
||||
message.error('发送命令失败: ' + error.message)
|
||||
} finally {
|
||||
sendingCommand.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const sendCommandWithPromise = async () => {
|
||||
if (!selectedCommand.value) {
|
||||
message.error('请选择要发送的命令')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
waitingResponse.value = true
|
||||
|
||||
let params = {}
|
||||
if (commandParams.value.trim()) {
|
||||
params = JSON.parse(commandParams.value)
|
||||
}
|
||||
|
||||
const response = await tokenStore.sendMessageWithPromise(
|
||||
selectedRoleId.value,
|
||||
selectedCommand.value,
|
||||
params
|
||||
)
|
||||
|
||||
addToLog('sent', {
|
||||
command: selectedCommand.value,
|
||||
params
|
||||
})
|
||||
|
||||
addToLog('received', response)
|
||||
|
||||
message.success('命令执行成功,已收到响应')
|
||||
} catch (error) {
|
||||
console.error('发送命令失败:', error)
|
||||
message.error('发送命令失败: ' + error.message)
|
||||
} finally {
|
||||
waitingResponse.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startMessageListener = () => {
|
||||
// 这里简化处理,实际应该通过WebSocket客户端的onMessage事件来接收消息
|
||||
// 由于消息处理在store中,这里只是演示
|
||||
}
|
||||
|
||||
const addToLog = (type, data) => {
|
||||
messageLog.value.unshift({
|
||||
type,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
// 限制日志条数
|
||||
if (messageLog.value.length > 100) {
|
||||
messageLog.value = messageLog.value.slice(0, 100)
|
||||
}
|
||||
}
|
||||
|
||||
const clearLog = () => {
|
||||
messageLog.value = []
|
||||
}
|
||||
|
||||
// 定时更新状态
|
||||
let statusTimer = null
|
||||
|
||||
onMounted(() => {
|
||||
// 定时更新连接状态
|
||||
statusTimer = setInterval(() => {
|
||||
if (selectedRoleId.value) {
|
||||
updateStatus()
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (statusTimer) {
|
||||
clearInterval(statusTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.websocket-tester {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.message-log {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e6;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.message-sent {
|
||||
border-left: 3px solid #2080f0;
|
||||
}
|
||||
|
||||
.message-received {
|
||||
border-left: 3px solid #18a058;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.message-content pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.no-messages {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
17
src/main.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import naive from 'naive-ui'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import './assets/styles/global.scss'
|
||||
|
||||
// 创建应用实例
|
||||
const app = createApp(App)
|
||||
|
||||
// 使用插件
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(naive)
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
137
src/router/index.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/Home.vue'),
|
||||
meta: {
|
||||
title: '首页',
|
||||
requiresToken: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tokens',
|
||||
name: 'TokenImport',
|
||||
component: () => import('@/views/TokenImport.vue'),
|
||||
meta: {
|
||||
title: 'Token管理',
|
||||
requiresToken: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
meta: {
|
||||
title: '控制台',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'Profile',
|
||||
component: () => import('@/views/Profile.vue'),
|
||||
meta: {
|
||||
title: '个人设置',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/daily-tasks',
|
||||
name: 'DailyTasks',
|
||||
component: () => import('@/views/DailyTasks.vue'),
|
||||
meta: {
|
||||
title: '日常任务',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/game-features',
|
||||
name: 'GameFeatures',
|
||||
component: () => import('@/views/GameFeatures.vue'),
|
||||
meta: {
|
||||
title: '游戏功能',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/message-test',
|
||||
name: 'MessageTest',
|
||||
component: () => import('@/components/MessageTester.vue'),
|
||||
meta: {
|
||||
title: '消息测试',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/websocket-test',
|
||||
name: 'WebSocketTest',
|
||||
component: () => import('@/components/WebSocketTester.vue'),
|
||||
meta: {
|
||||
title: 'WebSocket测试',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
// 兼容旧路由,重定向到新的token管理页面
|
||||
{
|
||||
path: '/login',
|
||||
redirect: '/tokens'
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
redirect: '/tokens'
|
||||
},
|
||||
{
|
||||
path: '/game-roles',
|
||||
redirect: '/tokens'
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/NotFound.vue'),
|
||||
meta: {
|
||||
title: '页面不存在'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else {
|
||||
return { top: 0 }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 导航守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 设置页面标题
|
||||
document.title = to.meta.title ? `${to.meta.title} - XYZW 游戏管理系统` : 'XYZW 游戏管理系统'
|
||||
|
||||
// 检查是否需要Token
|
||||
if (to.meta.requiresToken && !tokenStore.hasTokens) {
|
||||
next('/tokens')
|
||||
} else if (to.name === 'TokenImport' && tokenStore.hasTokens && tokenStore.selectedToken) {
|
||||
// 如果已有token且已选择,重定向到控制台
|
||||
next('/dashboard')
|
||||
} else if (to.path === '/' && tokenStore.hasTokens) {
|
||||
// 首页重定向逻辑
|
||||
if (tokenStore.selectedToken) {
|
||||
next('/dashboard')
|
||||
} else {
|
||||
next('/tokens')
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
158
src/stores/auth.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useLocalTokenStore } from './localTokenManager'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 状态
|
||||
const user = ref(null)
|
||||
const token = ref(localStorage.getItem('token') || null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const localTokenStore = useLocalTokenStore()
|
||||
|
||||
// 计算属性
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
const userInfo = computed(() => user.value)
|
||||
|
||||
// 登录 - 移除API调用,使用本地认证
|
||||
const login = async (credentials) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
// 模拟本地认证逻辑
|
||||
const mockUser = {
|
||||
id: 'local_user_' + Date.now(),
|
||||
username: credentials.username,
|
||||
email: credentials.email || `${credentials.username}@local.game`,
|
||||
avatar: '/icons/xiaoyugan.png',
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
const mockToken = 'local_token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
||||
|
||||
token.value = mockToken
|
||||
user.value = mockUser
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('token', token.value)
|
||||
localStorage.setItem('user', JSON.stringify(user.value))
|
||||
|
||||
// 同时保存到token管理器
|
||||
localTokenStore.setUserToken(mockToken)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
return { success: false, message: '本地认证失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 - 移除API调用,使用本地注册
|
||||
const register = async (userInfo) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
// 检查用户名是否已存在(简单的本地检查)
|
||||
const existingUsers = JSON.parse(localStorage.getItem('registeredUsers') || '[]')
|
||||
const userExists = existingUsers.some(u => u.username === userInfo.username)
|
||||
|
||||
if (userExists) {
|
||||
return { success: false, message: '用户名已存在' }
|
||||
}
|
||||
|
||||
// 保存新用户信息到本地
|
||||
const newUser = {
|
||||
...userInfo,
|
||||
id: 'user_' + Date.now(),
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
existingUsers.push(newUser)
|
||||
localStorage.setItem('registeredUsers', JSON.stringify(existingUsers))
|
||||
|
||||
return { success: true, message: '注册成功,请登录' }
|
||||
} catch (error) {
|
||||
console.error('注册错误:', error)
|
||||
return { success: false, message: '本地注册失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 登出
|
||||
const logout = () => {
|
||||
user.value = null
|
||||
token.value = null
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('gameRoles')
|
||||
|
||||
// 清除token管理器中的数据
|
||||
localTokenStore.clearUserToken()
|
||||
localTokenStore.clearAllGameTokens()
|
||||
}
|
||||
|
||||
// 获取用户信息 - 移除API调用,使用本地数据
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
if (!token.value) return false
|
||||
|
||||
// 从本地存储获取用户信息
|
||||
const savedUser = localStorage.getItem('user')
|
||||
if (savedUser) {
|
||||
try {
|
||||
user.value = JSON.parse(savedUser)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化认证状态 - 移除API验证,使用本地验证
|
||||
const initAuth = async () => {
|
||||
const savedUser = localStorage.getItem('user')
|
||||
if (token.value && savedUser) {
|
||||
try {
|
||||
user.value = JSON.parse(savedUser)
|
||||
// 初始化token管理器
|
||||
localTokenStore.initTokenManager()
|
||||
} catch (error) {
|
||||
console.error('初始化认证失败:', error)
|
||||
logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
user,
|
||||
token,
|
||||
isLoading,
|
||||
|
||||
// 计算属性
|
||||
isAuthenticated,
|
||||
userInfo,
|
||||
|
||||
// 方法
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
fetchUserInfo,
|
||||
initAuth
|
||||
}
|
||||
})
|
||||
204
src/stores/gameRoles.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useLocalTokenStore } from './localTokenManager'
|
||||
|
||||
export const useGameRolesStore = defineStore('gameRoles', () => {
|
||||
// 状态
|
||||
const gameRoles = ref([])
|
||||
const isLoading = ref(false)
|
||||
const selectedRole = ref(null)
|
||||
|
||||
const localTokenStore = useLocalTokenStore()
|
||||
|
||||
// 获取游戏角色列表 - 移除API调用,使用本地数据
|
||||
const fetchGameRoles = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
// 从本地存储获取角色数据
|
||||
const savedRoles = localStorage.getItem('gameRoles')
|
||||
if (savedRoles) {
|
||||
try {
|
||||
gameRoles.value = JSON.parse(savedRoles)
|
||||
} catch (error) {
|
||||
console.error('解析游戏角色数据失败:', error)
|
||||
gameRoles.value = []
|
||||
}
|
||||
} else {
|
||||
gameRoles.value = []
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('获取游戏角色失败:', error)
|
||||
return { success: false, message: '本地数据读取失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加游戏角色 - 移除API调用,本地生成角色和token
|
||||
const addGameRole = async (roleData) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
// 生成角色ID和游戏token
|
||||
const roleId = 'role_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
||||
const gameToken = 'game_token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 16)
|
||||
|
||||
const newRole = {
|
||||
...roleData,
|
||||
id: roleId,
|
||||
createdAt: new Date().toISOString(),
|
||||
isActive: false,
|
||||
exp: 0,
|
||||
gold: 1000, // 默认金币
|
||||
vip: false,
|
||||
avatar: roleData.avatar || '/icons/xiaoyugan.png'
|
||||
}
|
||||
|
||||
// 添加到角色列表
|
||||
gameRoles.value.push(newRole)
|
||||
localStorage.setItem('gameRoles', JSON.stringify(gameRoles.value))
|
||||
|
||||
// 生成并保存游戏token
|
||||
const tokenData = {
|
||||
token: gameToken,
|
||||
roleId: roleId,
|
||||
roleName: newRole.name,
|
||||
server: newRole.server,
|
||||
wsUrl: null, // 使用默认的游戏WebSocket地址
|
||||
createdAt: new Date().toISOString(),
|
||||
isActive: true
|
||||
}
|
||||
|
||||
localTokenStore.addGameToken(roleId, tokenData)
|
||||
|
||||
return { success: true, message: '添加角色成功,已生成游戏token' }
|
||||
} catch (error) {
|
||||
console.error('添加游戏角色失败:', error)
|
||||
return { success: false, message: '添加角色失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新游戏角色 - 移除API调用,使用本地更新
|
||||
const updateGameRole = async (roleId, roleData) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const index = gameRoles.value.findIndex(role => role.id === roleId)
|
||||
if (index !== -1) {
|
||||
gameRoles.value[index] = {
|
||||
...gameRoles.value[index],
|
||||
...roleData,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
localStorage.setItem('gameRoles', JSON.stringify(gameRoles.value))
|
||||
|
||||
// 更新对应的token信息
|
||||
const existingToken = localTokenStore.getGameToken(roleId)
|
||||
if (existingToken) {
|
||||
localTokenStore.updateGameToken(roleId, {
|
||||
roleName: roleData.name || existingToken.roleName,
|
||||
server: roleData.server || existingToken.server
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true, message: '更新角色成功' }
|
||||
} else {
|
||||
return { success: false, message: '角色不存在' }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新游戏角色失败:', error)
|
||||
return { success: false, message: '更新角色失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除游戏角色 - 移除API调用,同时删除对应token
|
||||
const deleteGameRole = async (roleId) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
gameRoles.value = gameRoles.value.filter(role => role.id !== roleId)
|
||||
localStorage.setItem('gameRoles', JSON.stringify(gameRoles.value))
|
||||
|
||||
// 删除对应的token和WebSocket连接
|
||||
localTokenStore.removeGameToken(roleId)
|
||||
|
||||
// 如果删除的是当前选中角色,清除选中状态
|
||||
if (selectedRole.value && selectedRole.value.id === roleId) {
|
||||
selectedRole.value = null
|
||||
localStorage.removeItem('selectedRole')
|
||||
}
|
||||
|
||||
return { success: true, message: '删除角色成功,已清理相关token' }
|
||||
} catch (error) {
|
||||
console.error('删除游戏角色失败:', error)
|
||||
return { success: false, message: '删除角色失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择角色 - 添加WebSocket连接功能
|
||||
const selectRole = (role) => {
|
||||
selectedRole.value = role
|
||||
localStorage.setItem('selectedRole', JSON.stringify(role))
|
||||
|
||||
// 自动建立WebSocket连接
|
||||
const tokenData = localTokenStore.getGameToken(role.id)
|
||||
if (tokenData && tokenData.token) {
|
||||
try {
|
||||
localTokenStore.createWebSocketConnection(
|
||||
role.id,
|
||||
tokenData.token,
|
||||
tokenData.wsUrl
|
||||
)
|
||||
console.log(`已为角色 ${role.name} 建立WebSocket连接`)
|
||||
} catch (error) {
|
||||
console.error(`建立WebSocket连接失败 [${role.name}]:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
const initGameRoles = () => {
|
||||
const cachedRoles = localStorage.getItem('gameRoles')
|
||||
const cachedSelectedRole = localStorage.getItem('selectedRole')
|
||||
|
||||
if (cachedRoles) {
|
||||
try {
|
||||
gameRoles.value = JSON.parse(cachedRoles)
|
||||
} catch (error) {
|
||||
console.error('解析缓存的游戏角色数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (cachedSelectedRole) {
|
||||
try {
|
||||
selectedRole.value = JSON.parse(cachedSelectedRole)
|
||||
} catch (error) {
|
||||
console.error('解析缓存的选中角色数据失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
gameRoles,
|
||||
isLoading,
|
||||
selectedRole,
|
||||
|
||||
// 方法
|
||||
fetchGameRoles,
|
||||
addGameRole,
|
||||
updateGameRole,
|
||||
deleteGameRole,
|
||||
selectRole,
|
||||
initGameRoles
|
||||
}
|
||||
})
|
||||
454
src/stores/localTokenManager.js
Normal file
@@ -0,0 +1,454 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
/**
|
||||
* 本地Token管理器
|
||||
* 用于管理用户认证token和游戏角色token的本地存储
|
||||
*/
|
||||
export const useLocalTokenStore = defineStore('localToken', () => {
|
||||
// 状态
|
||||
const userToken = ref(localStorage.getItem('userToken') || null)
|
||||
const gameTokens = ref(JSON.parse(localStorage.getItem('gameTokens') || '{}'))
|
||||
const wsConnections = ref({}) // WebSocket连接状态
|
||||
|
||||
// 计算属性
|
||||
const isUserAuthenticated = computed(() => !!userToken.value)
|
||||
const hasGameTokens = computed(() => Object.keys(gameTokens.value).length > 0)
|
||||
|
||||
// 用户认证token管理
|
||||
const setUserToken = (token) => {
|
||||
userToken.value = token
|
||||
localStorage.setItem('userToken', token)
|
||||
}
|
||||
|
||||
const clearUserToken = () => {
|
||||
userToken.value = null
|
||||
localStorage.removeItem('userToken')
|
||||
}
|
||||
|
||||
// 游戏token管理
|
||||
const addGameToken = (roleId, tokenData) => {
|
||||
const newTokenData = {
|
||||
...tokenData,
|
||||
roleId,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsed: new Date().toISOString()
|
||||
}
|
||||
|
||||
gameTokens.value[roleId] = newTokenData
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
|
||||
return newTokenData
|
||||
}
|
||||
|
||||
const getGameToken = (roleId) => {
|
||||
const token = gameTokens.value[roleId]
|
||||
if (token) {
|
||||
// 更新最后使用时间
|
||||
token.lastUsed = new Date().toISOString()
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
const updateGameToken = (roleId, updates) => {
|
||||
if (gameTokens.value[roleId]) {
|
||||
gameTokens.value[roleId] = {
|
||||
...gameTokens.value[roleId],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
}
|
||||
}
|
||||
|
||||
const removeGameToken = (roleId) => {
|
||||
delete gameTokens.value[roleId]
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
|
||||
// 同时断开对应的WebSocket连接
|
||||
if (wsConnections.value[roleId]) {
|
||||
closeWebSocketConnection(roleId)
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllGameTokens = () => {
|
||||
// 关闭所有WebSocket连接
|
||||
Object.keys(wsConnections.value).forEach(roleId => {
|
||||
closeWebSocketConnection(roleId)
|
||||
})
|
||||
|
||||
gameTokens.value = {}
|
||||
localStorage.removeItem('gameTokens')
|
||||
}
|
||||
|
||||
// WebSocket连接管理 - 使用新的WsAgent
|
||||
const createWebSocketConnection = async (roleId, base64Token, customWsUrl = null) => {
|
||||
if (wsConnections.value[roleId]) {
|
||||
closeWebSocketConnection(roleId)
|
||||
}
|
||||
|
||||
try {
|
||||
// 动态导入WebSocket客户端
|
||||
const { WsAgent } = await import('../utils/wsAgent.js')
|
||||
const { gameCommands } = await import('../utils/gameCommands.js')
|
||||
|
||||
// 解析Base64获取实际Token
|
||||
let actualToken = base64Token
|
||||
|
||||
// 尝试解析Base64获取实际token
|
||||
try {
|
||||
const cleanBase64 = base64Token.replace(/^data:.*base64,/, '').trim()
|
||||
const decoded = atob(cleanBase64)
|
||||
|
||||
// 尝试解析为JSON获取token字段
|
||||
try {
|
||||
const tokenData = JSON.parse(decoded)
|
||||
actualToken = tokenData.token || tokenData.gameToken || decoded
|
||||
} catch {
|
||||
// 如果不是JSON,直接使用解码后的字符串
|
||||
actualToken = decoded
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Base64解析失败,使用原始token:', error.message)
|
||||
actualToken = base64Token
|
||||
}
|
||||
|
||||
// 创建WebSocket客户端实例
|
||||
const wsAgent = new WsAgent({
|
||||
heartbeatInterval: 2000,
|
||||
queueInterval: 50,
|
||||
channel: 'x', // 使用x通道
|
||||
autoReconnect: true,
|
||||
maxReconnectAttempts: 5
|
||||
})
|
||||
|
||||
// 设置事件监听器
|
||||
wsAgent.onOpen = () => {
|
||||
console.log(`✅ WebSocket连接已建立: ${roleId}`)
|
||||
|
||||
// 更新连接状态
|
||||
wsConnections.value[roleId].status = 'connected'
|
||||
wsConnections.value[roleId].connectedAt = new Date().toISOString()
|
||||
|
||||
// 发送初始化命令
|
||||
setTimeout(() => {
|
||||
// 获取角色信息
|
||||
wsAgent.send(gameCommands.role_getroleinfo(0, 0, { roleId }))
|
||||
|
||||
// 获取数据包版本
|
||||
wsAgent.send(gameCommands.system_getdatabundlever())
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
wsAgent.onMessage = (message) => {
|
||||
console.log(`📨 收到消息 [${roleId}]:`, message)
|
||||
|
||||
// 处理不同类型的消息
|
||||
if (message.cmd) {
|
||||
handleGameMessage(roleId, message)
|
||||
}
|
||||
}
|
||||
|
||||
wsAgent.onError = (error) => {
|
||||
console.error(`❌ WebSocket错误 [${roleId}]:`, error)
|
||||
if (wsConnections.value[roleId]) {
|
||||
wsConnections.value[roleId].status = 'error'
|
||||
wsConnections.value[roleId].lastError = error.message
|
||||
}
|
||||
}
|
||||
|
||||
wsAgent.onClose = (event) => {
|
||||
console.log(`🔌 WebSocket连接已关闭 [${roleId}]:`, event.code, event.reason)
|
||||
if (wsConnections.value[roleId]) {
|
||||
wsConnections.value[roleId].status = 'disconnected'
|
||||
}
|
||||
}
|
||||
|
||||
wsAgent.onReconnect = (attempt) => {
|
||||
console.log(`🔄 WebSocket重连中 [${roleId}] 第${attempt}次`)
|
||||
if (wsConnections.value[roleId]) {
|
||||
wsConnections.value[roleId].status = 'reconnecting'
|
||||
wsConnections.value[roleId].reconnectAttempt = attempt
|
||||
}
|
||||
}
|
||||
|
||||
// 构建WebSocket URL
|
||||
const baseWsUrl = 'wss://xxz-xyzw.hortorgames.com/agent'
|
||||
const wsUrl = customWsUrl || WsAgent.buildUrl(baseWsUrl, {
|
||||
p: actualToken,
|
||||
e: 'x',
|
||||
lang: 'chinese'
|
||||
})
|
||||
|
||||
// 保存连接信息
|
||||
wsConnections.value[roleId] = {
|
||||
agent: wsAgent,
|
||||
gameCommands,
|
||||
status: 'connecting',
|
||||
roleId,
|
||||
wsUrl,
|
||||
actualToken,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastError: null,
|
||||
reconnectAttempt: 0
|
||||
}
|
||||
|
||||
// 建立连接
|
||||
await wsAgent.connect(wsUrl)
|
||||
|
||||
return wsAgent
|
||||
} catch (error) {
|
||||
console.error(`创建WebSocket连接失败 [${roleId}]:`, error)
|
||||
if (wsConnections.value[roleId]) {
|
||||
wsConnections.value[roleId].status = 'error'
|
||||
wsConnections.value[roleId].lastError = error.message
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理游戏消息
|
||||
const handleGameMessage = (roleId, message) => {
|
||||
const { cmd, body } = message
|
||||
|
||||
switch (cmd) {
|
||||
case 'role_getroleinfo':
|
||||
console.log(`角色信息 [${roleId}]:`, body)
|
||||
break
|
||||
|
||||
case 'system_getdatabundlever':
|
||||
console.log(`数据包版本 [${roleId}]:`, body)
|
||||
break
|
||||
|
||||
case 'task_claimdailyreward':
|
||||
console.log(`每日任务奖励 [${roleId}]:`, body)
|
||||
break
|
||||
|
||||
case 'system_signinreward':
|
||||
console.log(`签到奖励 [${roleId}]:`, body)
|
||||
break
|
||||
|
||||
default:
|
||||
console.log(`未处理的消息 [${roleId}] ${cmd}:`, body)
|
||||
}
|
||||
}
|
||||
|
||||
const closeWebSocketConnection = (roleId) => {
|
||||
const connection = wsConnections.value[roleId]
|
||||
if (connection) {
|
||||
// 如果是新的WsAgent实例
|
||||
if (connection.agent && typeof connection.agent.close === 'function') {
|
||||
connection.agent.close()
|
||||
}
|
||||
// 如果是旧的WebSocket实例
|
||||
else if (connection.connection && typeof connection.connection.close === 'function') {
|
||||
connection.connection.close()
|
||||
}
|
||||
|
||||
delete wsConnections.value[roleId]
|
||||
}
|
||||
}
|
||||
|
||||
const getWebSocketStatus = (roleId) => {
|
||||
return wsConnections.value[roleId]?.status || 'disconnected'
|
||||
}
|
||||
|
||||
// 发送游戏命令
|
||||
const sendGameCommand = (roleId, commandName, params = {}) => {
|
||||
const connection = wsConnections.value[roleId]
|
||||
if (!connection || !connection.agent) {
|
||||
console.warn(`角色 ${roleId} 的WebSocket连接不存在`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (connection.status !== 'connected') {
|
||||
console.warn(`角色 ${roleId} 的WebSocket未连接`)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const { gameCommands } = connection
|
||||
|
||||
if (typeof gameCommands[commandName] === 'function') {
|
||||
const command = gameCommands[commandName](0, 0, params)
|
||||
connection.agent.send(command)
|
||||
console.log(`发送游戏命令 [${roleId}] ${commandName}:`, params)
|
||||
return true
|
||||
} else {
|
||||
console.error(`未知的游戏命令: ${commandName}`)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`发送游戏命令失败 [${roleId}] ${commandName}:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 发送游戏命令并等待响应
|
||||
const sendGameCommandWithPromise = async (roleId, commandName, params = {}, timeout = 8000) => {
|
||||
const connection = wsConnections.value[roleId]
|
||||
if (!connection || !connection.agent) {
|
||||
throw new Error(`角色 ${roleId} 的WebSocket连接不存在`)
|
||||
}
|
||||
|
||||
if (connection.status !== 'connected') {
|
||||
throw new Error(`角色 ${roleId} 的WebSocket未连接`)
|
||||
}
|
||||
|
||||
try {
|
||||
const { gameCommands } = connection
|
||||
|
||||
if (typeof gameCommands[commandName] === 'function') {
|
||||
const response = await connection.agent.sendWithPromise({
|
||||
cmd: commandName,
|
||||
body: params,
|
||||
timeout
|
||||
})
|
||||
console.log(`游戏命令响应 [${roleId}] ${commandName}:`, response)
|
||||
return response
|
||||
} else {
|
||||
throw new Error(`未知的游戏命令: ${commandName}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`发送游戏命令失败 [${roleId}] ${commandName}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取连接详细状态
|
||||
const getWebSocketDetails = (roleId) => {
|
||||
const connection = wsConnections.value[roleId]
|
||||
if (!connection) {
|
||||
return {
|
||||
status: 'disconnected',
|
||||
roleId,
|
||||
error: '连接不存在'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: connection.status,
|
||||
roleId: connection.roleId,
|
||||
wsUrl: connection.wsUrl,
|
||||
connectedAt: connection.connectedAt,
|
||||
createdAt: connection.createdAt,
|
||||
lastError: connection.lastError,
|
||||
reconnectAttempt: connection.reconnectAttempt,
|
||||
agentStatus: connection.agent ? connection.agent.getStatus() : null
|
||||
}
|
||||
}
|
||||
|
||||
// 批量导入/导出功能
|
||||
const exportTokens = () => {
|
||||
return {
|
||||
userToken: userToken.value,
|
||||
gameTokens: gameTokens.value,
|
||||
exportedAt: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const importTokens = (tokenData) => {
|
||||
try {
|
||||
if (tokenData.userToken) {
|
||||
setUserToken(tokenData.userToken)
|
||||
}
|
||||
|
||||
if (tokenData.gameTokens) {
|
||||
gameTokens.value = tokenData.gameTokens
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
}
|
||||
|
||||
return { success: true, message: 'Token导入成功' }
|
||||
} catch (error) {
|
||||
console.error('Token导入失败:', error)
|
||||
return { success: false, message: '导入失败:数据格式错误' }
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期token
|
||||
const cleanExpiredTokens = () => {
|
||||
const now = new Date()
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
const cleanedTokens = {}
|
||||
let cleanedCount = 0
|
||||
|
||||
Object.entries(gameTokens.value).forEach(([roleId, tokenData]) => {
|
||||
const lastUsed = new Date(tokenData.lastUsed || tokenData.createdAt)
|
||||
if (lastUsed > oneDayAgo) {
|
||||
cleanedTokens[roleId] = tokenData
|
||||
} else {
|
||||
cleanedCount++
|
||||
// 关闭对应的WebSocket连接
|
||||
if (wsConnections.value[roleId]) {
|
||||
closeWebSocketConnection(roleId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
gameTokens.value = cleanedTokens
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
|
||||
return cleanedCount
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const initTokenManager = () => {
|
||||
// 从localStorage恢复数据
|
||||
const savedUserToken = localStorage.getItem('userToken')
|
||||
const savedGameTokens = localStorage.getItem('gameTokens')
|
||||
|
||||
if (savedUserToken) {
|
||||
userToken.value = savedUserToken
|
||||
}
|
||||
|
||||
if (savedGameTokens) {
|
||||
try {
|
||||
gameTokens.value = JSON.parse(savedGameTokens)
|
||||
} catch (error) {
|
||||
console.error('解析游戏token数据失败:', error)
|
||||
gameTokens.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期token
|
||||
cleanExpiredTokens()
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
userToken,
|
||||
gameTokens,
|
||||
wsConnections,
|
||||
|
||||
// 计算属性
|
||||
isUserAuthenticated,
|
||||
hasGameTokens,
|
||||
|
||||
// 用户token方法
|
||||
setUserToken,
|
||||
clearUserToken,
|
||||
|
||||
// 游戏token方法
|
||||
addGameToken,
|
||||
getGameToken,
|
||||
updateGameToken,
|
||||
removeGameToken,
|
||||
clearAllGameTokens,
|
||||
|
||||
// WebSocket方法
|
||||
createWebSocketConnection,
|
||||
closeWebSocketConnection,
|
||||
getWebSocketStatus,
|
||||
getWebSocketDetails,
|
||||
sendGameCommand,
|
||||
sendGameCommandWithPromise,
|
||||
|
||||
// 工具方法
|
||||
exportTokens,
|
||||
importTokens,
|
||||
cleanExpiredTokens,
|
||||
initTokenManager
|
||||
}
|
||||
})
|
||||
792
src/stores/tokenStore.js
Normal file
@@ -0,0 +1,792 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { bonProtocol, GameMessages, g_utils } from '../utils/bonProtocol.js'
|
||||
import { XyzwWebSocketClient } from '../utils/xyzwWebSocket.js'
|
||||
|
||||
/**
|
||||
* 重构后的Token管理存储
|
||||
* 以名称-token列表形式管理多个游戏角色
|
||||
*/
|
||||
export const useTokenStore = defineStore('tokens', () => {
|
||||
// 状态
|
||||
const gameTokens = ref(JSON.parse(localStorage.getItem('gameTokens') || '[]'))
|
||||
const selectedTokenId = ref(localStorage.getItem('selectedTokenId') || null)
|
||||
const wsConnections = ref({}) // WebSocket连接状态
|
||||
|
||||
// 游戏数据存储
|
||||
const gameData = ref({
|
||||
roleInfo: null,
|
||||
legionInfo: null,
|
||||
presetTeam: null,
|
||||
lastUpdated: null
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const hasTokens = computed(() => gameTokens.value.length > 0)
|
||||
const selectedToken = computed(() =>
|
||||
gameTokens.value.find(token => token.id === selectedTokenId.value)
|
||||
)
|
||||
|
||||
// Token管理
|
||||
const addToken = (tokenData) => {
|
||||
const newToken = {
|
||||
id: 'token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
||||
name: tokenData.name,
|
||||
token: tokenData.token, // 保存原始Base64 token
|
||||
wsUrl: tokenData.wsUrl || null, // 可选的自定义WebSocket URL
|
||||
server: tokenData.server || '',
|
||||
level: tokenData.level || 1,
|
||||
profession: tokenData.profession || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsed: new Date().toISOString(),
|
||||
isActive: true
|
||||
}
|
||||
|
||||
gameTokens.value.push(newToken)
|
||||
saveTokensToStorage()
|
||||
|
||||
return newToken
|
||||
}
|
||||
|
||||
const updateToken = (tokenId, updates) => {
|
||||
const index = gameTokens.value.findIndex(token => token.id === tokenId)
|
||||
if (index !== -1) {
|
||||
gameTokens.value[index] = {
|
||||
...gameTokens.value[index],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
saveTokensToStorage()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const removeToken = (tokenId) => {
|
||||
gameTokens.value = gameTokens.value.filter(token => token.id !== tokenId)
|
||||
saveTokensToStorage()
|
||||
|
||||
// 关闭对应的WebSocket连接
|
||||
if (wsConnections.value[tokenId]) {
|
||||
closeWebSocketConnection(tokenId)
|
||||
}
|
||||
|
||||
// 如果删除的是当前选中token,清除选中状态
|
||||
if (selectedTokenId.value === tokenId) {
|
||||
selectedTokenId.value = null
|
||||
localStorage.removeItem('selectedTokenId')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const selectToken = (tokenId) => {
|
||||
const token = gameTokens.value.find(t => t.id === tokenId)
|
||||
if (token) {
|
||||
selectedTokenId.value = tokenId
|
||||
localStorage.setItem('selectedTokenId', tokenId)
|
||||
|
||||
// 更新最后使用时间
|
||||
updateToken(tokenId, { lastUsed: new Date().toISOString() })
|
||||
|
||||
// 自动建立WebSocket连接
|
||||
createWebSocketConnection(tokenId, token.token, token.wsUrl)
|
||||
|
||||
return token
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 辅助函数:分析数据结构
|
||||
const analyzeDataStructure = (obj, depth = 0, maxDepth = 3) => {
|
||||
if (depth > maxDepth || !obj || typeof obj !== 'object') {
|
||||
return typeof obj
|
||||
}
|
||||
|
||||
const structure = {}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (Array.isArray(value)) {
|
||||
structure[key] = `Array[${value.length}]${value.length > 0 ? `: ${analyzeDataStructure(value[0], depth + 1, maxDepth)}` : ''}`
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
structure[key] = analyzeDataStructure(value, depth + 1, maxDepth)
|
||||
} else {
|
||||
structure[key] = typeof value
|
||||
}
|
||||
}
|
||||
return structure
|
||||
}
|
||||
|
||||
// 辅助函数:尝试解析队伍数据
|
||||
const tryParseTeamData = (data, cmd) => {
|
||||
console.log(`👥 尝试解析队伍数据 [${cmd}]:`, data)
|
||||
|
||||
// 查找队伍相关字段
|
||||
const teamFields = []
|
||||
const scanForTeamData = (obj, path = '') => {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const currentPath = path ? `${path}.${key}` : key
|
||||
|
||||
if (key.toLowerCase().includes('team') ||
|
||||
key.toLowerCase().includes('preset') ||
|
||||
key.toLowerCase().includes('formation') ||
|
||||
key.toLowerCase().includes('lineup')) {
|
||||
teamFields.push({
|
||||
path: currentPath,
|
||||
key: key,
|
||||
value: value,
|
||||
type: typeof value,
|
||||
isArray: Array.isArray(value)
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
scanForTeamData(value, currentPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanForTeamData(data)
|
||||
|
||||
if (teamFields.length > 0) {
|
||||
console.log(`👥 找到 ${teamFields.length} 个队伍相关字段:`, teamFields)
|
||||
|
||||
// 尝试更新游戏数据
|
||||
teamFields.forEach(field => {
|
||||
if (field.key === 'presetTeamInfo' || field.path.includes('presetTeamInfo')) {
|
||||
console.log(`👥 发现预设队伍信息,准备更新:`, field.value)
|
||||
if (!gameData.value.presetTeam) {
|
||||
gameData.value.presetTeam = {}
|
||||
}
|
||||
gameData.value.presetTeam.presetTeamInfo = field.value
|
||||
gameData.value.lastUpdated = new Date().toISOString()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log(`👥 未找到明显的队伍字段,完整数据结构:`, analyzeDataStructure(data))
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏消息处理
|
||||
const handleGameMessage = (tokenId, message) => {
|
||||
try {
|
||||
if (!message || message.error) {
|
||||
console.warn(`⚠️ 消息处理跳过 [${tokenId}]:`, message?.error || '无效消息')
|
||||
return
|
||||
}
|
||||
|
||||
const cmd = message.cmd?.toLowerCase()
|
||||
// 优先使用rawData(ProtoMsg自动解码),然后decodedBody(手动解码),最后body(原始数据)
|
||||
const body = message.rawData !== undefined ? message.rawData :
|
||||
message.decodedBody !== undefined ? message.decodedBody :
|
||||
message.body
|
||||
|
||||
console.log(`📋 处理消息 [${tokenId}] ${cmd}:`, {
|
||||
hasRawData: message.rawData !== undefined,
|
||||
hasDecodedBody: message.decodedBody !== undefined,
|
||||
hasBody: message.body !== undefined,
|
||||
bodyType: body ? typeof body : 'undefined',
|
||||
bodyContent: body,
|
||||
originalCmd: message.cmd,
|
||||
fullMessage: message
|
||||
})
|
||||
|
||||
// 记录所有消息的原始命令名
|
||||
console.log(`📨 收到消息 [${tokenId}] 原始cmd: "${message.cmd}", 处理cmd: "${cmd}"`)
|
||||
|
||||
// 特别记录所有包含tower的消息
|
||||
if (cmd && cmd.includes('tower')) {
|
||||
console.log(`🗼 发现塔相关消息 [${tokenId}] ${cmd}:`, message)
|
||||
}
|
||||
|
||||
// 处理角色信息 - 支持多种可能的响应命令
|
||||
if (cmd === 'role_getroleinfo' || cmd === 'role_getroleinforesp' || cmd.includes('role') && cmd.includes('info')) {
|
||||
console.log(`📊 匹配到角色信息命令: ${cmd}`)
|
||||
|
||||
if (body) {
|
||||
gameData.value.roleInfo = body
|
||||
gameData.value.lastUpdated = new Date().toISOString()
|
||||
console.log('📊 角色信息已更新:', body)
|
||||
console.log('📊 角色信息类型:', typeof body)
|
||||
console.log('📊 角色信息内容概览:', Object.keys(body || {}))
|
||||
|
||||
// 特别检查塔信息
|
||||
if (body.role?.tower) {
|
||||
console.log('🗼 在角色信息中找到塔信息:', body.role.tower)
|
||||
} else if (body.tower) {
|
||||
console.log('🗼 在响应根级别找到塔信息:', body.tower)
|
||||
} else {
|
||||
console.log('🗼 未找到塔信息在角色数据中')
|
||||
console.log('📊 角色数据结构:', body.role ? Object.keys(body.role) : '没有role对象')
|
||||
}
|
||||
} else {
|
||||
console.log('📊 角色信息响应body为空')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理军团信息
|
||||
else if (cmd === 'legion_getinfo') {
|
||||
if (body) {
|
||||
gameData.value.legionInfo = body
|
||||
console.log('🏛️ 军团信息已更新:', body)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理队伍信息 - 支持多种队伍相关响应
|
||||
else if (cmd === 'presetteam_getteam' || cmd === 'presetteam_getteamresp' ||
|
||||
cmd === 'presetteam_setteam' || cmd === 'presetteam_setteamresp' ||
|
||||
cmd === 'presetteam_saveteam' || cmd === 'presetteam_saveteamresp' ||
|
||||
cmd === 'role_gettargetteam' || cmd === 'role_gettargetteamresp' ||
|
||||
(cmd && cmd.includes('presetteam')) || (cmd && cmd.includes('team'))) {
|
||||
console.log(`👥 匹配到队伍信息命令: ${cmd}`)
|
||||
|
||||
if (body) {
|
||||
// 更新队伍数据
|
||||
if (!gameData.value.presetTeam) {
|
||||
gameData.value.presetTeam = {}
|
||||
}
|
||||
|
||||
// 根据不同的响应类型处理数据
|
||||
if (cmd.includes('getteam')) {
|
||||
// 获取队伍信息响应
|
||||
gameData.value.presetTeam = { ...gameData.value.presetTeam, ...body }
|
||||
} else if (cmd.includes('setteam') || cmd.includes('saveteam')) {
|
||||
// 设置/保存队伍响应 - 可能只返回确认信息
|
||||
if (body.presetTeamInfo) {
|
||||
gameData.value.presetTeam.presetTeamInfo = body.presetTeamInfo
|
||||
}
|
||||
// 合并其他队伍相关数据
|
||||
Object.keys(body).forEach(key => {
|
||||
if (key.includes('team') || key.includes('Team')) {
|
||||
gameData.value.presetTeam[key] = body[key]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 其他队伍相关响应
|
||||
gameData.value.presetTeam = { ...gameData.value.presetTeam, ...body }
|
||||
}
|
||||
|
||||
gameData.value.lastUpdated = new Date().toISOString()
|
||||
console.log('👥 队伍信息已更新:', {
|
||||
cmd: cmd,
|
||||
updatedData: gameData.value.presetTeam,
|
||||
bodyKeys: Object.keys(body),
|
||||
bodyContent: body
|
||||
})
|
||||
|
||||
// 详细日志队伍数据结构
|
||||
if (gameData.value.presetTeam.presetTeamInfo) {
|
||||
console.log('👥 队伍详细结构:', {
|
||||
teamCount: Object.keys(gameData.value.presetTeam.presetTeamInfo).length,
|
||||
teamIds: Object.keys(gameData.value.presetTeam.presetTeamInfo),
|
||||
useTeamId: gameData.value.presetTeam.presetTeamInfo.useTeamId,
|
||||
sampleTeam: gameData.value.presetTeam.presetTeamInfo[1] || gameData.value.presetTeam.presetTeamInfo[Object.keys(gameData.value.presetTeam.presetTeamInfo)[0]]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.log('👥 队伍信息响应body为空')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理爬塔响应
|
||||
else if (cmd === 'fight_starttower' || cmd === 'fight_starttowerresp') {
|
||||
if (body) {
|
||||
console.log('🗼 爬塔响应:', body)
|
||||
// 爬塔后立即更新角色信息和塔信息
|
||||
setTimeout(() => {
|
||||
console.log('🗼 爬塔后自动更新数据')
|
||||
try {
|
||||
const connection = wsConnections.value[tokenId]
|
||||
if (connection && connection.status === 'connected' && connection.client) {
|
||||
// 获取最新角色信息
|
||||
console.log('🗼 正在请求角色信息...')
|
||||
connection.client.send('role_getroleinfo', {})
|
||||
} else {
|
||||
console.warn('🗼 WebSocket未连接,无法更新数据')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('爬塔后更新数据失败:', error)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理心跳响应
|
||||
else if (cmd === '_sys/ack') {
|
||||
console.log(`💗 心跳响应 [${tokenId}]`)
|
||||
}
|
||||
|
||||
// 处理其他消息
|
||||
else {
|
||||
console.log(`📋 收到游戏消息 [${tokenId}] ${cmd}:`, body)
|
||||
|
||||
// 特别关注队伍相关的未处理消息
|
||||
if (cmd && (cmd.includes('team') || cmd.includes('preset') || cmd.includes('formation'))) {
|
||||
console.log(`👥 未处理的队伍相关消息 [${tokenId}] ${cmd}:`, {
|
||||
originalMessage: message,
|
||||
parsedBody: body,
|
||||
messageKeys: Object.keys(message || {}),
|
||||
bodyStructure: body ? analyzeDataStructure(body) : null
|
||||
})
|
||||
|
||||
// 尝试自动解析队伍数据
|
||||
if (body && typeof body === 'object') {
|
||||
tryParseTeamData(body, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// 特别关注塔相关的未处理消息
|
||||
if (cmd && cmd.includes('tower')) {
|
||||
console.log(`🗼 未处理的塔相关消息 [${tokenId}] ${cmd}:`, {
|
||||
originalMessage: message,
|
||||
parsedBody: body,
|
||||
messageKeys: Object.keys(message || {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理游戏消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Base64解析功能
|
||||
const parseBase64Token = (base64String) => {
|
||||
try {
|
||||
// 移除可能的前缀和空格
|
||||
const cleanBase64 = base64String.replace(/^data:.*base64,/, '').trim()
|
||||
|
||||
// 解码base64
|
||||
const decoded = atob(cleanBase64)
|
||||
|
||||
// 尝试解析为JSON
|
||||
let tokenData
|
||||
try {
|
||||
tokenData = JSON.parse(decoded)
|
||||
} catch {
|
||||
// 如果不是JSON,当作纯token字符串处理
|
||||
tokenData = { token: decoded }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: tokenData
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: '解析失败:' + error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const importBase64Token = (name, base64String, additionalInfo = {}) => {
|
||||
const parseResult = parseBase64Token(base64String)
|
||||
|
||||
if (!parseResult.success) {
|
||||
return parseResult
|
||||
}
|
||||
|
||||
const tokenData = {
|
||||
name,
|
||||
token: parseResult.data.token || parseResult.data.gameToken || base64String,
|
||||
...additionalInfo,
|
||||
...parseResult.data // 解析出的数据覆盖手动输入
|
||||
}
|
||||
|
||||
const newToken = addToken(tokenData)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: newToken,
|
||||
message: `Token "${name}" 导入成功`
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket连接管理
|
||||
const createWebSocketConnection = (tokenId, base64Token, customWsUrl = null) => {
|
||||
if (wsConnections.value[tokenId]) {
|
||||
closeWebSocketConnection(tokenId)
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析Base64获取实际Token
|
||||
let actualToken = base64Token
|
||||
|
||||
// 尝试解析Base64获取实际token
|
||||
try {
|
||||
const cleanBase64 = base64Token.replace(/^data:.*base64,/, '').trim()
|
||||
const decoded = atob(cleanBase64)
|
||||
|
||||
// 尝试解析为JSON获取token字段
|
||||
try {
|
||||
const tokenData = JSON.parse(decoded)
|
||||
actualToken = tokenData.token || tokenData.gameToken || decoded
|
||||
} catch {
|
||||
// 如果不是JSON,直接使用解码后的字符串
|
||||
actualToken = decoded
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Base64解析失败,使用原始token:', error.message)
|
||||
actualToken = base64Token
|
||||
}
|
||||
|
||||
// 使用固定的WebSocket基础地址,将token带入占位符
|
||||
const baseWsUrl = 'wss://xxz-xyzw.hortorgames.com/agent?p=%s&e=x&lang=chinese'
|
||||
const wsUrl = customWsUrl || baseWsUrl.replace('%s', encodeURIComponent(actualToken))
|
||||
|
||||
console.log(`🔗 创建WebSocket连接:`, wsUrl)
|
||||
console.log(`🎯 Token ID: ${tokenId}`)
|
||||
console.log(`🔑 使用Token: ${actualToken.substring(0, 20)}...`)
|
||||
|
||||
// 检查g_utils结构
|
||||
console.log('🔍 g_utils结构检查:', {
|
||||
hasGetEnc: !!g_utils.getEnc,
|
||||
hasEncode: !!g_utils.encode,
|
||||
hasParse: !!g_utils.parse,
|
||||
hasBon: !!g_utils.bon,
|
||||
bonHasDecode: !!(g_utils.bon && g_utils.bon.decode)
|
||||
})
|
||||
|
||||
// 创建新的WebSocket客户端
|
||||
const wsClient = new XyzwWebSocketClient({
|
||||
url: wsUrl,
|
||||
utils: g_utils,
|
||||
heartbeatMs: 5000 // 5秒心跳间隔
|
||||
})
|
||||
|
||||
// 设置连接状态
|
||||
wsConnections.value[tokenId] = {
|
||||
client: wsClient,
|
||||
status: 'connecting',
|
||||
tokenId,
|
||||
wsUrl,
|
||||
actualToken,
|
||||
connectedAt: null,
|
||||
lastMessage: null,
|
||||
lastError: null
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
wsClient.onConnect = () => {
|
||||
console.log(`✅ WebSocket连接已建立: ${tokenId}`)
|
||||
if (wsConnections.value[tokenId]) {
|
||||
wsConnections.value[tokenId].status = 'connected'
|
||||
wsConnections.value[tokenId].connectedAt = new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
wsClient.onDisconnect = (event) => {
|
||||
console.log(`🔌 WebSocket连接已断开: ${tokenId}`, event)
|
||||
if (wsConnections.value[tokenId]) {
|
||||
wsConnections.value[tokenId].status = 'disconnected'
|
||||
}
|
||||
}
|
||||
|
||||
wsClient.onError = (error) => {
|
||||
console.error(`❌ WebSocket错误 [${tokenId}]:`, error)
|
||||
if (wsConnections.value[tokenId]) {
|
||||
wsConnections.value[tokenId].status = 'error'
|
||||
wsConnections.value[tokenId].lastError = {
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error.toString(),
|
||||
url: wsUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置消息监听
|
||||
wsClient.setMessageListener((message) => {
|
||||
console.log(`📨 收到消息 [${tokenId}]:`, message)
|
||||
|
||||
// 更新连接状态中的最后接收消息
|
||||
if (wsConnections.value[tokenId]) {
|
||||
wsConnections.value[tokenId].lastMessage = {
|
||||
timestamp: new Date().toISOString(),
|
||||
data: message
|
||||
}
|
||||
}
|
||||
|
||||
// 处理游戏消息
|
||||
handleGameMessage(tokenId, message)
|
||||
})
|
||||
|
||||
// 开启调试模式
|
||||
wsClient.setShowMsg(true)
|
||||
|
||||
// 初始化连接
|
||||
wsClient.init()
|
||||
|
||||
return wsClient
|
||||
} catch (error) {
|
||||
console.error(`创建WebSocket连接失败 [${tokenId}]:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const closeWebSocketConnection = (tokenId) => {
|
||||
const connection = wsConnections.value[tokenId]
|
||||
if (connection && connection.client) {
|
||||
connection.client.disconnect()
|
||||
delete wsConnections.value[tokenId]
|
||||
}
|
||||
}
|
||||
|
||||
const getWebSocketStatus = (tokenId) => {
|
||||
return wsConnections.value[tokenId]?.status || 'disconnected'
|
||||
}
|
||||
|
||||
// 获取WebSocket客户端
|
||||
const getWebSocketClient = (tokenId) => {
|
||||
return wsConnections.value[tokenId]?.client || null
|
||||
}
|
||||
|
||||
|
||||
// 发送消息到WebSocket
|
||||
const sendMessage = (tokenId, cmd, params = {}, options = {}) => {
|
||||
const connection = wsConnections.value[tokenId]
|
||||
if (!connection || connection.status !== 'connected') {
|
||||
console.error(`❌ WebSocket未连接,无法发送消息 [${tokenId}]`)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const client = connection.client
|
||||
if (!client) {
|
||||
console.error(`❌ WebSocket客户端不存在 [${tokenId}]`)
|
||||
return false
|
||||
}
|
||||
|
||||
client.send(cmd, params, options)
|
||||
console.log(`📤 发送消息 [${tokenId}]: ${cmd}`, params)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`❌ 发送消息失败 [${tokenId}]:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Promise版发送消息
|
||||
const sendMessageWithPromise = async (tokenId, cmd, params = {}, timeout = 5000) => {
|
||||
const connection = wsConnections.value[tokenId]
|
||||
if (!connection || connection.status !== 'connected') {
|
||||
throw new Error(`WebSocket未连接 [${tokenId}]`)
|
||||
}
|
||||
|
||||
const client = connection.client
|
||||
if (!client) {
|
||||
throw new Error(`WebSocket客户端不存在 [${tokenId}]`)
|
||||
}
|
||||
|
||||
return await client.sendWithPromise(cmd, params, timeout)
|
||||
}
|
||||
|
||||
// 发送心跳消息
|
||||
const sendHeartbeat = (tokenId) => {
|
||||
return sendMessage(tokenId, 'heart_beat')
|
||||
}
|
||||
|
||||
// 发送获取角色信息请求
|
||||
const sendGetRoleInfo = (tokenId, params = {}) => {
|
||||
return sendMessageWithPromise(tokenId, 'role_getroleinfo', params)
|
||||
}
|
||||
|
||||
// 发送获取数据版本请求
|
||||
const sendGetDataBundleVersion = (tokenId, params = {}) => {
|
||||
return sendMessageWithPromise(tokenId, 'system_getdatabundlever', params)
|
||||
}
|
||||
|
||||
// 发送签到请求
|
||||
const sendSignIn = (tokenId) => {
|
||||
return sendMessageWithPromise(tokenId, 'system_signinreward')
|
||||
}
|
||||
|
||||
// 发送领取日常任务奖励
|
||||
const sendClaimDailyReward = (tokenId, rewardId = 0) => {
|
||||
return sendMessageWithPromise(tokenId, 'task_claimdailyreward', { rewardId })
|
||||
}
|
||||
|
||||
// 发送获取队伍信息
|
||||
const sendGetTeamInfo = (tokenId, params = {}) => {
|
||||
return sendMessageWithPromise(tokenId, 'presetteam_getteam', params)
|
||||
}
|
||||
|
||||
// 发送自定义游戏消息
|
||||
const sendGameMessage = (tokenId, cmd, params = {}, options = {}) => {
|
||||
if (options.usePromise) {
|
||||
return sendMessageWithPromise(tokenId, cmd, params, options.timeout)
|
||||
} else {
|
||||
return sendMessage(tokenId, cmd, params, options)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前塔层数
|
||||
const getCurrentTowerLevel = () => {
|
||||
try {
|
||||
// 从游戏数据中获取塔信息
|
||||
const roleInfo = gameData.value.roleInfo
|
||||
if (!roleInfo || !roleInfo.role) {
|
||||
console.warn('⚠️ 角色信息不存在')
|
||||
return null
|
||||
}
|
||||
|
||||
const tower = roleInfo.role.tower
|
||||
if (!tower) {
|
||||
console.warn('⚠️ 塔信息不存在')
|
||||
return null
|
||||
}
|
||||
|
||||
// 可能的塔层数字段(根据实际数据结构调整)
|
||||
const level = tower.level || tower.currentLevel || tower.floor || tower.stage
|
||||
|
||||
console.log('🗼 当前塔层数:', level, '塔信息:', tower)
|
||||
return level
|
||||
} catch (error) {
|
||||
console.error('❌ 获取塔层数失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取详细塔信息
|
||||
const getTowerInfo = () => {
|
||||
try {
|
||||
const roleInfo = gameData.value.roleInfo
|
||||
if (!roleInfo || !roleInfo.role) {
|
||||
return null
|
||||
}
|
||||
|
||||
return roleInfo.role.tower || null
|
||||
} catch (error) {
|
||||
console.error('❌ 获取塔信息失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
const exportTokens = () => {
|
||||
return {
|
||||
tokens: gameTokens.value,
|
||||
exportedAt: new Date().toISOString(),
|
||||
version: '2.0'
|
||||
}
|
||||
}
|
||||
|
||||
const importTokens = (data) => {
|
||||
try {
|
||||
if (data.tokens && Array.isArray(data.tokens)) {
|
||||
gameTokens.value = data.tokens
|
||||
saveTokensToStorage()
|
||||
return { success: true, message: `成功导入 ${data.tokens.length} 个Token` }
|
||||
} else {
|
||||
return { success: false, message: '导入数据格式错误' }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, message: '导入失败:' + error.message }
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllTokens = () => {
|
||||
// 关闭所有WebSocket连接
|
||||
Object.keys(wsConnections.value).forEach(tokenId => {
|
||||
closeWebSocketConnection(tokenId)
|
||||
})
|
||||
|
||||
gameTokens.value = []
|
||||
selectedTokenId.value = null
|
||||
localStorage.removeItem('gameTokens')
|
||||
localStorage.removeItem('selectedTokenId')
|
||||
}
|
||||
|
||||
const cleanExpiredTokens = () => {
|
||||
const now = new Date()
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
const cleanedTokens = gameTokens.value.filter(token => {
|
||||
const lastUsed = new Date(token.lastUsed || token.createdAt)
|
||||
return lastUsed > oneDayAgo
|
||||
})
|
||||
|
||||
const cleanedCount = gameTokens.value.length - cleanedTokens.length
|
||||
gameTokens.value = cleanedTokens
|
||||
saveTokensToStorage()
|
||||
|
||||
return cleanedCount
|
||||
}
|
||||
|
||||
const saveTokensToStorage = () => {
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const initTokenStore = () => {
|
||||
// 恢复数据
|
||||
const savedTokens = localStorage.getItem('gameTokens')
|
||||
const savedSelectedId = localStorage.getItem('selectedTokenId')
|
||||
|
||||
if (savedTokens) {
|
||||
try {
|
||||
gameTokens.value = JSON.parse(savedTokens)
|
||||
} catch (error) {
|
||||
console.error('解析Token数据失败:', error)
|
||||
gameTokens.value = []
|
||||
}
|
||||
}
|
||||
|
||||
if (savedSelectedId) {
|
||||
selectedTokenId.value = savedSelectedId
|
||||
}
|
||||
|
||||
// 清理过期token
|
||||
cleanExpiredTokens()
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
gameTokens,
|
||||
selectedTokenId,
|
||||
wsConnections,
|
||||
gameData,
|
||||
|
||||
// 计算属性
|
||||
hasTokens,
|
||||
selectedToken,
|
||||
|
||||
// Token管理方法
|
||||
addToken,
|
||||
updateToken,
|
||||
removeToken,
|
||||
selectToken,
|
||||
|
||||
// Base64解析方法
|
||||
parseBase64Token,
|
||||
importBase64Token,
|
||||
|
||||
// WebSocket方法
|
||||
createWebSocketConnection,
|
||||
closeWebSocketConnection,
|
||||
getWebSocketStatus,
|
||||
getWebSocketClient,
|
||||
sendMessage,
|
||||
sendMessageWithPromise,
|
||||
sendHeartbeat,
|
||||
sendGetRoleInfo,
|
||||
sendGetDataBundleVersion,
|
||||
sendSignIn,
|
||||
sendClaimDailyReward,
|
||||
sendGetTeamInfo,
|
||||
sendGameMessage,
|
||||
|
||||
// 工具方法
|
||||
exportTokens,
|
||||
importTokens,
|
||||
clearAllTokens,
|
||||
cleanExpiredTokens,
|
||||
initTokenStore,
|
||||
|
||||
// 塔信息方法
|
||||
getCurrentTowerLevel,
|
||||
getTowerInfo
|
||||
}
|
||||
})
|
||||
779
src/utils/bonProtocol.js
Normal file
@@ -0,0 +1,779 @@
|
||||
/**
|
||||
* BON (Binary Object Notation) 协议实现
|
||||
* 基于提供的真实 BON 源码重新实现
|
||||
*/
|
||||
import lz4 from 'lz4js';
|
||||
|
||||
// -----------------------------
|
||||
// BON 编解码器核心实现
|
||||
// -----------------------------
|
||||
|
||||
export class Int64 {
|
||||
constructor(high, low) {
|
||||
this.high = high;
|
||||
this.low = low;
|
||||
}
|
||||
}
|
||||
|
||||
export class DataReader {
|
||||
constructor(bytes) {
|
||||
this._data = bytes || new Uint8Array(0);
|
||||
this._view = null;
|
||||
this.position = 0;
|
||||
}
|
||||
|
||||
get data() { return this._data; }
|
||||
get dataView() {
|
||||
return this._view || (this._view = new DataView(this._data.buffer, this._data.byteOffset, this._data.byteLength));
|
||||
}
|
||||
|
||||
reset(bytes) {
|
||||
this._data = bytes;
|
||||
this.position = 0;
|
||||
this._view = null;
|
||||
}
|
||||
|
||||
validate(n) {
|
||||
if (this.position + n > this._data.length) {
|
||||
console.error('read eof');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
readUInt8() {
|
||||
if (!this.validate(1)) return;
|
||||
return this._data[this.position++];
|
||||
}
|
||||
|
||||
readInt16() {
|
||||
if (!this.validate(2)) return;
|
||||
const v = this._data[this.position++] | (this._data[this.position++] << 8);
|
||||
return (v << 16) >> 16;
|
||||
}
|
||||
|
||||
readInt32() {
|
||||
if (!this.validate(4)) return;
|
||||
const v = this._data[this.position++] | (this._data[this.position++] << 8) | (this._data[this.position++] << 16) | (this._data[this.position++] << 24);
|
||||
return v | 0;
|
||||
}
|
||||
|
||||
readInt64() {
|
||||
const lo = this.readInt32();
|
||||
if (lo === undefined) return;
|
||||
let _lo = lo;
|
||||
if (_lo < 0) _lo += 0x100000000;
|
||||
const hi = this.readInt32();
|
||||
if (hi === undefined) return;
|
||||
return _lo + 0x100000000 * hi;
|
||||
}
|
||||
|
||||
readFloat32() {
|
||||
if (!this.validate(4)) return;
|
||||
const v = this.dataView.getFloat32(this.position, true);
|
||||
this.position += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
readFloat64() {
|
||||
if (!this.validate(8)) return;
|
||||
const v = this.dataView.getFloat64(this.position, true);
|
||||
this.position += 8;
|
||||
return v;
|
||||
}
|
||||
|
||||
read7BitInt() {
|
||||
let value = 0;
|
||||
let shift = 0;
|
||||
let b = 0;
|
||||
let count = 0;
|
||||
do {
|
||||
if (count++ === 35) throw new Error('Format_Bad7BitInt32');
|
||||
b = this.readUInt8();
|
||||
value |= (b & 0x7F) << shift;
|
||||
shift += 7;
|
||||
} while ((b & 0x80) !== 0);
|
||||
return value >>> 0;
|
||||
}
|
||||
|
||||
readUTF() {
|
||||
const len = this.read7BitInt();
|
||||
return this.readUTFBytes(len);
|
||||
}
|
||||
|
||||
readUint8Array(length, copy = false) {
|
||||
const start = this.position;
|
||||
const end = start + length;
|
||||
const out = copy ? this._data.slice(start, end) : this._data.subarray(start, end);
|
||||
this.position = end;
|
||||
return out;
|
||||
}
|
||||
|
||||
readUTFBytes(length) {
|
||||
if (length === 0) return '';
|
||||
if (!this.validate(length)) return;
|
||||
const str = new TextDecoder('utf8').decode(this._data.subarray(this.position, this.position + length));
|
||||
this.position += length;
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
let _shared = new Uint8Array(524288); // 512 KB initial buffer
|
||||
|
||||
export class DataWriter {
|
||||
constructor() {
|
||||
this.position = 0;
|
||||
this._view = null;
|
||||
this.data = _shared;
|
||||
}
|
||||
|
||||
get dataView() {
|
||||
return this._view || (this._view = new DataView(this.data.buffer, 0, this.data.byteLength));
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.data = _shared;
|
||||
this._view = null;
|
||||
this.position = 0;
|
||||
}
|
||||
|
||||
ensureBuffer(size) {
|
||||
if (this.position + size <= _shared.byteLength) return;
|
||||
const prev = _shared;
|
||||
const need = this.position + size;
|
||||
const nextLen = Math.max(Math.floor((_shared.byteLength * 12) / 10), need);
|
||||
_shared = new Uint8Array(nextLen);
|
||||
_shared.set(prev, 0);
|
||||
this.data = _shared;
|
||||
this._view = null;
|
||||
}
|
||||
|
||||
writeInt8(v) {
|
||||
this.ensureBuffer(1);
|
||||
this.data[this.position++] = v | 0;
|
||||
}
|
||||
|
||||
writeInt16(v) {
|
||||
this.ensureBuffer(2);
|
||||
this.data[this.position++] = v | 0;
|
||||
this.data[this.position++] = (v >> 8) & 0xFF;
|
||||
}
|
||||
|
||||
writeInt32(v) {
|
||||
this.ensureBuffer(4);
|
||||
this.data[this.position++] = v | 0;
|
||||
this.data[this.position++] = (v >> 8) & 0xFF;
|
||||
this.data[this.position++] = (v >> 16) & 0xFF;
|
||||
this.data[this.position++] = (v >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
writeInt64(v) {
|
||||
this.writeInt32(v);
|
||||
if (v < 0) {
|
||||
this.writeInt32(~Math.floor((-v) / 0x100000000));
|
||||
} else {
|
||||
this.writeInt32(Math.floor(v / 0x100000000) | 0);
|
||||
}
|
||||
}
|
||||
|
||||
writeFloat32(v) {
|
||||
this.ensureBuffer(4);
|
||||
this.dataView.setFloat32(this.position, v, true);
|
||||
this.position += 4;
|
||||
}
|
||||
|
||||
writeFloat64(v) {
|
||||
this.ensureBuffer(8);
|
||||
this.dataView.setFloat64(this.position, v, true);
|
||||
this.position += 8;
|
||||
}
|
||||
|
||||
_write7BitInt(v) {
|
||||
let n = v >>> 0;
|
||||
while (n >= 0x80) {
|
||||
this.data[this.position++] = (n & 0xFF) | 0x80;
|
||||
n >>>= 7;
|
||||
}
|
||||
this.data[this.position++] = n & 0x7F;
|
||||
}
|
||||
|
||||
write7BitInt(v) {
|
||||
this.ensureBuffer(5);
|
||||
this._write7BitInt(v);
|
||||
}
|
||||
|
||||
_7BitIntLen(v) {
|
||||
return v < 0 ? 5
|
||||
: v < 0x80 ? 1
|
||||
: v < 0x4000 ? 2
|
||||
: v < 0x200000 ? 3
|
||||
: v < 0x10000000 ? 4
|
||||
: 5;
|
||||
}
|
||||
|
||||
writeUTF(str) {
|
||||
const t = str.length;
|
||||
if (t === 0) {
|
||||
this.write7BitInt(0);
|
||||
return;
|
||||
}
|
||||
const max = 6 * t;
|
||||
this.ensureBuffer(5 + max);
|
||||
const start = this.position;
|
||||
this.position += this._7BitIntLen(max);
|
||||
const from = this.position;
|
||||
const reserved = from - start;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const { written } = encoder.encodeInto(str, this.data.subarray(this.position));
|
||||
this.position += written;
|
||||
const after = this.position;
|
||||
const size = after - from;
|
||||
|
||||
this.position = start;
|
||||
this._write7BitInt(size);
|
||||
const used = this.position - start;
|
||||
if (used !== reserved) {
|
||||
this.data.copyWithin(from + (used - reserved), from, after);
|
||||
}
|
||||
this.position = from + size + (used - reserved);
|
||||
}
|
||||
|
||||
writeUint8Array(src, offset = 0, length) {
|
||||
const start = offset | 0;
|
||||
const end = Math.min(src.byteLength, start + (length ?? src.byteLength));
|
||||
const n = end - start;
|
||||
if (n <= 0) return;
|
||||
this.ensureBuffer(n);
|
||||
this.data.set(src.subarray(start, end), this.position);
|
||||
this.position += n;
|
||||
}
|
||||
|
||||
writeUTFBytes(str) {
|
||||
this.ensureBuffer(6 * str.length);
|
||||
const encoder = new TextEncoder();
|
||||
const { written } = encoder.encodeInto(str, this.data.subarray(this.position));
|
||||
this.position += written;
|
||||
}
|
||||
|
||||
getBytes(clone = false) {
|
||||
return clone ? this.data.slice(0, this.position) : this.data.subarray(0, this.position);
|
||||
}
|
||||
}
|
||||
|
||||
export class BonEncoder {
|
||||
constructor() {
|
||||
this.dw = new DataWriter();
|
||||
this.strMap = new Map();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.dw.reset();
|
||||
this.strMap.clear();
|
||||
}
|
||||
|
||||
encodeInt(v) {
|
||||
this.dw.writeInt8(1);
|
||||
this.dw.writeInt32(v | 0);
|
||||
}
|
||||
|
||||
encodeLong(v) {
|
||||
this.dw.writeInt8(2);
|
||||
if (typeof v === 'number') {
|
||||
this.dw.writeInt64(v);
|
||||
} else {
|
||||
this.dw.writeInt32(v.low | 0);
|
||||
this.dw.writeInt32(v.high | 0);
|
||||
}
|
||||
}
|
||||
|
||||
encodeFloat(v) {
|
||||
this.dw.writeInt8(3);
|
||||
this.dw.writeFloat32(v);
|
||||
}
|
||||
|
||||
encodeDouble(v) {
|
||||
this.dw.writeInt8(4);
|
||||
this.dw.writeFloat64(v);
|
||||
}
|
||||
|
||||
encodeNumber(v) {
|
||||
if ((v | 0) === v) this.encodeInt(v);
|
||||
else if (Math.floor(v) === v) this.encodeLong(v);
|
||||
else this.encodeDouble(v);
|
||||
}
|
||||
|
||||
encodeString(s) {
|
||||
const hit = this.strMap.get(s);
|
||||
if (hit !== undefined) {
|
||||
this.dw.writeInt8(99); // StringRef
|
||||
this.dw.write7BitInt(hit);
|
||||
return;
|
||||
}
|
||||
this.dw.writeInt8(5); // String
|
||||
this.dw.writeUTF(s);
|
||||
this.strMap.set(s, this.strMap.size);
|
||||
}
|
||||
|
||||
encodeBoolean(b) {
|
||||
this.dw.writeInt8(6);
|
||||
this.dw.writeInt8(b ? 1 : 0);
|
||||
}
|
||||
|
||||
encodeNull() {
|
||||
this.dw.writeInt8(0);
|
||||
}
|
||||
|
||||
encodeDateTime(d) {
|
||||
this.dw.writeInt8(10);
|
||||
this.dw.writeInt64(d.getTime());
|
||||
}
|
||||
|
||||
encodeBinary(u8) {
|
||||
this.dw.writeInt8(7);
|
||||
this.dw.write7BitInt(u8.byteLength);
|
||||
this.dw.writeUint8Array(u8);
|
||||
}
|
||||
|
||||
encodeArray(arr) {
|
||||
this.dw.writeInt8(9);
|
||||
this.dw.write7BitInt(arr.length);
|
||||
for (let i = 0; i < arr.length; i++) this.encode(arr[i]);
|
||||
}
|
||||
|
||||
encodeMap(mp) {
|
||||
this.dw.writeInt8(8);
|
||||
this.dw.write7BitInt(mp.size);
|
||||
mp.forEach((v, k) => {
|
||||
this.encode(k);
|
||||
this.encode(v);
|
||||
});
|
||||
}
|
||||
|
||||
encodeObject(obj) {
|
||||
this.dw.writeInt8(8);
|
||||
const keys = [];
|
||||
for (const k in obj) {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
|
||||
if (k.startsWith('_')) continue;
|
||||
const type = typeof obj[k];
|
||||
if (type === 'function' || type === 'undefined') continue;
|
||||
keys.push(k);
|
||||
}
|
||||
this.dw.write7BitInt(keys.length);
|
||||
for (const k of keys) {
|
||||
this.encode(k);
|
||||
this.encode(obj[k]);
|
||||
}
|
||||
}
|
||||
|
||||
encode(v) {
|
||||
if (v == null) {
|
||||
this.encodeNull();
|
||||
return;
|
||||
}
|
||||
switch (v.constructor) {
|
||||
case Number:
|
||||
this.encodeNumber(v);
|
||||
return;
|
||||
case Boolean:
|
||||
this.encodeBoolean(v);
|
||||
return;
|
||||
case String:
|
||||
this.encodeString(v);
|
||||
return;
|
||||
case Int64:
|
||||
this.encodeLong(v);
|
||||
return;
|
||||
case Array:
|
||||
this.encodeArray(v);
|
||||
return;
|
||||
case Map:
|
||||
this.encodeMap(v);
|
||||
return;
|
||||
case Date:
|
||||
this.encodeDateTime(v);
|
||||
return;
|
||||
case Uint8Array:
|
||||
this.encodeBinary(v);
|
||||
return;
|
||||
default:
|
||||
if (typeof v !== 'object') {
|
||||
this.encodeNull();
|
||||
return;
|
||||
}
|
||||
this.encodeObject(v);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getBytes(clone = false) {
|
||||
return this.dw.getBytes(clone);
|
||||
}
|
||||
}
|
||||
|
||||
export class BonDecoder {
|
||||
constructor() {
|
||||
this.dr = new DataReader(new Uint8Array(0));
|
||||
this.strArr = [];
|
||||
}
|
||||
|
||||
reset(bytes) {
|
||||
this.dr.reset(bytes);
|
||||
this.strArr.length = 0;
|
||||
}
|
||||
|
||||
decode() {
|
||||
const tag = this.dr.readUInt8();
|
||||
switch (tag) {
|
||||
default:
|
||||
return null;
|
||||
case 1:
|
||||
return this.dr.readInt32();
|
||||
case 2:
|
||||
return this.dr.readInt64();
|
||||
case 3:
|
||||
return this.dr.readFloat32();
|
||||
case 4:
|
||||
return this.dr.readFloat64();
|
||||
case 5: {
|
||||
const s = this.dr.readUTF();
|
||||
this.strArr.push(s);
|
||||
return s;
|
||||
}
|
||||
case 6:
|
||||
return this.dr.readUInt8() === 1;
|
||||
case 7: {
|
||||
const len = this.dr.read7BitInt();
|
||||
return this.dr.readUint8Array(len, false);
|
||||
}
|
||||
case 8: {
|
||||
const count = this.dr.read7BitInt();
|
||||
const obj = {};
|
||||
for (let i = 0; i < count; i++) {
|
||||
const k = this.decode();
|
||||
const v = this.decode();
|
||||
obj[k] = v;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
case 9: {
|
||||
const len = this.dr.read7BitInt();
|
||||
const arr = new Array(len);
|
||||
for (let i = 0; i < len; i++) arr[i] = this.decode();
|
||||
return arr;
|
||||
}
|
||||
case 10:
|
||||
return new Date(this.dr.readInt64());
|
||||
case 99:
|
||||
return this.strArr[this.dr.read7BitInt()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单例实例
|
||||
const _enc = new BonEncoder();
|
||||
const _dec = new BonDecoder();
|
||||
|
||||
// BON 编解码函数
|
||||
export const bon = {
|
||||
encode: (value, clone = true) => {
|
||||
_enc.reset();
|
||||
_enc.encode(value);
|
||||
return _enc.getBytes(clone);
|
||||
},
|
||||
decode: (bytes) => {
|
||||
_dec.reset(bytes);
|
||||
return _dec.decode();
|
||||
}
|
||||
};
|
||||
|
||||
/** —— 协议消息包装,与原 ProtoMsg 类等价 —— */
|
||||
export class ProtoMsg {
|
||||
constructor(raw) {
|
||||
if (raw?.cmd) {
|
||||
raw.cmd = raw.cmd.toLowerCase();
|
||||
}
|
||||
this._raw = raw;
|
||||
this._rawData = undefined;
|
||||
this._data = undefined;
|
||||
this._t = undefined;
|
||||
this._sendMsg = undefined;
|
||||
this.rtt = 0;
|
||||
}
|
||||
|
||||
get sendMsg() { return this._sendMsg; }
|
||||
get seq() { return this._raw.seq; }
|
||||
get resp() { return this._raw.resp; }
|
||||
get ack() { return this._raw.ack; }
|
||||
get cmd() { return this._raw?.cmd && this._raw?.cmd.toLowerCase(); }
|
||||
get code() { return ~~this._raw.code; }
|
||||
get error() { return this._raw.error; }
|
||||
get time() { return this._raw.time; }
|
||||
get body() { return this._raw.body; }
|
||||
|
||||
/** 惰性 decode body → rawData(bon.decode) */
|
||||
get rawData() {
|
||||
if (this._rawData !== undefined || this.body === undefined) return this._rawData;
|
||||
this._rawData = bon.decode(this.body);
|
||||
return this._rawData;
|
||||
}
|
||||
|
||||
/** 指定数据类型 */
|
||||
setDataType(t) {
|
||||
if (t) this._t = { name: t.name ?? 'Anonymous', ctor: t };
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 配置"请求"对象,让 respType 自动对齐 */
|
||||
setSendMsg(msg) {
|
||||
this._sendMsg = msg;
|
||||
return this.setDataType(msg.respType);
|
||||
}
|
||||
|
||||
/** 将 rawData 反序列化为业务对象 */
|
||||
getData(clazz) {
|
||||
if (this._data !== undefined || this.rawData === undefined) return this._data;
|
||||
|
||||
let t = this._t;
|
||||
if (clazz && t && clazz !== t.ctor) {
|
||||
console.warn(`getData type not match, ${clazz.name} != ${t.name}`);
|
||||
t = { name: clazz.name, ctor: clazz };
|
||||
}
|
||||
|
||||
this._data = this.rawData;
|
||||
return this._data;
|
||||
}
|
||||
|
||||
toLogString() {
|
||||
const e = { ...this._raw };
|
||||
delete e.body;
|
||||
e.data = this.rawData;
|
||||
e.rtt = this.rtt;
|
||||
return JSON.stringify(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** —— 加解密器注册表 —— */
|
||||
const registry = new Map();
|
||||
|
||||
/** lz4 + 头部掩码的 "lx" 方案 */
|
||||
const lx = {
|
||||
encrypt: (buf) => {
|
||||
let e = lz4.compress(buf);
|
||||
const t = 2 + ~~(Math.random() * 248);
|
||||
for (let n = Math.min(e.length, 100); --n >= 0; ) e[n] ^= t;
|
||||
|
||||
// 写入标识与混淆位
|
||||
e[0] = 112; e[1] = 108;
|
||||
e[2] = (e[2] & 0b10101010) | ((t >> 7 & 1) << 6) | ((t >> 6 & 1) << 4) | ((t >> 5 & 1) << 2) | (t >> 4 & 1);
|
||||
e[3] = (e[3] & 0b10101010) | ((t >> 3 & 1) << 6) | ((t >> 2 & 1) << 4) | ((t >> 1 & 1) << 2) | (t & 1);
|
||||
return e;
|
||||
},
|
||||
decrypt: (e) => {
|
||||
const t =
|
||||
((e[2] >> 6 & 1) << 7) | ((e[2] >> 4 & 1) << 6) | ((e[2] >> 2 & 1) << 5) | ((e[2] & 1) << 4) |
|
||||
((e[3] >> 6 & 1) << 3) | ((e[3] >> 4 & 1) << 2) | ((e[3] >> 2 & 1) << 1) | (e[3] & 1);
|
||||
for (let n = Math.min(100, e.length); --n >= 2; ) e[n] ^= t;
|
||||
e[0] = 4; e[1] = 34; e[2] = 77; e[3] = 24; // 还原头以便 lz4 解
|
||||
return lz4.decompress(e);
|
||||
}
|
||||
};
|
||||
|
||||
/** 随机首 4 字节 + XOR 的 "x" 方案 */
|
||||
const x = {
|
||||
encrypt: (e) => {
|
||||
const rnd = ~~(Math.random() * 0xFFFFFFFF) >>> 0;
|
||||
const n = new Uint8Array(e.length + 4);
|
||||
n[0] = rnd & 0xFF; n[1] = (rnd >>> 8) & 0xFF; n[2] = (rnd >>> 16) & 0xFF; n[3] = (rnd >>> 24) & 0xFF;
|
||||
n.set(e, 4);
|
||||
const r = 2 + ~~(Math.random() * 248);
|
||||
for (let i = n.length; --i >= 0; ) n[i] ^= r;
|
||||
n[0] = 112; n[1] = 120;
|
||||
n[2] = (n[2] & 0b10101010) | ((r >> 7 & 1) << 6) | ((r >> 6 & 1) << 4) | ((r >> 5 & 1) << 2) | (r >> 4 & 1);
|
||||
n[3] = (n[3] & 0b10101010) | ((r >> 3 & 1) << 6) | ((r >> 2 & 1) << 4) | ((r >> 1 & 1) << 2) | (r & 1);
|
||||
return n;
|
||||
},
|
||||
decrypt: (e) => {
|
||||
const t =
|
||||
((e[2] >> 6 & 1) << 7) | ((e[2] >> 4 & 1) << 6) | ((e[2] >> 2 & 1) << 5) | ((e[2] & 1) << 4) |
|
||||
((e[3] >> 6 & 1) << 3) | ((e[3] >> 4 & 1) << 2) | ((e[3] >> 2 & 1) << 1) | (e[3] & 1);
|
||||
for (let n = e.length; --n >= 4; ) e[n] ^= t;
|
||||
return e.subarray(4);
|
||||
}
|
||||
};
|
||||
|
||||
/** 依赖 globalThis.XXTEA 的 "xtm" 方案 */
|
||||
const xtm = {
|
||||
encrypt: (e) => globalThis.XXTEA ? globalThis.XXTEA.encryptMod({ data: e.buffer, length: e.length }) : e,
|
||||
decrypt: (e) => globalThis.XXTEA ? globalThis.XXTEA.decryptMod({ data: e.buffer, length: e.length }) : e,
|
||||
};
|
||||
|
||||
/** 注册器 */
|
||||
function register(name, impl) {
|
||||
registry.set(name, impl);
|
||||
}
|
||||
|
||||
register('lx', lx);
|
||||
register('x', x);
|
||||
register('xtm', xtm);
|
||||
|
||||
/** 默认使用 x 加密(自动检测解密) */
|
||||
const passthrough = {
|
||||
encrypt: (e) => getEnc('x').encrypt(e),
|
||||
decrypt: (e) => {
|
||||
if (e.length > 4 && e[0] === 112 && e[1] === 108) e = getEnc('lx').decrypt(e);
|
||||
else if (e.length > 4 && e[0] === 112 && e[1] === 120) e = getEnc('x').decrypt(e);
|
||||
else if (e.length > 3 && e[0] === 112 && e[1] === 116) e = getEnc('xtm').decrypt(e);
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
/** 对外:按名称取加解密器;找不到则用默认 */
|
||||
export function getEnc(name) {
|
||||
return registry.get(name) ?? passthrough;
|
||||
}
|
||||
|
||||
/** 对外:encode(bon.encode → 加密) */
|
||||
export function encode(obj, enc) {
|
||||
let bytes = bon.encode(obj, false);
|
||||
const out = enc.encrypt(bytes);
|
||||
return out.buffer.byteLength === out.length ? out.buffer : out.buffer.slice(0, out.length);
|
||||
}
|
||||
|
||||
/** 对外:parse(解密 → bon.decode → ProtoMsg) */
|
||||
export function parse(buf, enc) {
|
||||
const u8 = new Uint8Array(buf);
|
||||
const plain = enc.decrypt(u8);
|
||||
const raw = bon.decode(plain);
|
||||
return new ProtoMsg(raw);
|
||||
}
|
||||
|
||||
// 游戏消息模板
|
||||
export const GameMessages = {
|
||||
// 心跳消息
|
||||
heartBeat: (ack = 0, seq = 0) => ({
|
||||
ack,
|
||||
body: undefined,
|
||||
c: undefined,
|
||||
cmd: "_sys/ack",
|
||||
hint: undefined,
|
||||
seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 获取角色信息
|
||||
getRoleInfo: (ack = 0, seq = 0, params = {}) => ({
|
||||
cmd: "role_getroleinfo",
|
||||
body: encode({
|
||||
clientVersion: "1.65.3-wx",
|
||||
inviteUid: 0,
|
||||
platform: "hortor",
|
||||
platformExt: "mix",
|
||||
scene: "",
|
||||
...params
|
||||
}, getEnc('x')),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 获取数据包版本
|
||||
getDataBundleVer: (ack = 0, seq = 0, params = {}) => ({
|
||||
cmd: "system_getdatabundlever",
|
||||
body: encode({
|
||||
isAudit: false,
|
||||
...params
|
||||
}, getEnc('x')),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 购买金币
|
||||
buyGold: (ack = 0, seq = 0, params = {}) => ({
|
||||
ack,
|
||||
body: encode({
|
||||
buyNum: 1,
|
||||
...params
|
||||
}, getEnc('x')),
|
||||
cmd: "system_buygold",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 签到奖励
|
||||
signInReward: (ack = 0, seq = 0, params = {}) => ({
|
||||
ack,
|
||||
body: encode({
|
||||
...params
|
||||
}, getEnc('x')),
|
||||
cmd: "system_signinreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 领取每日任务奖励
|
||||
claimDailyReward: (ack = 0, seq = 0, params = {}) => ({
|
||||
ack,
|
||||
body: encode({
|
||||
rewardId: 0,
|
||||
...params
|
||||
}, getEnc('x')),
|
||||
cmd: "task_claimdailyreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
})
|
||||
};
|
||||
|
||||
// 创建全局实例
|
||||
export const g_utils = {
|
||||
getEnc,
|
||||
encode: (obj, encName = 'x') => encode(obj, getEnc(encName)),
|
||||
parse: (data, encName = 'auto') => parse(data, getEnc(encName)),
|
||||
bon // 添加BON编解码器
|
||||
};
|
||||
|
||||
// 兼容性导出(保持旧的接口)
|
||||
export const bonProtocol = {
|
||||
encode: bon.encode,
|
||||
decode: bon.decode,
|
||||
createMessage: (cmd, body = {}, ack = 0, seq = 0, options = {}) => ({
|
||||
cmd,
|
||||
body: bon.encode(body),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
time: Date.now(),
|
||||
...options
|
||||
}),
|
||||
parseMessage: (messageData) => {
|
||||
try {
|
||||
let message;
|
||||
if (typeof messageData === 'string') {
|
||||
message = JSON.parse(messageData);
|
||||
} else {
|
||||
message = messageData;
|
||||
}
|
||||
if (message.body && (message.body instanceof ArrayBuffer || message.body instanceof Uint8Array)) {
|
||||
message.body = bon.decode(message.body);
|
||||
}
|
||||
return message;
|
||||
} catch (error) {
|
||||
console.error('消息解析失败:', error);
|
||||
return {
|
||||
error: true,
|
||||
message: '消息解析失败',
|
||||
originalData: messageData
|
||||
};
|
||||
}
|
||||
},
|
||||
generateSeq: () => Math.floor(Math.random() * 1000000),
|
||||
generateMessageId: () => 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
||||
};
|
||||
|
||||
// 导出单独的加密器类以兼容测试文件
|
||||
export const LXCrypto = lx;
|
||||
export const XCrypto = x;
|
||||
export const XTMCrypto = xtm;
|
||||
|
||||
export default { ProtoMsg, getEnc, encode, parse, GameMessages, g_utils, bon, bonProtocol };
|
||||
692
src/utils/gameCommands.js
Normal file
@@ -0,0 +1,692 @@
|
||||
/**
|
||||
* 游戏命令构造器
|
||||
* 基于mirror代码中的游戏指令实现完整的游戏功能
|
||||
*/
|
||||
|
||||
import { g_utils } from './bonProtocol.js'
|
||||
|
||||
// 生成随机数工具函数
|
||||
function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏命令构造器类
|
||||
* 每个命令方法返回标准的WebSocket消息格式
|
||||
*/
|
||||
export class GameCommands {
|
||||
constructor(g_utils_instance = g_utils) {
|
||||
this.g_utils = g_utils_instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳消息
|
||||
*/
|
||||
heart_beat(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: undefined,
|
||||
c: undefined,
|
||||
cmd: "_sys/ack",
|
||||
hint: undefined,
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色信息
|
||||
*/
|
||||
role_getroleinfo(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
cmd: "role_getroleinfo",
|
||||
body: this.g_utils.bon.encode({
|
||||
clientVersion: "1.65.3-wx",
|
||||
inviteUid: 0,
|
||||
platform: "hortor",
|
||||
platformExt: "mix",
|
||||
scene: "",
|
||||
...params
|
||||
}),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
rtt: randomInt(0, 500),
|
||||
code: 0,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据包版本
|
||||
*/
|
||||
system_getdatabundlever(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
cmd: "system_getdatabundlever",
|
||||
body: this.g_utils.bon.encode({
|
||||
isAudit: false,
|
||||
...params
|
||||
}),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
rtt: randomInt(0, 500),
|
||||
code: 0,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买金币
|
||||
*/
|
||||
system_buygold(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
buyNum: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "system_buygold",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享回调
|
||||
*/
|
||||
system_mysharecallback(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
type: 3,
|
||||
isSkipShareCard: true,
|
||||
...params
|
||||
}),
|
||||
cmd: "system_mysharecallback",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 好友批处理
|
||||
*/
|
||||
friend_batch(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
friendId: 0,
|
||||
...params
|
||||
}),
|
||||
cmd: "friend_batch",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 英雄招募
|
||||
*/
|
||||
hero_recruit(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
byClub: false,
|
||||
recruitNumber: 1,
|
||||
recruitType: 3,
|
||||
...params
|
||||
}),
|
||||
cmd: "hero_recruit",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取挂机奖励
|
||||
*/
|
||||
system_claimhangupreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "system_claimhangupreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开宝箱
|
||||
*/
|
||||
item_openbox(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
itemId: 2001,
|
||||
number: 10,
|
||||
...params
|
||||
}),
|
||||
cmd: "item_openbox",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始竞技场
|
||||
*/
|
||||
arena_startarea(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "arena_startarea",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取竞技场目标
|
||||
*/
|
||||
arena_getareatarget(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
refresh: false,
|
||||
...params
|
||||
}),
|
||||
cmd: "arena_getareatarget",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始竞技场战斗
|
||||
*/
|
||||
fight_startareaarena(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
targetId: 530479307,
|
||||
...params
|
||||
}),
|
||||
cmd: "fight_startareaarena",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取竞技场排名
|
||||
*/
|
||||
arena_getarearank(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
rankType: 0,
|
||||
...params
|
||||
}),
|
||||
cmd: "arena_getarearank",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商店商品列表
|
||||
*/
|
||||
store_goodslist(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
storeId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "store_goodslist",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 商店购买
|
||||
*/
|
||||
store_buy(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
goodsId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "store_buy",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 商店刷新
|
||||
*/
|
||||
store_refresh(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
storeId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "store_refresh",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取机器人助手奖励
|
||||
*/
|
||||
bottlehelper_claim(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "bottlehelper_claim",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动机器人助手
|
||||
*/
|
||||
bottlehelper_start(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
bottleType: -1,
|
||||
...params
|
||||
}),
|
||||
cmd: "bottlehelper_start",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止机器人助手
|
||||
*/
|
||||
bottlehelper_stop(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
bottleType: -1,
|
||||
...params
|
||||
}),
|
||||
cmd: "bottlehelper_stop",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 神器抽奖
|
||||
*/
|
||||
artifact_lottery(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
lotteryNumber: 1,
|
||||
newFree: true,
|
||||
type: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "artifact_lottery",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取每日积分
|
||||
*/
|
||||
task_claimdailypoint(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
taskId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "task_claimdailypoint",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取周奖励
|
||||
*/
|
||||
task_claimweekreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
rewardId: 0,
|
||||
...params
|
||||
}),
|
||||
cmd: "task_claimweekreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始BOSS战
|
||||
*/
|
||||
fight_startboss(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "fight_startboss",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 精灵扫荡
|
||||
*/
|
||||
genie_sweep(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "genie_sweep",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买精灵扫荡
|
||||
*/
|
||||
genie_buysweep(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "genie_buysweep",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 签到奖励
|
||||
*/
|
||||
system_signinreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "system_signinreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取折扣奖励
|
||||
*/
|
||||
discount_claimreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
discountId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "discount_claimreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取卡片奖励
|
||||
*/
|
||||
card_claimreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
cardId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "card_claimreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 军团签到
|
||||
*/
|
||||
legion_signin(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "legion_signin",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始军团BOSS战
|
||||
*/
|
||||
fight_startlegionboss(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "fight_startlegionboss",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取每日任务奖励
|
||||
*/
|
||||
task_claimdailyreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
rewardId: 0,
|
||||
...params
|
||||
}),
|
||||
cmd: "task_claimdailyreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取军团信息
|
||||
*/
|
||||
legion_getinfo(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({}),
|
||||
cmd: "legion_getinfo",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 军团匹配角色报名
|
||||
*/
|
||||
legionmatch_rolesignup(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({}),
|
||||
cmd: "legionmatch_rolesignup",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始爬塔
|
||||
*/
|
||||
fight_starttower(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({}),
|
||||
cmd: "fight_starttower",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取爬塔奖励
|
||||
*/
|
||||
tower_claimreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "tower_claimreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取爬塔信息
|
||||
*/
|
||||
tower_getinfo(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "tower_getinfo",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始答题游戏
|
||||
*/
|
||||
study_startgame(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({}),
|
||||
cmd: "study_startgame",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 答题
|
||||
*/
|
||||
study_answer(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "study_answer",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取答题奖励
|
||||
*/
|
||||
study_claimreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
rewardId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "study_claimreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邮件列表
|
||||
*/
|
||||
mail_getlist(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
category: [0, 4, 5],
|
||||
lastId: 0,
|
||||
size: 60,
|
||||
...params
|
||||
}),
|
||||
cmd: "mail_getlist",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取所有邮件附件
|
||||
*/
|
||||
mail_claimallattachment(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
category: 0,
|
||||
...params
|
||||
}),
|
||||
cmd: "mail_claimallattachment",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 三国答题题库(基于mirror代码中的题目)
|
||||
export const studyQuestions = [
|
||||
{name: "", value: 2},
|
||||
{name: "《三国演义》中,「大意失街亭」的是马谩?", value: 1},
|
||||
{name: "《三国演义》中,「挥泪斩马谩」的是孙权?", value: 2},
|
||||
{name: "《三国演义》中,「火烧博望坡」的是庞统?", value: 2},
|
||||
{name: "《三国演义》中,「火烧藤甲兵」的是徐庶?", value: 2},
|
||||
{name: "《三国演义》中,「千里走单骑」的是赵云?", value: 2},
|
||||
{name: "《三国演义》中,「温酒斩华雄」的是张飞?", value: 2},
|
||||
{name: "《三国演义》中,关羽在长坂坡「七进七出」?", value: 2},
|
||||
{name: "《三国演义》中,刘备三顾茅庐请诸葛亮出山?", value: 1},
|
||||
{name: "《三国演义》中,孙权与曹操「煮酒论英雄」?", value: 2},
|
||||
{name: "《三国演义》中,提出「隆中对」的是诸葛亮?", value: 1},
|
||||
{name: "《三国演义》中,夏侯杰在当阳桥被张飞吓死?", value: 1},
|
||||
{name: "《三国演义》中,张飞在当阳桥厉吼吓退曹军?", value: 1},
|
||||
{name: "《三国演义》中,赵云参与了「三英战吕布」?", value: 2},
|
||||
{name: "《三国演义》中,赵云参与了「桃园三结义」?", value: 2}
|
||||
// 更多题目可以从原始数据中添加...
|
||||
]
|
||||
|
||||
// 创建命令实例
|
||||
export const gameCommands = new GameCommands()
|
||||
export default GameCommands
|
||||
547
src/utils/readable-xyzw-ws.js
Normal file
@@ -0,0 +1,547 @@
|
||||
// 解析后的XYZW WebSocket通信库
|
||||
// 原文件: CTx_gHj7.js (混淆版本)
|
||||
|
||||
// 导入依赖模块
|
||||
import { a$ as createRef, G as createApp, $ as defineComponent, n as ref, b0 as computed } from "./DpD38Hq9.js";
|
||||
import { c as useI18n, g as getConfig, u as useState } from "./BUzHT0Ek.js";
|
||||
|
||||
// 字符串相似度计算函数 (Levenshtein Distance 算法)
|
||||
const calculateStringSimilarity = (() => {
|
||||
let cache, isInitialized;
|
||||
|
||||
return createRef(isInitialized ? cache : (isInitialized = 1, cache = function () {
|
||||
// 计算两个字符串之间的编辑距离
|
||||
function calculateDistance(a, b, c, d, e) {
|
||||
return a < b || c < b ? a > c ? c + 1 : a + 1 : d === e ? b : b + 1;
|
||||
}
|
||||
|
||||
return function (str1, str2) {
|
||||
if (str1 === str2) return 0;
|
||||
|
||||
// 确保str1是较短的字符串
|
||||
if (str1.length > str2.length) {
|
||||
[str1, str2] = [str2, str1];
|
||||
}
|
||||
|
||||
let len1 = str1.length;
|
||||
let len2 = str2.length;
|
||||
|
||||
// 去除相同的前缀和后缀
|
||||
while (len1 > 0 && str1.charCodeAt(len1 - 1) === str2.charCodeAt(len2 - 1)) {
|
||||
len1--;
|
||||
len2--;
|
||||
}
|
||||
|
||||
let start = 0;
|
||||
while (start < len1 && str1.charCodeAt(start) === str2.charCodeAt(start)) {
|
||||
start++;
|
||||
}
|
||||
|
||||
len2 -= start;
|
||||
len1 -= start;
|
||||
|
||||
if (len1 === 0 || len2 < 3) return len2;
|
||||
|
||||
// 动态规划计算编辑距离
|
||||
let row = [];
|
||||
for (let i = 0; i < len1; i++) {
|
||||
row.push(i + 1, str1.charCodeAt(start + i));
|
||||
}
|
||||
|
||||
let currentRow = 0;
|
||||
let rowLength = row.length - 1;
|
||||
|
||||
while (currentRow < len2 - 3) {
|
||||
let char1 = str2.charCodeAt(start + currentRow);
|
||||
let char2 = str2.charCodeAt(start + currentRow + 1);
|
||||
let char3 = str2.charCodeAt(start + currentRow + 2);
|
||||
let char4 = str2.charCodeAt(start + currentRow + 3);
|
||||
|
||||
let newValue = currentRow += 4;
|
||||
|
||||
for (let j = 0; j < rowLength; j += 2) {
|
||||
let oldValue = row[j];
|
||||
let charCode = row[j + 1];
|
||||
|
||||
char1 = calculateDistance(oldValue, char1, char2, char1, charCode);
|
||||
char2 = calculateDistance(char1, char2, char3, char2, charCode);
|
||||
char3 = calculateDistance(char2, char3, char4, char3, charCode);
|
||||
newValue = calculateDistance(char3, char4, newValue, char4, charCode);
|
||||
|
||||
row[j] = newValue;
|
||||
char4 = char3;
|
||||
char3 = char2;
|
||||
char2 = char1;
|
||||
char1 = oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理剩余字符
|
||||
while (currentRow < len2) {
|
||||
let char = str2.charCodeAt(start + currentRow);
|
||||
let newValue = ++currentRow;
|
||||
|
||||
for (let j = 0; j < rowLength; j += 2) {
|
||||
let oldValue = row[j];
|
||||
row[j] = newValue = calculateDistance(oldValue, char, newValue, char, row[j + 1]);
|
||||
char = oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
return newValue;
|
||||
};
|
||||
}()));
|
||||
})();
|
||||
|
||||
// 生成随机数
|
||||
function generateRandomNumber(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
// 时间格式化函数
|
||||
function formatTime(seconds) {
|
||||
const totalSeconds = Math.floor(seconds);
|
||||
const hours = Math.floor(totalSeconds / 3600).toString().padStart(2, "0");
|
||||
const remainingSeconds = totalSeconds % 3600;
|
||||
const minutes = Math.floor(remainingSeconds / 60);
|
||||
const secs = Math.floor(remainingSeconds % 60);
|
||||
|
||||
const formattedHours = hours.toString().padStart(2, "0");
|
||||
const formattedMinutes = minutes.toString().padStart(2, "0");
|
||||
const formattedSeconds = (secs < 10 ? "0" : "") + secs.toString();
|
||||
|
||||
let formatTime = "00:00:00";
|
||||
if (seconds > 0) {
|
||||
formatTime = `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
|
||||
}
|
||||
|
||||
return {
|
||||
hours: formattedHours,
|
||||
minutes: formattedMinutes,
|
||||
seconds: formattedSeconds,
|
||||
formatTime: formatTime
|
||||
};
|
||||
}
|
||||
|
||||
// 字符串相似度检查
|
||||
function checkStringSimilarity(str1, str2, threshold) {
|
||||
if (!str1 || !str2) return false;
|
||||
return 1 - calculateStringSimilarity(str1, str2) / Math.max(str1.length, str2.length) >= threshold;
|
||||
}
|
||||
|
||||
// 数值格式化函数 (支持万、亿单位)
|
||||
function formatNumber(num, decimals = 2) {
|
||||
if (num === undefined || isNaN(num) || num <= 0) return "0";
|
||||
|
||||
const billion = 100000000; // 1亿
|
||||
const tenThousand = 10000; // 1万
|
||||
|
||||
const formatDecimal = (value) => {
|
||||
const str = value.toString();
|
||||
const [integer, decimal = ""] = str.split(".");
|
||||
return decimal.length >= decimals
|
||||
? `${integer}.${decimal.slice(0, decimals)}`
|
||||
: `${integer}.${"0".repeat(decimals - decimal.length)}${decimal}`;
|
||||
};
|
||||
|
||||
if (num >= billion) {
|
||||
return `${formatDecimal(num / billion)}亿`;
|
||||
} else if (num >= tenThousand) {
|
||||
return `${formatDecimal(num / tenThousand)}万`;
|
||||
} else if (num < 1) {
|
||||
return `0.${"0".repeat(decimals)}${num.toFixed(decimals + 1).slice(-decimals)}`;
|
||||
} else {
|
||||
return num.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟函数
|
||||
function delay(milliseconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
// 游戏消息模板定义
|
||||
const gameMessageTemplates = {
|
||||
// 心跳包
|
||||
heart_beat: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: undefined,
|
||||
c: undefined,
|
||||
cmd: "_sys/ack",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 获取角色信息
|
||||
role_getroleinfo: (client, ack, seq, params) => ({
|
||||
cmd: "role_getroleinfo",
|
||||
body: client.bon.encode({
|
||||
clientVersion: "1.65.3-wx",
|
||||
inviteUid: 0,
|
||||
platform: "hortor",
|
||||
platformExt: "mix",
|
||||
scene: "",
|
||||
...params
|
||||
}),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
rtt: generateRandomNumber(0, 500),
|
||||
code: 0,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 获取数据包版本
|
||||
system_getdatabundlever: (client, ack, seq, params) => ({
|
||||
cmd: "system_getdatabundlever",
|
||||
body: client.bon.encode({
|
||||
isAudit: false,
|
||||
...params
|
||||
}),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
rtt: generateRandomNumber(0, 500),
|
||||
code: 0,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 购买金币
|
||||
system_buygold: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
buyNum: 1,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "system_buygold",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 分享回调
|
||||
system_mysharecallback: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
type: 3,
|
||||
isSkipShareCard: true,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "system_mysharecallback",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 好友批处理
|
||||
friend_batch: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
friendId: 0,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "friend_batch",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 英雄招募
|
||||
hero_recruit: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
byClub: false,
|
||||
recruitNumber: 1,
|
||||
recruitType: 3,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "hero_recruit",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 领取挂机奖励
|
||||
system_claimhangupreward: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "system_claimhangupreward",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 开启宝箱
|
||||
item_openbox: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
itemId: 2001,
|
||||
number: 10,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "item_openbox",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 竞技场相关命令
|
||||
arena_startarea: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({...params}),
|
||||
c: undefined,
|
||||
cmd: "arena_startarea",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
arena_getareatarget: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
refresh: false,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "arena_getareatarget",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
fight_startareaarena: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
targetId: 530479307,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "fight_startareaarena",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
arena_getarearank: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
rankType: 0,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "arena_getarearank",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 商店相关
|
||||
store_goodslist: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
storeId: 1,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "store_goodslist",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
store_buy: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
goodsId: 1,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "store_buy",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
store_refresh: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({...params}),
|
||||
c: undefined,
|
||||
cmd: "store_refresh",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
})
|
||||
};
|
||||
|
||||
// 游戏逻辑处理函数 (从原始混淆代码中提取的核心逻辑)
|
||||
function processGameLogic(client) {
|
||||
const app = createApp();
|
||||
const state = useState();
|
||||
const { message } = useI18n(["message", "dialog"]);
|
||||
|
||||
// 处理问答逻辑
|
||||
const handleQuestionsLogic = (responseData) => {
|
||||
const questionList = responseData.body.questionList;
|
||||
let hasMatch = false;
|
||||
const config = useState();
|
||||
|
||||
// 遍历问题列表寻找匹配
|
||||
for (let i = 0; i < questionList.length; i++) {
|
||||
const question = questionList[i];
|
||||
//todo
|
||||
// 这里应该有问题匹配逻辑,但在原代码中被混淆了
|
||||
// 原始逻辑涉及某个答案数组 v,可能需要根据实际需求补充
|
||||
}
|
||||
|
||||
return hasMatch;
|
||||
};
|
||||
|
||||
return {
|
||||
handleQuestionsLogic,
|
||||
// 其他游戏逻辑函数可以在这里添加
|
||||
};
|
||||
}
|
||||
|
||||
// Base64 编解码工具 (从原始代码第1部分提取)
|
||||
const base64Utils = {
|
||||
// 字节长度计算
|
||||
byteLength: function (str) {
|
||||
const parsed = this.parseBase64(str);
|
||||
const validLength = parsed[0];
|
||||
const paddingLength = parsed[1];
|
||||
return validLength;
|
||||
},
|
||||
|
||||
// 转换为字节数组
|
||||
toByteArray: function (str) {
|
||||
const parsed = this.parseBase64(str);
|
||||
const validLength = parsed[0];
|
||||
const paddingLength = parsed[1];
|
||||
const result = new Uint8Array(this.calculateLength(validLength, paddingLength, str.length));
|
||||
|
||||
// 解码逻辑
|
||||
// ... 这里应该包含完整的Base64解码实现
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// 从字节数组转换
|
||||
fromByteArray: function (uint8Array) {
|
||||
const length = uint8Array.length;
|
||||
const remainder = length % 3;
|
||||
const chunks = [];
|
||||
const maxChunkLength = 16383;
|
||||
|
||||
// 处理主要部分
|
||||
for (let i = 0; i < length - remainder; i += maxChunkLength) {
|
||||
const end = i + maxChunkLength > length - remainder ? length - remainder : i + maxChunkLength;
|
||||
chunks.push(this.encodeChunk(uint8Array, i, end));
|
||||
}
|
||||
|
||||
// 处理剩余字节
|
||||
if (remainder === 1) {
|
||||
const byte = uint8Array[length - 1];
|
||||
chunks.push(this.chars[byte >> 2] + this.chars[byte << 4 & 63] + '==');
|
||||
} else if (remainder === 2) {
|
||||
const byte1 = uint8Array[length - 2];
|
||||
const byte2 = uint8Array[length - 1];
|
||||
chunks.push(
|
||||
this.chars[byte1 >> 2] +
|
||||
this.chars[byte1 << 4 & 63 | byte2 >> 4] +
|
||||
this.chars[byte2 << 2 & 63] +
|
||||
'='
|
||||
);
|
||||
}
|
||||
|
||||
return chunks.join('');
|
||||
},
|
||||
|
||||
// Base64字符表
|
||||
chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
|
||||
|
||||
// 辅助函数
|
||||
parseBase64: function (str) {
|
||||
const length = str.length;
|
||||
let paddingIndex = str.indexOf('=');
|
||||
if (paddingIndex === -1) paddingIndex = length;
|
||||
|
||||
const validLength = paddingIndex;
|
||||
const paddingLength = paddingIndex === length ? 0 : 4 - (paddingIndex % 4);
|
||||
|
||||
return [validLength, paddingLength];
|
||||
},
|
||||
|
||||
calculateLength: function (validLength, paddingLength, totalLength) {
|
||||
return Math.floor((validLength + paddingLength) * 3 / 4);
|
||||
},
|
||||
|
||||
encodeChunk: function (uint8Array, start, end) {
|
||||
const chars = this.chars;
|
||||
const result = [];
|
||||
|
||||
for (let i = start; i < end; i += 3) {
|
||||
const byte1 = uint8Array[i];
|
||||
const byte2 = i + 1 < end ? uint8Array[i + 1] : 0;
|
||||
const byte3 = i + 2 < end ? uint8Array[i + 2] : 0;
|
||||
|
||||
const triplet = (byte1 << 16) + (byte2 << 8) + byte3;
|
||||
|
||||
result.push(
|
||||
chars[triplet >> 18 & 63] +
|
||||
chars[triplet >> 12 & 63] +
|
||||
chars[triplet >> 6 & 63] +
|
||||
chars[triplet & 63]
|
||||
);
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
}
|
||||
};
|
||||
|
||||
// 数据存储管理 (从文件末尾部分提取)
|
||||
const createDataStore = () => {
|
||||
return {
|
||||
// 响应数据存储
|
||||
resp: {},
|
||||
|
||||
// 更新军团信息
|
||||
updateLegioninfo: function(newData) {
|
||||
const currentLegionData = this.resp.legion_getinforesp;
|
||||
|
||||
if (currentLegionData && currentLegionData.data) {
|
||||
this.resp.legion_getinforesp = {
|
||||
loading: false,
|
||||
data: Object.assign({}, currentLegionData.data, newData),
|
||||
cmd: "legion_getinfor"
|
||||
};
|
||||
} else {
|
||||
this.resp.legion_getinforesp = {
|
||||
loading: false,
|
||||
data: newData,
|
||||
cmd: "legion_getinfor"
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 导出的主要功能模块
|
||||
export {
|
||||
useState as createGameState, // b -> a
|
||||
formatNumber as formatGameNumber, // h -> b
|
||||
gameMessageTemplates as gameCommands, // m -> c
|
||||
processGameLogic as gameLogicHandler, // y -> d
|
||||
createDataStore as dataStoreFactory, // C -> e
|
||||
formatTime, // f
|
||||
base64Utils as encodingUtils, // E -> g
|
||||
createDataStore as storeManager, // S -> h
|
||||
delay as sleep, // g -> s
|
||||
createApp as appFactory // A -> u
|
||||
};
|
||||
438
src/utils/wsAgent.js
Normal file
@@ -0,0 +1,438 @@
|
||||
/**
|
||||
* WebSocket客户端 - 基于mirror代码的完整实现
|
||||
* 支持BON协议编解码、加密通道、心跳保活、消息队列等
|
||||
*/
|
||||
|
||||
import { g_utils } from './bonProtocol.js'
|
||||
|
||||
export class WsAgent {
|
||||
/**
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
const {
|
||||
heartbeatInterval = 2000, // 心跳间隔(ms)
|
||||
queueInterval = 50, // 发送队列轮询间隔(ms)
|
||||
heartbeatCmd = 'heart_beat', // 心跳命令
|
||||
channel = 'x', // 加密通道
|
||||
autoReconnect = true, // 自动重连
|
||||
maxReconnectAttempts = 5, // 最大重连次数
|
||||
reconnectDelay = 3000 // 重连延迟(ms)
|
||||
} = options
|
||||
|
||||
// 配置参数
|
||||
this.heartbeatInterval = heartbeatInterval
|
||||
this.queueInterval = queueInterval
|
||||
this.heartbeatCmd = heartbeatCmd
|
||||
this.channel = channel
|
||||
this.autoReconnect = autoReconnect
|
||||
this.maxReconnectAttempts = maxReconnectAttempts
|
||||
this.reconnectDelay = reconnectDelay
|
||||
|
||||
// 连接状态
|
||||
this.ws = null
|
||||
this.connected = false
|
||||
this.connecting = false
|
||||
this.reconnectAttempts = 0
|
||||
|
||||
// 协议状态
|
||||
this.ack = 0
|
||||
this.seq = 1
|
||||
|
||||
// 定时器
|
||||
this._heartbeatTimer = null
|
||||
this._queueTimer = null
|
||||
this._reconnectTimer = null
|
||||
|
||||
// 发送队列
|
||||
this.sendQueue = []
|
||||
|
||||
// Promise等待队列 respKey -> {resolve, reject, timeoutId}
|
||||
this.waitingPromises = new Map()
|
||||
|
||||
// 事件监听器
|
||||
this.onOpen = () => {}
|
||||
this.onClose = () => {}
|
||||
this.onError = () => {}
|
||||
this.onMessage = () => {}
|
||||
this.onReconnect = () => {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接WebSocket
|
||||
* @param {string} url WebSocket URL
|
||||
* @param {Object} connectionParams 连接参数
|
||||
*/
|
||||
connect(url, connectionParams = {}) {
|
||||
if (this.connecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
||||
console.warn('WebSocket已连接或正在连接中')
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.connecting = true
|
||||
console.log(`🔗 连接WebSocket: ${url}`)
|
||||
|
||||
this.ws = new WebSocket(url)
|
||||
this.ws.binaryType = 'arraybuffer'
|
||||
|
||||
// 连接打开
|
||||
this.ws.onopen = () => {
|
||||
this.connecting = false
|
||||
this.connected = true
|
||||
this.reconnectAttempts = 0
|
||||
|
||||
console.log('✅ WebSocket连接已建立')
|
||||
|
||||
// 重置协议状态
|
||||
this.seq = 1
|
||||
|
||||
// 启动心跳和队列处理
|
||||
this._startHeartbeat()
|
||||
this._startQueueProcessor()
|
||||
|
||||
this.onOpen()
|
||||
resolve()
|
||||
}
|
||||
|
||||
// 消息接收
|
||||
this.ws.onmessage = (event) => {
|
||||
this._handleMessage(event.data)
|
||||
}
|
||||
|
||||
// 连接关闭
|
||||
this.ws.onclose = (event) => {
|
||||
this.connecting = false
|
||||
this.connected = false
|
||||
this._cleanup()
|
||||
|
||||
console.log(`🔌 WebSocket连接已关闭: ${event.code} ${event.reason}`)
|
||||
|
||||
this.onClose(event)
|
||||
|
||||
// 自动重连
|
||||
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this._scheduleReconnect(url, connectionParams)
|
||||
}
|
||||
}
|
||||
|
||||
// 连接错误
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('❌ WebSocket错误:', error)
|
||||
this.onError(error)
|
||||
|
||||
if (this.connecting) {
|
||||
this.connecting = false
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.connecting = false
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
* @param {number} code 关闭码
|
||||
* @param {string} reason 关闭原因
|
||||
*/
|
||||
close(code = 1000, reason = 'normal') {
|
||||
this.autoReconnect = false
|
||||
if (this.ws) {
|
||||
this.ws.close(code, reason)
|
||||
}
|
||||
this._cleanup()
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* @param {Object|Array} payload 消息载荷
|
||||
*/
|
||||
send(payload) {
|
||||
if (Array.isArray(payload)) {
|
||||
this.sendQueue.push(...payload)
|
||||
} else {
|
||||
this.sendQueue.push(payload)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息并等待响应
|
||||
* @param {Object} options 请求选项
|
||||
* @returns {Promise} 响应Promise
|
||||
*/
|
||||
sendWithPromise(options) {
|
||||
const { cmd, body = {}, respKey, timeout = 8000 } = options
|
||||
const responseKey = respKey || `${cmd}resp`
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 设置超时
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.waitingPromises.delete(responseKey)
|
||||
reject(new Error(`请求超时: ${cmd}`))
|
||||
}, timeout)
|
||||
|
||||
// 注册Promise
|
||||
this.waitingPromises.set(responseKey, {
|
||||
resolve,
|
||||
reject,
|
||||
timeoutId
|
||||
})
|
||||
|
||||
// 发送消息
|
||||
this.send({ cmd, body, respKey: responseKey })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
* @private
|
||||
*/
|
||||
_handleMessage(data) {
|
||||
try {
|
||||
// 使用g_utils解密和解码消息
|
||||
const message = g_utils.parse(data, this.channel)
|
||||
|
||||
if (!message) {
|
||||
console.warn('消息解析失败')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('📨 收到消息:', message)
|
||||
|
||||
// 更新ack
|
||||
if (message.seq) {
|
||||
this.ack = message.seq
|
||||
}
|
||||
|
||||
// 检查是否有等待的Promise
|
||||
const cmd = message.cmd || message.c
|
||||
const respKey = message.respKey || cmd
|
||||
|
||||
if (respKey && this.waitingPromises.has(respKey)) {
|
||||
const { resolve, timeoutId } = this.waitingPromises.get(respKey)
|
||||
clearTimeout(timeoutId)
|
||||
this.waitingPromises.delete(respKey)
|
||||
resolve(message)
|
||||
return
|
||||
}
|
||||
|
||||
// 派发给普通消息处理器
|
||||
this.onMessage(message)
|
||||
|
||||
} catch (error) {
|
||||
console.error('消息处理失败:', error)
|
||||
this.onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳
|
||||
* @private
|
||||
*/
|
||||
_startHeartbeat() {
|
||||
this._stopHeartbeat()
|
||||
|
||||
if (!this.heartbeatInterval) return
|
||||
|
||||
this._heartbeatTimer = setInterval(() => {
|
||||
if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
|
||||
this._sendHeartbeat()
|
||||
}
|
||||
}, this.heartbeatInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳
|
||||
* @private
|
||||
*/
|
||||
_stopHeartbeat() {
|
||||
if (this._heartbeatTimer) {
|
||||
clearInterval(this._heartbeatTimer)
|
||||
this._heartbeatTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳消息
|
||||
* @private
|
||||
*/
|
||||
_sendHeartbeat() {
|
||||
const heartbeatMsg = {
|
||||
ack: this.ack,
|
||||
body: undefined,
|
||||
c: undefined,
|
||||
cmd: '_sys/ack',
|
||||
hint: undefined,
|
||||
seq: 0, // 心跳消息seq为0
|
||||
time: Date.now()
|
||||
}
|
||||
|
||||
this._rawSend(heartbeatMsg)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动队列处理器
|
||||
* @private
|
||||
*/
|
||||
_startQueueProcessor() {
|
||||
this._stopQueueProcessor()
|
||||
this._queueTimer = setInterval(() => {
|
||||
this._processQueue()
|
||||
}, this.queueInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止队列处理器
|
||||
* @private
|
||||
*/
|
||||
_stopQueueProcessor() {
|
||||
if (this._queueTimer) {
|
||||
clearInterval(this._queueTimer)
|
||||
this._queueTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理发送队列
|
||||
* @private
|
||||
*/
|
||||
_processQueue() {
|
||||
if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.sendQueue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const item = this.sendQueue.shift()
|
||||
const packet = this._buildPacket(item)
|
||||
this._rawSend(packet)
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建数据包
|
||||
* @private
|
||||
*/
|
||||
_buildPacket(payload) {
|
||||
const { cmd, body = {}, respKey } = payload
|
||||
|
||||
// 生成随机RTT (0-500ms)
|
||||
const rtt = Math.floor(Math.random() * 500)
|
||||
|
||||
const packet = {
|
||||
ack: this.ack,
|
||||
seq: cmd === this.heartbeatCmd ? 0 : this.seq++,
|
||||
time: Date.now(),
|
||||
cmd,
|
||||
body,
|
||||
respKey,
|
||||
rtt,
|
||||
code: 0
|
||||
}
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
/**
|
||||
* 原始发送数据
|
||||
* @private
|
||||
*/
|
||||
_rawSend(packet) {
|
||||
try {
|
||||
// 使用g_utils编码和加密
|
||||
const data = g_utils.encode(packet, this.channel)
|
||||
this.ws.send(data)
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
this.onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计划重连
|
||||
* @private
|
||||
*/
|
||||
_scheduleReconnect(url, connectionParams) {
|
||||
if (this._reconnectTimer) {
|
||||
clearTimeout(this._reconnectTimer)
|
||||
}
|
||||
|
||||
this.reconnectAttempts++
|
||||
console.log(`🔄 计划重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts}) 延迟: ${this.reconnectDelay}ms`)
|
||||
|
||||
this._reconnectTimer = setTimeout(() => {
|
||||
console.log(`🔄 开始第${this.reconnectAttempts}次重连...`)
|
||||
this.onReconnect(this.reconnectAttempts)
|
||||
this.connect(url, connectionParams).catch(error => {
|
||||
console.error('重连失败:', error)
|
||||
})
|
||||
}, this.reconnectDelay)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
* @private
|
||||
*/
|
||||
_cleanup() {
|
||||
this._stopHeartbeat()
|
||||
this._stopQueueProcessor()
|
||||
|
||||
if (this._reconnectTimer) {
|
||||
clearTimeout(this._reconnectTimer)
|
||||
this._reconnectTimer = null
|
||||
}
|
||||
|
||||
// 清理等待的Promise
|
||||
for (const [key, { reject, timeoutId }] of this.waitingPromises) {
|
||||
clearTimeout(timeoutId)
|
||||
reject(new Error('连接已关闭'))
|
||||
}
|
||||
this.waitingPromises.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
connected: this.connected,
|
||||
connecting: this.connecting,
|
||||
readyState: this.ws?.readyState,
|
||||
ack: this.ack,
|
||||
seq: this.seq,
|
||||
queueLength: this.sendQueue.length,
|
||||
waitingPromises: this.waitingPromises.size,
|
||||
reconnectAttempts: this.reconnectAttempts
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建WebSocket URL
|
||||
* @static
|
||||
*/
|
||||
static buildUrl(baseUrl, params = {}) {
|
||||
const url = new URL(baseUrl)
|
||||
|
||||
// 添加连接参数到p参数
|
||||
if (params.p && typeof params.p === 'object') {
|
||||
url.searchParams.set('p', JSON.stringify(params.p))
|
||||
}
|
||||
|
||||
// 添加其他参数
|
||||
Object.keys(params).forEach(key => {
|
||||
if (key !== 'p' && params[key] !== undefined) {
|
||||
url.searchParams.set(key, params[key])
|
||||
}
|
||||
})
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export default WsAgent
|
||||
537
src/utils/xyzwWebSocket.js
Normal file
@@ -0,0 +1,537 @@
|
||||
/**
|
||||
* XYZW WebSocket 客户端
|
||||
* 基于 readable-xyzw-ws.js 重构,适配本项目架构
|
||||
*/
|
||||
|
||||
import { bonProtocol, g_utils } from './bonProtocol.js'
|
||||
|
||||
/** 生成 [min,max] 的随机整数 */
|
||||
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
|
||||
|
||||
/** Promise 版 sleep */
|
||||
const sleep = (ms) => new Promise((res) => setTimeout(res, ms))
|
||||
|
||||
/**
|
||||
* 命令注册器:保存每个 cmd 的默认体,发送时与 params 合并
|
||||
*/
|
||||
export class CommandRegistry {
|
||||
constructor(encoder, enc) {
|
||||
this.encoder = encoder
|
||||
this.enc = enc
|
||||
this.commands = new Map()
|
||||
}
|
||||
|
||||
/** 注册命令 */
|
||||
register(cmd, defaultBody = {}) {
|
||||
this.commands.set(cmd, (ack = 0, seq = 0, params = {}) => ({
|
||||
cmd,
|
||||
ack,
|
||||
seq,
|
||||
code: 0,
|
||||
rtt: randInt(0, 500),
|
||||
time: Date.now(),
|
||||
body: this.encoder?.bon?.encode
|
||||
? this.encoder.bon.encode({ ...defaultBody, ...params })
|
||||
: undefined,
|
||||
c: undefined,
|
||||
hint: undefined,
|
||||
}))
|
||||
return this
|
||||
}
|
||||
|
||||
/** 特例:系统心跳的 ack 用的是 "_sys/ack" */
|
||||
registerHeartbeat() {
|
||||
this.commands.set("heart_beat", (ack, seq) => ({
|
||||
cmd: "_sys/ack",
|
||||
ack,
|
||||
seq,
|
||||
time: Date.now(),
|
||||
body: undefined,
|
||||
c: undefined,
|
||||
hint: undefined,
|
||||
}))
|
||||
return this
|
||||
}
|
||||
|
||||
/** 生成最终可发送的二进制 */
|
||||
encodePacket(raw) {
|
||||
if (this.encoder?.encode && this.enc) {
|
||||
// 使用加密编码
|
||||
return this.encoder.encode(raw, this.enc)
|
||||
} else {
|
||||
// 降级到JSON字符串
|
||||
return JSON.stringify(raw)
|
||||
}
|
||||
}
|
||||
|
||||
/** 构造报文 */
|
||||
build(cmd, ack, seq, params) {
|
||||
const fn = this.commands.get(cmd)
|
||||
if (!fn) throw new Error(`Unknown cmd: ${cmd}`)
|
||||
return fn(ack, seq, params)
|
||||
}
|
||||
}
|
||||
|
||||
/** 预注册游戏命令 */
|
||||
export function registerDefaultCommands(reg) {
|
||||
return reg.registerHeartbeat()
|
||||
// 角色/系统
|
||||
.register("role_getroleinfo", {
|
||||
clientVersion: "1.65.3-wx",
|
||||
inviteUid: 0,
|
||||
platform: "hortor",
|
||||
platformExt: "mix",
|
||||
scene: "",
|
||||
})
|
||||
.register("system_getdatabundlever", { isAudit: false })
|
||||
.register("system_buygold", { buyNum: 1 })
|
||||
.register("system_claimhangupreward")
|
||||
.register("system_signinreward")
|
||||
|
||||
// 任务相关
|
||||
.register("task_claimdailypoint", { taskId: 1 })
|
||||
.register("task_claimdailyreward", { rewardId: 0 })
|
||||
.register("task_claimweekreward", { rewardId: 0 })
|
||||
|
||||
// 好友/招募
|
||||
.register("friend_batch", { friendId: 0 })
|
||||
.register("hero_recruit", { byClub: false, recruitNumber: 1, recruitType: 3 })
|
||||
.register("item_openbox", { itemId: 2001, number: 10 })
|
||||
|
||||
// 竞技场
|
||||
.register("arena_startarea")
|
||||
.register("arena_getareatarget", { refresh: false })
|
||||
.register("fight_startareaarena", { targetId: 530479307 })
|
||||
.register("arena_getarearank", { rankType: 0 })
|
||||
|
||||
// 商店
|
||||
.register("store_goodslist", { storeId: 1 })
|
||||
.register("store_buy", { goodsId: 1 })
|
||||
.register("store_refresh", { storeId: 1 })
|
||||
|
||||
// 军团
|
||||
.register("legion_getinfo")
|
||||
.register("legion_signin")
|
||||
.register("legion_getwarrank")
|
||||
|
||||
// 邮件
|
||||
.register("mail_getlist", { category: [0, 4, 5], lastId: 0, size: 60 })
|
||||
.register("mail_claimallattachment", { category: 0 })
|
||||
|
||||
// 学习问答
|
||||
.register("study_startgame")
|
||||
.register("study_answer")
|
||||
.register("study_claimreward", { rewardId: 1 })
|
||||
|
||||
// 战斗相关
|
||||
.register("fight_starttower")
|
||||
.register("fight_startboss")
|
||||
.register("fight_startlegionboss")
|
||||
.register("fight_startdungeon")
|
||||
.register("fight_startpvp")
|
||||
|
||||
// 瓶子机器人
|
||||
.register("bottlehelper_claim")
|
||||
.register("bottlehelper_start", { bottleType: -1 })
|
||||
.register("bottlehelper_stop", { bottleType: -1 })
|
||||
|
||||
// 军团匹配和签到
|
||||
.register("legionmatch_rolesignup")
|
||||
.register("legion_signin")
|
||||
|
||||
// 神器抽奖
|
||||
.register("artifact_lottery", { lotteryNumber: 1, newFree: true, type: 1 })
|
||||
|
||||
// 爬塔相关
|
||||
.register("tower_getinfo")
|
||||
.register("tower_claimreward")
|
||||
|
||||
// 队伍相关
|
||||
.register("presetteam_getteam")
|
||||
.register("presetteam_setteam")
|
||||
.register("presetteam_saveteam")
|
||||
.register("role_gettargetteam")
|
||||
|
||||
// 排名相关
|
||||
.register("rank_getroleinfo")
|
||||
|
||||
// 梦魇相关
|
||||
.register("nightmare_getroleinfo")
|
||||
}
|
||||
|
||||
/**
|
||||
* XYZW WebSocket 客户端
|
||||
*/
|
||||
export class XyzwWebSocketClient {
|
||||
constructor({ url, utils, heartbeatMs = 5000 }) {
|
||||
this.url = url
|
||||
this.utils = utils || g_utils
|
||||
this.enc = this.utils?.getEnc ? this.utils.getEnc("auto") : undefined
|
||||
|
||||
this.socket = null
|
||||
this.ack = 1
|
||||
this.seq = 0
|
||||
this.sendQueue = []
|
||||
this.sendQueueTimer = null
|
||||
this.heartbeatTimer = null
|
||||
this.heartbeatInterval = heartbeatMs
|
||||
|
||||
this.dialogStatus = false
|
||||
this.messageListener = null
|
||||
this.showMsg = false
|
||||
this.connected = false
|
||||
|
||||
this.promises = Object.create(null)
|
||||
this.registry = registerDefaultCommands(new CommandRegistry(this.utils, this.enc))
|
||||
|
||||
console.log('🔧 WebSocket客户端初始化:', {
|
||||
url: this.url,
|
||||
hasUtils: !!this.utils,
|
||||
hasEnc: !!this.enc,
|
||||
hasEncoder: !!this.utils?.encode
|
||||
})
|
||||
|
||||
// 状态回调
|
||||
this.onConnect = null
|
||||
this.onDisconnect = null
|
||||
this.onError = null
|
||||
}
|
||||
|
||||
/** 初始化连接 */
|
||||
init() {
|
||||
console.log(`🔗 连接 WebSocket: ${this.url}`)
|
||||
|
||||
this.socket = new WebSocket(this.url)
|
||||
|
||||
this.socket.onopen = () => {
|
||||
console.log(`✅ WebSocket 连接成功`)
|
||||
this.connected = true
|
||||
console.log(`🔄 启动心跳机制,间隔: ${this.heartbeatInterval}ms`)
|
||||
this._setupHeartbeat()
|
||||
console.log(`🔄 启动消息队列处理`)
|
||||
this._processQueueLoop()
|
||||
if (this.onConnect) this.onConnect()
|
||||
}
|
||||
|
||||
this.socket.onmessage = (evt) => {
|
||||
try {
|
||||
let packet
|
||||
if (typeof evt.data === "string") {
|
||||
packet = JSON.parse(evt.data)
|
||||
} else if (evt.data instanceof ArrayBuffer) {
|
||||
// 二进制数据需要自动检测并解码
|
||||
packet = this.utils?.parse ? this.utils.parse(evt.data, "auto") : evt.data
|
||||
} else if (evt.data instanceof Blob) {
|
||||
// 处理Blob数据
|
||||
console.log('📦 收到Blob数据, 大小:', evt.data.size)
|
||||
evt.data.arrayBuffer().then(buffer => {
|
||||
try {
|
||||
packet = this.utils?.parse ? this.utils.parse(buffer, "auto") : buffer
|
||||
console.log('📦 Blob解析结果:', packet)
|
||||
|
||||
// 处理消息体解码(ProtoMsg会自动解码)
|
||||
if (packet instanceof Object && packet.rawData !== undefined) {
|
||||
console.log('✅ ProtoMsg消息,使用rawData:', packet.rawData)
|
||||
} else if (packet.body && packet.body instanceof Uint8Array) {
|
||||
try {
|
||||
if (this.utils && this.utils.bon && this.utils.bon.decode) {
|
||||
const decodedBody = this.utils.bon.decode(packet.body)
|
||||
console.log('✅ 手动解码消息体成功:', decodedBody)
|
||||
// 不修改packet.body,而是创建一个新的属性存储解码后的数据
|
||||
packet.decodedBody = decodedBody
|
||||
} else {
|
||||
console.warn('⚠️ BON解码器不可用:', this.utils)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('❌ 消息体解码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.showMsg) {
|
||||
console.log(`📨 收到消息(Blob解析后):`, packet)
|
||||
}
|
||||
|
||||
// 回调处理
|
||||
if (this.messageListener) {
|
||||
this.messageListener(packet)
|
||||
}
|
||||
|
||||
// Promise 响应处理
|
||||
this._handlePromiseResponse(packet)
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Blob解析失败:', error)
|
||||
}
|
||||
})
|
||||
return // 异步处理,直接返回
|
||||
} else {
|
||||
console.warn('⚠️ 未知数据类型:', typeof evt.data, evt.data)
|
||||
packet = evt.data
|
||||
}
|
||||
|
||||
if (this.showMsg) {
|
||||
console.log(`📨 收到消息:`, packet)
|
||||
}
|
||||
|
||||
// 处理消息体解码(ProtoMsg会自动解码)
|
||||
if (packet instanceof Object && packet.rawData !== undefined) {
|
||||
console.log('✅ ProtoMsg消息,使用rawData:', packet.rawData)
|
||||
} else if (packet.body && packet.body instanceof Uint8Array) {
|
||||
try {
|
||||
if (this.utils && this.utils.bon && this.utils.bon.decode) {
|
||||
const decodedBody = this.utils.bon.decode(packet.body)
|
||||
console.log('✅ 手动解码消息体成功:', decodedBody)
|
||||
// 不修改packet.body,而是创建一个新的属性存储解码后的数据
|
||||
packet.decodedBody = decodedBody
|
||||
} else {
|
||||
console.warn('⚠️ BON解码器不可用:', this.utils)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('❌ 消息体解码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 回调处理
|
||||
if (this.messageListener) {
|
||||
this.messageListener(packet)
|
||||
}
|
||||
|
||||
// Promise 响应处理
|
||||
this._handlePromiseResponse(packet)
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 消息处理失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
this.socket.onclose = (evt) => {
|
||||
console.log(`🔌 WebSocket 连接关闭:`, evt.code, evt.reason)
|
||||
console.log(`🔍 关闭详情:`, {
|
||||
code: evt.code,
|
||||
reason: evt.reason || '未提供原因',
|
||||
wasClean: evt.wasClean,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
this.connected = false
|
||||
this._clearTimers()
|
||||
if (this.onDisconnect) this.onDisconnect(evt)
|
||||
}
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
console.error(`❌ WebSocket 错误:`, error)
|
||||
this.connected = false
|
||||
this._clearTimers()
|
||||
if (this.onError) this.onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 注册消息回调 */
|
||||
setMessageListener(fn) {
|
||||
this.messageListener = fn
|
||||
}
|
||||
|
||||
/** 控制台消息开关 */
|
||||
setShowMsg(val) {
|
||||
this.showMsg = !!val
|
||||
}
|
||||
|
||||
/** 重连 */
|
||||
reconnect() {
|
||||
this.disconnect()
|
||||
setTimeout(() => this.init(), 1000)
|
||||
}
|
||||
|
||||
/** 断开连接 */
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close()
|
||||
this.socket = null
|
||||
}
|
||||
this.connected = false
|
||||
this._clearTimers()
|
||||
}
|
||||
|
||||
/** 发送消息 */
|
||||
send(cmd, params = {}, options = {}) {
|
||||
if (!this.connected) {
|
||||
console.warn(`⚠️ WebSocket 未连接,消息已入队: ${cmd}`)
|
||||
if (!this.dialogStatus) {
|
||||
this.dialogStatus = true
|
||||
this.reconnect()
|
||||
setTimeout(() => { this.dialogStatus = false }, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const task = {
|
||||
cmd,
|
||||
params,
|
||||
respKey: options.respKey || cmd,
|
||||
sleep: options.sleep || 0,
|
||||
onSent: options.onSent
|
||||
}
|
||||
|
||||
this.sendQueue.push(task)
|
||||
return task
|
||||
}
|
||||
|
||||
/** Promise 版发送 */
|
||||
sendWithPromise(cmd, params = {}, timeoutMs = 5000) {
|
||||
const respKey = `${cmd}_${this.seq + 1}`
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.connected && !this.socket) {
|
||||
return reject(new Error("WebSocket 连接已关闭"))
|
||||
}
|
||||
|
||||
// 设置 Promise 状态
|
||||
this.promises[respKey] = { resolve, reject }
|
||||
|
||||
// 超时处理
|
||||
const timer = setTimeout(() => {
|
||||
delete this.promises[respKey]
|
||||
reject(new Error(`请求超时: ${cmd} (${timeoutMs}ms)`))
|
||||
}, timeoutMs)
|
||||
|
||||
// 发送消息
|
||||
this.send(cmd, params, {
|
||||
respKey,
|
||||
onSent: () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 发送心跳 */
|
||||
sendHeartbeat() {
|
||||
console.log('💓 发送心跳消息')
|
||||
this.send("heart_beat", {}, { respKey: "_sys/ack" })
|
||||
}
|
||||
|
||||
/** 获取角色信息 */
|
||||
getRoleInfo(params = {}) {
|
||||
return this.sendWithPromise("role_getroleinfo", params)
|
||||
}
|
||||
|
||||
/** 获取数据版本 */
|
||||
getDataBundleVersion(params = {}) {
|
||||
return this.sendWithPromise("system_getdatabundlever", params)
|
||||
}
|
||||
|
||||
/** 签到 */
|
||||
signIn() {
|
||||
return this.sendWithPromise("system_signinreward")
|
||||
}
|
||||
|
||||
/** 领取日常任务奖励 */
|
||||
claimDailyReward(rewardId = 0) {
|
||||
return this.sendWithPromise("task_claimdailyreward", { rewardId })
|
||||
}
|
||||
|
||||
/** =============== 内部方法 =============== */
|
||||
|
||||
/** 设置心跳 */
|
||||
_setupHeartbeat() {
|
||||
// 延迟3秒后开始发送第一个心跳,避免连接刚建立就发送
|
||||
setTimeout(() => {
|
||||
if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
|
||||
console.log('💓 开始发送首次心跳')
|
||||
this.sendHeartbeat()
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
// 设置定期心跳
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
|
||||
this.sendHeartbeat()
|
||||
} else {
|
||||
console.log('⚠️ 心跳检查失败: 连接状态异常')
|
||||
}
|
||||
}, this.heartbeatInterval)
|
||||
}
|
||||
|
||||
/** 队列处理循环 */
|
||||
_processQueueLoop() {
|
||||
if (this.sendQueueTimer) clearInterval(this.sendQueueTimer)
|
||||
|
||||
this.sendQueueTimer = setInterval(async () => {
|
||||
if (!this.sendQueue.length) return
|
||||
if (!this.connected || this.socket?.readyState !== WebSocket.OPEN) return
|
||||
|
||||
const task = this.sendQueue.shift()
|
||||
if (!task) return
|
||||
|
||||
try {
|
||||
// 构建报文
|
||||
const raw = this.registry.build(task.cmd, this.ack, this.seq, task.params)
|
||||
if (task.cmd !== "heart_beat") this.seq++
|
||||
|
||||
// 编码并发送
|
||||
const bin = this.registry.encodePacket(raw)
|
||||
this.socket?.send(bin)
|
||||
|
||||
if (this.showMsg || task.cmd === "heart_beat") {
|
||||
console.log(`📤 发送消息: ${task.cmd}`, task.params)
|
||||
if (this.showMsg) {
|
||||
console.log(`🔐 原始数据:`, raw)
|
||||
console.log(`🚀 编码后数据:`, bin)
|
||||
console.log(`🔧 编码类型:`, typeof bin, bin instanceof Uint8Array ? '✅ Uint8Array (加密)' : '❌ String (明文)')
|
||||
if (bin instanceof Uint8Array && bin.length > 0) {
|
||||
console.log(`🎯 加密验证: 前8字节 [${Array.from(bin.slice(0, 8)).join(', ')}]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发发送回调
|
||||
if (task.onSent) {
|
||||
try {
|
||||
task.onSent(task.respKey, task.cmd)
|
||||
} catch (error) {
|
||||
console.warn('发送回调执行失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 可选延时
|
||||
if (task.sleep) await sleep(task.sleep)
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 发送消息失败: ${task.cmd}`, error)
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
/** 处理 Promise 响应 */
|
||||
_handlePromiseResponse(packet) {
|
||||
const cmd = packet.cmd
|
||||
if (!cmd) return
|
||||
|
||||
// 查找对应的 Promise
|
||||
for (const [key, promise] of Object.entries(this.promises)) {
|
||||
if (key.startsWith(cmd) || cmd === key) {
|
||||
delete this.promises[key]
|
||||
|
||||
if (packet.code === 0 || packet.code === undefined) {
|
||||
promise.resolve(packet.body || packet)
|
||||
} else {
|
||||
promise.reject(new Error(`服务器错误: ${packet.code} - ${packet.hint || '未知错误'}`))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 清理定时器 */
|
||||
_clearTimers() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
if (this.sendQueueTimer) {
|
||||
clearInterval(this.sendQueueTimer)
|
||||
this.sendQueueTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 默认导出 */
|
||||
export default XyzwWebSocketClient
|
||||
759
src/views/DailyTasks.vue
Normal file
@@ -0,0 +1,759 @@
|
||||
<template>
|
||||
<div class="daily-tasks-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<h1 class="page-title">
|
||||
日常任务
|
||||
</h1>
|
||||
<p class="page-subtitle">
|
||||
管理和执行您的日常游戏任务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="isRefreshing"
|
||||
@click="refreshTasks"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<Refresh />
|
||||
</n-icon>
|
||||
</template>
|
||||
刷新任务
|
||||
</n-button>
|
||||
|
||||
<n-dropdown
|
||||
:options="bulkActionOptions"
|
||||
@select="handleBulkAction"
|
||||
>
|
||||
<n-button size="large">
|
||||
批量操作
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ChevronDown />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色选择器 -->
|
||||
<div class="role-selector-section">
|
||||
<div class="container">
|
||||
<div class="role-selector">
|
||||
<span class="selector-label">选择角色:</span>
|
||||
<n-select
|
||||
v-model:value="selectedRoleId"
|
||||
:options="roleOptions"
|
||||
placeholder="请选择游戏角色"
|
||||
style="min-width: 200px"
|
||||
@update:value="onRoleChange"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="selectedRole"
|
||||
class="role-stats"
|
||||
>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总任务:</span>
|
||||
<span class="stat-value">{{ taskStats.total }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">已完成:</span>
|
||||
<span class="stat-value">{{ taskStats.completed }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">进度:</span>
|
||||
<span class="stat-value">{{ taskStats.percentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务筛选 -->
|
||||
<div class="filter-section">
|
||||
<div class="container">
|
||||
<div class="filter-bar">
|
||||
<n-radio-group
|
||||
v-model:value="currentFilter"
|
||||
@update:value="onFilterChange"
|
||||
>
|
||||
<n-radio-button value="all">
|
||||
全部任务
|
||||
</n-radio-button>
|
||||
<n-radio-button value="pending">
|
||||
待完成
|
||||
</n-radio-button>
|
||||
<n-radio-button value="completed">
|
||||
已完成
|
||||
</n-radio-button>
|
||||
<n-radio-button value="auto">
|
||||
自动执行
|
||||
</n-radio-button>
|
||||
</n-radio-group>
|
||||
|
||||
<div class="search-box">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索任务..."
|
||||
clearable
|
||||
@update:value="onSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<Search />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="tasks-section">
|
||||
<div class="container">
|
||||
<div
|
||||
v-if="filteredTasks.length"
|
||||
class="tasks-grid"
|
||||
>
|
||||
<DailyTaskCard
|
||||
v-for="task in filteredTasks"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
@execute="executeTask"
|
||||
@toggle-status="toggleTaskStatus"
|
||||
@update:task="updateTask"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-else-if="!isLoading"
|
||||
class="empty-state"
|
||||
>
|
||||
<n-empty
|
||||
description="暂无任务数据"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<Cube />
|
||||
</n-icon>
|
||||
</template>
|
||||
<template #extra>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="refreshTasks"
|
||||
>
|
||||
刷新任务
|
||||
</n-button>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="loading-state"
|
||||
>
|
||||
<n-spin size="large">
|
||||
<template #description>
|
||||
正在加载任务数据...
|
||||
</template>
|
||||
</n-spin>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage, useDialog } from 'naive-ui'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import DailyTaskCard from '@/components/DailyTaskCard.vue'
|
||||
import {
|
||||
Refresh,
|
||||
ChevronDown,
|
||||
Search,
|
||||
Cube
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 响应式数据
|
||||
const isLoading = ref(false)
|
||||
const isRefreshing = ref(false)
|
||||
const selectedRoleId = ref(null)
|
||||
const currentFilter = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
const tasks = ref([])
|
||||
|
||||
// 计算属性
|
||||
const selectedRole = computed(() => {
|
||||
return gameRolesStore.gameRoles.find(role => role.id === selectedRoleId.value)
|
||||
})
|
||||
|
||||
const roleOptions = computed(() => {
|
||||
return gameRolesStore.gameRoles.map(role => ({
|
||||
label: `${role.name} (${role.server})`,
|
||||
value: role.id
|
||||
}))
|
||||
})
|
||||
|
||||
const taskStats = computed(() => {
|
||||
const total = tasks.value.length
|
||||
const completed = tasks.value.filter(task => task.completed).length
|
||||
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
|
||||
return { total, completed, percentage }
|
||||
})
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
let filtered = tasks.value
|
||||
|
||||
// 状态筛选
|
||||
switch (currentFilter.value) {
|
||||
case 'pending':
|
||||
filtered = filtered.filter(task => !task.completed)
|
||||
break
|
||||
case 'completed':
|
||||
filtered = filtered.filter(task => task.completed)
|
||||
break
|
||||
case 'auto':
|
||||
filtered = filtered.filter(task => task.settings?.autoExecute)
|
||||
break
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
filtered = filtered.filter(task =>
|
||||
task.title.toLowerCase().includes(keyword) ||
|
||||
task.subtitle?.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
const bulkActionOptions = [
|
||||
{
|
||||
label: '执行所有待完成任务',
|
||||
key: 'execute-all-pending'
|
||||
},
|
||||
{
|
||||
label: '标记所有为已完成',
|
||||
key: 'mark-all-completed'
|
||||
},
|
||||
{
|
||||
label: '重置所有任务状态',
|
||||
key: 'reset-all-tasks'
|
||||
}
|
||||
]
|
||||
|
||||
// 方法
|
||||
const refreshTasks = async () => {
|
||||
if (!selectedRoleId.value) {
|
||||
message.warning('请先选择游戏角色')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isRefreshing.value = true
|
||||
isLoading.value = true
|
||||
|
||||
// 使用本地模拟任务数据
|
||||
const mockTasks = generateMockTasks(selectedRoleId.value)
|
||||
tasks.value = mockTasks
|
||||
|
||||
// 缓存到本地存储
|
||||
localStorage.setItem(`dailyTasks_${selectedRoleId.value}`, JSON.stringify(mockTasks))
|
||||
|
||||
message.success('任务数据刷新成功')
|
||||
} catch (error) {
|
||||
console.error('刷新任务失败:', error)
|
||||
message.error('本地数据生成失败')
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成模拟任务数据
|
||||
const generateMockTasks = (roleId) => {
|
||||
const role = gameRolesStore.gameRoles.find(r => r.id === roleId)
|
||||
const roleName = role?.name || '未知角色'
|
||||
|
||||
return [
|
||||
{
|
||||
id: `task_${roleId}_daily_signin`,
|
||||
title: '每日签到',
|
||||
subtitle: '登录游戏获取签到奖励',
|
||||
icon: '/icons/ta.png',
|
||||
completed: false,
|
||||
canExecute: true,
|
||||
progress: { current: 0, total: 1 },
|
||||
reward: '金币 x100, 经验 x50',
|
||||
nextReset: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
settings: { autoExecute: false, delay: 0, notification: true },
|
||||
details: [
|
||||
{ id: 1, name: '打开游戏客户端', completed: false },
|
||||
{ id: 2, name: '点击签到按钮', completed: false }
|
||||
],
|
||||
logs: []
|
||||
},
|
||||
{
|
||||
id: `task_${roleId}_daily_quest`,
|
||||
title: '完成日常任务',
|
||||
subtitle: '完成5个日常任务获得奖励',
|
||||
icon: '/icons/ta.png',
|
||||
completed: false,
|
||||
canExecute: true,
|
||||
progress: { current: 2, total: 5 },
|
||||
reward: '金币 x500, 装备碎片 x10',
|
||||
nextReset: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
settings: { autoExecute: true, delay: 5, notification: true },
|
||||
details: [
|
||||
{ id: 1, name: '击败10只怪物', completed: true },
|
||||
{ id: 2, name: '收集20个材料', completed: true },
|
||||
{ id: 3, name: '完成一次副本', completed: false },
|
||||
{ id: 4, name: '参与公会活动', completed: false },
|
||||
{ id: 5, name: '强化装备', completed: false }
|
||||
],
|
||||
logs: [
|
||||
{ id: 1, timestamp: Date.now() - 30 * 60 * 1000, type: 'success', message: '已完成击败怪物任务' },
|
||||
{ id: 2, timestamp: Date.now() - 60 * 60 * 1000, type: 'success', message: '已完成材料收集任务' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: `task_${roleId}_guild_contribution`,
|
||||
title: '公会贡献',
|
||||
subtitle: '为公会贡献资源获得贡献点',
|
||||
icon: '/icons/ta.png',
|
||||
completed: true,
|
||||
canExecute: false,
|
||||
progress: { current: 1, total: 1 },
|
||||
reward: '公会贡献点 x100',
|
||||
nextReset: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
settings: { autoExecute: true, delay: 0, notification: true },
|
||||
details: [
|
||||
{ id: 1, name: '捐献金币', completed: true }
|
||||
],
|
||||
logs: [
|
||||
{ id: 1, timestamp: Date.now() - 2 * 60 * 60 * 1000, type: 'success', message: '已完成公会贡献' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const onRoleChange = (roleId) => {
|
||||
selectedRoleId.value = roleId
|
||||
gameRolesStore.selectRole(
|
||||
gameRolesStore.gameRoles.find(role => role.id === roleId)
|
||||
)
|
||||
|
||||
if (roleId) {
|
||||
refreshTasks()
|
||||
}
|
||||
}
|
||||
|
||||
const onFilterChange = (filter) => {
|
||||
currentFilter.value = filter
|
||||
}
|
||||
|
||||
const onSearch = (keyword) => {
|
||||
searchKeyword.value = keyword
|
||||
}
|
||||
|
||||
const executeTask = async (taskId) => {
|
||||
if (!selectedRoleId.value) {
|
||||
message.error('请先选择游戏角色')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查WebSocket连接状态
|
||||
const wsStatus = localTokenStore.getWebSocketStatus(selectedRoleId.value)
|
||||
if (wsStatus !== 'connected') {
|
||||
// 尝试建立连接
|
||||
const tokenData = localTokenStore.getGameToken(selectedRoleId.value)
|
||||
if (tokenData) {
|
||||
localTokenStore.createWebSocketConnection(
|
||||
selectedRoleId.value,
|
||||
tokenData.token,
|
||||
tokenData.wsUrl
|
||||
)
|
||||
// 等待一秒让连接建立
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
} else {
|
||||
throw new Error('未找到游戏token,请重新添加角色')
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟通过WebSocket执行任务
|
||||
console.log(`通过WebSocket执行任务: ${taskId}`)
|
||||
|
||||
// 更新本地任务状态
|
||||
const taskIndex = tasks.value.findIndex(task => task.id === taskId)
|
||||
if (taskIndex !== -1) {
|
||||
tasks.value[taskIndex] = {
|
||||
...tasks.value[taskIndex],
|
||||
completed: true,
|
||||
completedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
// 添加执行日志
|
||||
if (!tasks.value[taskIndex].logs) {
|
||||
tasks.value[taskIndex].logs = []
|
||||
}
|
||||
tasks.value[taskIndex].logs.push({
|
||||
id: Date.now(),
|
||||
timestamp: Date.now(),
|
||||
type: 'success',
|
||||
message: `任务 "${tasks.value[taskIndex].title}" 执行成功`
|
||||
})
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem(`dailyTasks_${selectedRoleId.value}`, JSON.stringify(tasks.value))
|
||||
}
|
||||
|
||||
message.success('任务执行成功')
|
||||
} catch (error) {
|
||||
console.error('执行任务失败:', error)
|
||||
|
||||
// 添加错误日志
|
||||
const taskIndex = tasks.value.findIndex(task => task.id === taskId)
|
||||
if (taskIndex !== -1) {
|
||||
if (!tasks.value[taskIndex].logs) {
|
||||
tasks.value[taskIndex].logs = []
|
||||
}
|
||||
tasks.value[taskIndex].logs.push({
|
||||
id: Date.now(),
|
||||
timestamp: Date.now(),
|
||||
type: 'error',
|
||||
message: `任务执行失败: ${error.message}`
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const toggleTaskStatus = (taskId) => {
|
||||
const taskIndex = tasks.value.findIndex(task => task.id === taskId)
|
||||
if (taskIndex !== -1) {
|
||||
tasks.value[taskIndex].completed = !tasks.value[taskIndex].completed
|
||||
message.info('任务状态已更新')
|
||||
}
|
||||
}
|
||||
|
||||
const updateTask = (updatedTask) => {
|
||||
const taskIndex = tasks.value.findIndex(task => task.id === updatedTask.id)
|
||||
if (taskIndex !== -1) {
|
||||
tasks.value[taskIndex] = updatedTask
|
||||
}
|
||||
}
|
||||
|
||||
const handleBulkAction = (key) => {
|
||||
switch (key) {
|
||||
case 'execute-all-pending':
|
||||
executeAllPendingTasks()
|
||||
break
|
||||
case 'mark-all-completed':
|
||||
markAllCompleted()
|
||||
break
|
||||
case 'reset-all-tasks':
|
||||
resetAllTasks()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const executeAllPendingTasks = async () => {
|
||||
const pendingTasks = tasks.value.filter(task => !task.completed && task.canExecute)
|
||||
|
||||
if (pendingTasks.length === 0) {
|
||||
message.info('没有可执行的待完成任务')
|
||||
return
|
||||
}
|
||||
|
||||
dialog.confirm({
|
||||
title: '批量执行任务',
|
||||
content: `确定要执行 ${pendingTasks.length} 个待完成任务吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
for (const task of pendingTasks) {
|
||||
try {
|
||||
await executeTask(task.id)
|
||||
successCount++
|
||||
} catch (error) {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
message.info(`批量执行完成:成功 ${successCount} 个,失败 ${failCount} 个`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const markAllCompleted = () => {
|
||||
const pendingTasks = tasks.value.filter(task => !task.completed)
|
||||
|
||||
if (pendingTasks.length === 0) {
|
||||
message.info('所有任务都已完成')
|
||||
return
|
||||
}
|
||||
|
||||
dialog.confirm({
|
||||
title: '标记所有任务为已完成',
|
||||
content: `确定要将 ${pendingTasks.length} 个待完成任务标记为已完成吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
pendingTasks.forEach(task => {
|
||||
task.completed = true
|
||||
task.completedAt = new Date().toISOString()
|
||||
})
|
||||
message.success('所有任务已标记为完成')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetAllTasks = () => {
|
||||
dialog.confirm({
|
||||
title: '重置所有任务状态',
|
||||
content: '确定要重置所有任务状态吗?此操作将清除所有完成记录。',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
tasks.value.forEach(task => {
|
||||
task.completed = false
|
||||
task.completedAt = null
|
||||
})
|
||||
message.success('所有任务状态已重置')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
// 确保用户已登录
|
||||
if (!authStore.isAuthenticated) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化游戏角色数据
|
||||
if (gameRolesStore.gameRoles.length === 0) {
|
||||
await gameRolesStore.fetchGameRoles()
|
||||
}
|
||||
|
||||
// 设置默认选中的角色
|
||||
if (gameRolesStore.selectedRole) {
|
||||
selectedRoleId.value = gameRolesStore.selectedRole.id
|
||||
// 尝试从本地存储加载任务数据
|
||||
const savedTasks = localStorage.getItem(`dailyTasks_${selectedRoleId.value}`)
|
||||
if (savedTasks) {
|
||||
try {
|
||||
tasks.value = JSON.parse(savedTasks)
|
||||
} catch (error) {
|
||||
console.error('解析任务数据失败:', error)
|
||||
refreshTasks()
|
||||
}
|
||||
} else {
|
||||
refreshTasks()
|
||||
}
|
||||
} else if (gameRolesStore.gameRoles.length > 0) {
|
||||
selectedRoleId.value = gameRolesStore.gameRoles[0].id
|
||||
onRoleChange(selectedRoleId.value)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听选中角色变化
|
||||
watch(() => gameRolesStore.selectedRole, (newRole) => {
|
||||
if (newRole && newRole.id !== selectedRoleId.value) {
|
||||
selectedRoleId.value = newRole.id
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.daily-tasks-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: var(--spacing-xl) 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: var(--font-size-lg);
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.role-selector-section {
|
||||
background: white;
|
||||
padding: var(--spacing-lg) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.role-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.selector-label {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.role-stats {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: white;
|
||||
padding: var(--spacing-md) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.tasks-section {
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.tasks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1200px) {
|
||||
.tasks-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.role-selector {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.role-stats {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tasks-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
840
src/views/Dashboard.vue
Normal file
@@ -0,0 +1,840 @@
|
||||
<template>
|
||||
<div class="dashboard-page">
|
||||
<!-- 顶部导航 -->
|
||||
<nav class="dashboard-nav">
|
||||
<div class="nav-container">
|
||||
<div class="nav-brand">
|
||||
<img
|
||||
src="/icons/logo.png"
|
||||
alt="XYZW"
|
||||
class="brand-logo"
|
||||
>
|
||||
<span class="brand-text">XYZW 控制台</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu">
|
||||
<router-link
|
||||
to="/dashboard"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><Home /></n-icon>
|
||||
<span>首页</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/game-features"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><Cube /></n-icon>
|
||||
<span>游戏功能</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/tokens"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><PersonCircle /></n-icon>
|
||||
<span>Token管理</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/daily-tasks"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><Settings /></n-icon>
|
||||
<span>任务管理</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/message-test"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><ChatBubbles /></n-icon>
|
||||
<span>消息测试</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/profile"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><Settings /></n-icon>
|
||||
<span>个人设置</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="nav-user">
|
||||
<n-dropdown
|
||||
:options="userMenuOptions"
|
||||
@select="handleUserAction"
|
||||
>
|
||||
<div class="user-info">
|
||||
<n-avatar
|
||||
size="medium"
|
||||
fallback-src="/icons/xiaoyugan.png"
|
||||
/>
|
||||
<span class="username">{{ tokenStore.selectedToken?.name || '未选择Token' }}</span>
|
||||
<n-icon><ChevronDown /></n-icon>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="dashboard-main">
|
||||
<div class="main-container">
|
||||
<!-- 欢迎区域 -->
|
||||
<section class="welcome-section">
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-text">
|
||||
<h1>欢迎回来,{{ tokenStore.selectedToken?.name || '游戏玩家' }}!</h1>
|
||||
<p>今天是 {{ currentDate }},继续您的游戏管理之旅吧</p>
|
||||
</div>
|
||||
<div class="welcome-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="router.push('/game-features')"
|
||||
>
|
||||
进入游戏功能
|
||||
</n-button>
|
||||
<n-button
|
||||
size="large"
|
||||
@click="router.push('/tokens')"
|
||||
>
|
||||
管理Token
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<section class="stats-section">
|
||||
<div class="stats-grid">
|
||||
<div
|
||||
v-for="stat in statistics"
|
||||
:key="stat.id"
|
||||
class="stat-card"
|
||||
>
|
||||
<div
|
||||
class="stat-icon"
|
||||
:style="{ color: stat.color }"
|
||||
>
|
||||
<component :is="stat.icon" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">
|
||||
{{ stat.value }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
{{ stat.label }}
|
||||
</div>
|
||||
<div
|
||||
class="stat-change"
|
||||
:class="stat.changeType"
|
||||
>
|
||||
{{ stat.change }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<section class="quick-actions-section">
|
||||
<h2 class="section-title">
|
||||
快速操作
|
||||
</h2>
|
||||
<div class="actions-grid">
|
||||
<div
|
||||
v-for="action in quickActions"
|
||||
:key="action.id"
|
||||
class="action-card"
|
||||
@click="handleQuickAction(action)"
|
||||
>
|
||||
<div class="action-icon">
|
||||
<component :is="action.icon" />
|
||||
</div>
|
||||
<div class="action-content">
|
||||
<h3>{{ action.title }}</h3>
|
||||
<p>{{ action.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<section class="recent-activity-section">
|
||||
<div class="activity-header">
|
||||
<h2 class="section-title">
|
||||
最近活动
|
||||
</h2>
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="refreshActivity"
|
||||
>
|
||||
刷新
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="recentActivities.length"
|
||||
class="activity-list"
|
||||
>
|
||||
<div
|
||||
v-for="activity in recentActivities"
|
||||
:key="activity.id"
|
||||
class="activity-item"
|
||||
>
|
||||
<div
|
||||
class="activity-icon"
|
||||
:class="activity.type"
|
||||
>
|
||||
<component :is="getActivityIcon(activity.type)" />
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">
|
||||
{{ activity.message }}
|
||||
</div>
|
||||
<div class="activity-time">
|
||||
{{ formatTime(activity.timestamp) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="empty-activity"
|
||||
>
|
||||
<n-empty description="暂无活动记录" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import {
|
||||
Home,
|
||||
PersonCircle,
|
||||
Cube,
|
||||
Settings,
|
||||
ChevronDown,
|
||||
Ribbon,
|
||||
CheckmarkCircle,
|
||||
Time,
|
||||
TrendingUp,
|
||||
Add,
|
||||
Chatbubbles,
|
||||
Cloud
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 响应式数据
|
||||
const recentActivities = ref([])
|
||||
|
||||
// 计算属性
|
||||
const currentDate = computed(() => {
|
||||
return new Date().toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long'
|
||||
})
|
||||
})
|
||||
|
||||
const statistics = computed(() => [
|
||||
{
|
||||
id: 1,
|
||||
icon: PersonCircle,
|
||||
label: '游戏Token',
|
||||
value: tokenStore.gameTokens.length,
|
||||
change: '+2 本月',
|
||||
changeType: 'positive',
|
||||
color: '#18a058'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: CheckmarkCircle,
|
||||
label: '已完成任务',
|
||||
value: '156',
|
||||
change: '+12 今日',
|
||||
changeType: 'positive',
|
||||
color: '#2080f0'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: Time,
|
||||
label: '节省时间',
|
||||
value: '24.5h',
|
||||
change: '+3.2h 本周',
|
||||
changeType: 'positive',
|
||||
color: '#f0a020'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: TrendingUp,
|
||||
label: '效率提升',
|
||||
value: '85%',
|
||||
change: '+15% 本月',
|
||||
changeType: 'positive',
|
||||
color: '#d03050'
|
||||
}
|
||||
])
|
||||
|
||||
const quickActions = ref([
|
||||
{
|
||||
id: 1,
|
||||
icon: Cube,
|
||||
title: '游戏功能',
|
||||
description: '访问所有游戏功能模块',
|
||||
action: 'game-features'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: Add,
|
||||
title: '添加Token',
|
||||
description: '快速添加新的游戏Token',
|
||||
action: 'add-token'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: CheckmarkCircle,
|
||||
title: '执行任务',
|
||||
description: '一键执行所有待完成任务',
|
||||
action: 'execute-tasks'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: Cloud,
|
||||
title: 'WebSocket测试',
|
||||
description: '测试WebSocket连接和游戏命令',
|
||||
action: 'websocket-test'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
icon: Settings,
|
||||
title: '系统设置',
|
||||
description: '配置个人偏好和系统选项',
|
||||
action: 'open-settings'
|
||||
}
|
||||
])
|
||||
|
||||
const userMenuOptions = [
|
||||
{
|
||||
label: '个人资料',
|
||||
key: 'profile'
|
||||
},
|
||||
{
|
||||
label: '账户设置',
|
||||
key: 'settings'
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout'
|
||||
}
|
||||
]
|
||||
|
||||
// 方法
|
||||
const handleUserAction = (key) => {
|
||||
switch (key) {
|
||||
case 'profile':
|
||||
router.push('/profile')
|
||||
break
|
||||
case 'settings':
|
||||
router.push('/settings')
|
||||
break
|
||||
case 'logout':
|
||||
tokenStore.clearAllTokens()
|
||||
message.success('已清除所有Token')
|
||||
router.push('/tokens')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuickAction = (action) => {
|
||||
switch (action.action) {
|
||||
case 'game-features':
|
||||
router.push('/game-features')
|
||||
break
|
||||
case 'add-token':
|
||||
router.push('/tokens')
|
||||
break
|
||||
case 'execute-tasks':
|
||||
router.push('/game-features')
|
||||
break
|
||||
case 'websocket-test':
|
||||
router.push('/websocket-test')
|
||||
break
|
||||
case 'open-settings':
|
||||
router.push('/profile')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const refreshActivity = () => {
|
||||
// 模拟刷新活动数据
|
||||
recentActivities.value = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'success',
|
||||
message: '成功完成日常任务:每日签到',
|
||||
timestamp: Date.now() - 30 * 60 * 1000
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'info',
|
||||
message: '添加了新的游戏角色:剑士小明',
|
||||
timestamp: Date.now() - 2 * 60 * 60 * 1000
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'warning',
|
||||
message: '任务执行遇到错误,请检查网络连接',
|
||||
timestamp: Date.now() - 4 * 60 * 60 * 1000
|
||||
}
|
||||
]
|
||||
message.success('活动数据已刷新')
|
||||
}
|
||||
|
||||
const getActivityIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return CheckmarkCircle
|
||||
case 'warning':
|
||||
return Time
|
||||
case 'info':
|
||||
default:
|
||||
return Cube
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
const diff = Date.now() - timestamp
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}天前`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时前`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟前`
|
||||
} else {
|
||||
return '刚刚'
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
// 确保有Token
|
||||
if (!tokenStore.hasTokens) {
|
||||
router.push('/tokens')
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化Token数据
|
||||
tokenStore.initTokenStore()
|
||||
refreshActivity()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dashboard-page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
// 导航栏
|
||||
.dashboard-nav {
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
padding: 0 var(--spacing-lg);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-sticky);
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
margin-right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--border-radius-medium);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary-color-light);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-user {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--border-radius-medium);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
// 主要内容
|
||||
.dashboard-main {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.main-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// 欢迎区域
|
||||
.welcome-section {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-2xl);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-lg);
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
// 统计区域
|
||||
.stats-section {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-light);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: var(--spacing-md);
|
||||
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
&.positive {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 快速操作区域
|
||||
.quick-actions-section {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.action-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-light);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: var(--spacing-md);
|
||||
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.action-content {
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 最近活动区域
|
||||
.recent-activity-section {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--border-radius-medium);
|
||||
transition: background var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.success {
|
||||
background: rgba(24, 160, 88, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: rgba(240, 160, 32, 0.1);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: rgba(32, 128, 240, 0.1);
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
:deep(svg) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-text {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.empty-activity {
|
||||
text-align: center;
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1024px) {
|
||||
.welcome-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-main {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.welcome-text h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
570
src/views/GameFeatures.vue
Normal file
@@ -0,0 +1,570 @@
|
||||
<template>
|
||||
<div class="game-features-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<h1 class="page-title">
|
||||
游戏功能
|
||||
</h1>
|
||||
<p class="page-subtitle">
|
||||
{{ tokenStore.selectedToken?.name || '未选择Token' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<div
|
||||
class="connection-status"
|
||||
:class="connectionStatus"
|
||||
>
|
||||
<n-icon><CloudDone /></n-icon>
|
||||
<span>{{ connectionStatusText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 反馈提示区域 -->
|
||||
<div
|
||||
v-if="showFeedback"
|
||||
class="feedback-section"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 功能模块网格 -->
|
||||
<div class="features-grid-section">
|
||||
<div class="container">
|
||||
<GameStatus />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket 连接状态 -->
|
||||
<div class="ws-status-section">
|
||||
<div class="container">
|
||||
<div class="ws-status-card">
|
||||
<div class="status-header">
|
||||
<h3>连接状态</h3>
|
||||
<n-button
|
||||
text
|
||||
@click="toggleConnection"
|
||||
>
|
||||
{{ isConnected ? '断开连接' : '重新连接' }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="status-content">
|
||||
<div class="status-item">
|
||||
<span>WebSocket状态:</span>
|
||||
<span :class="connectionClass">{{ connectionStatusText }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="tokenStore.selectedToken"
|
||||
class="status-item"
|
||||
>
|
||||
<span>当前Token:</span>
|
||||
<span>{{ tokenStore.selectedToken.name }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="lastActivity"
|
||||
class="status-item"
|
||||
>
|
||||
<span>最后活动:</span>
|
||||
<span>{{ lastActivity }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import GameStatus from '@/components/GameStatus.vue'
|
||||
import { CloudDone } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 响应式数据
|
||||
const showFeedback = ref(true)
|
||||
const lastActivity = ref(null)
|
||||
|
||||
// 计算属性
|
||||
const connectionStatus = computed(() => {
|
||||
if (!tokenStore.selectedToken) return 'disconnected'
|
||||
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
return status === 'connected' ? 'connected' : 'disconnected'
|
||||
})
|
||||
|
||||
const connectionStatusText = computed(() => {
|
||||
if (!tokenStore.selectedToken) return '未选择Token'
|
||||
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
return status === 'connected' ? '已连接' : '未连接'
|
||||
})
|
||||
|
||||
const connectionClass = computed(() => {
|
||||
return connectionStatus.value === 'connected' ? 'status-connected' : 'status-disconnected'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleFeatureAction = (featureType) => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
router.push('/tokens')
|
||||
return
|
||||
}
|
||||
|
||||
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
if (status !== 'connected') {
|
||||
message.warning('WebSocket未连接,请先建立连接')
|
||||
return
|
||||
}
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
const actions = {
|
||||
'team-challenge': () => {
|
||||
message.info('开始执行队伍挑战...')
|
||||
tokenStore.sendMessage(tokenId, 'fight_startareaarena')
|
||||
},
|
||||
'daily-tasks': () => {
|
||||
message.info('启动每日任务服务...')
|
||||
tokenStore.sendMessage(tokenId, 'task_claimdailyreward')
|
||||
},
|
||||
'salt-robot': () => {
|
||||
message.info('领取盐罐机器人奖励...')
|
||||
tokenStore.sendMessage(tokenId, 'bottlehelper_claim')
|
||||
},
|
||||
'idle-time': () => {
|
||||
message.info('领取挂机时间奖励...')
|
||||
tokenStore.sendMessage(tokenId, 'system_claimhangupreward')
|
||||
},
|
||||
'power-switch': () => {
|
||||
message.info('执行威震大开关...')
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
},
|
||||
'club-ranking': () => {
|
||||
message.info('报名俱乐部排位...')
|
||||
tokenStore.sendMessage(tokenId, 'legionmatch_rolesignup')
|
||||
},
|
||||
'club-checkin': () => {
|
||||
message.info('执行俱乐部签到...')
|
||||
tokenStore.sendMessage(tokenId, 'legion_signin')
|
||||
},
|
||||
'tower-challenge': () => {
|
||||
message.info('开始爬塔挑战...')
|
||||
tokenStore.sendMessage(tokenId, 'fight_starttower')
|
||||
}
|
||||
}
|
||||
|
||||
const action = actions[featureType]
|
||||
if (action) {
|
||||
action()
|
||||
} else {
|
||||
message.warning('功能暂未实现')
|
||||
}
|
||||
}
|
||||
|
||||
// 已移除 sendWebSocketMessage,使用 tokenStore.sendMessage 代替
|
||||
|
||||
const connectWebSocket = () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择一个Token')
|
||||
router.push('/tokens')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
const token = tokenStore.selectedToken.token
|
||||
|
||||
// 使用 tokenStore 的 WebSocket 连接管理
|
||||
tokenStore.createWebSocketConnection(tokenId, token)
|
||||
message.info('正在建立 WebSocket 连接...')
|
||||
|
||||
// 等待连接建立
|
||||
setTimeout(async () => {
|
||||
const status = tokenStore.getWebSocketStatus(tokenId)
|
||||
if (status === 'connected') {
|
||||
message.success('WebSocket 连接成功')
|
||||
// 连接成功后自动初始化游戏数据
|
||||
await initializeGameData()
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('WebSocket连接失败:', error)
|
||||
message.error('WebSocket连接失败')
|
||||
}
|
||||
}
|
||||
|
||||
const disconnectWebSocket = () => {
|
||||
if (tokenStore.selectedToken) {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.closeWebSocketConnection(tokenId)
|
||||
message.info('WebSocket连接已断开')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleConnection = () => {
|
||||
if (connectionStatus.value === 'connected') {
|
||||
disconnectWebSocket()
|
||||
} else {
|
||||
connectWebSocket()
|
||||
}
|
||||
}
|
||||
|
||||
// handleWebSocketMessage 已移除,消息处理由 tokenStore 负责
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 检查是否需要连接 WebSocket
|
||||
if (tokenStore.selectedToken) {
|
||||
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
if (status !== 'connected') {
|
||||
connectWebSocket()
|
||||
} else {
|
||||
// 如果已连接,立即获取初始数据
|
||||
initializeGameData()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化游戏数据
|
||||
const initializeGameData = async () => {
|
||||
if (!tokenStore.selectedToken) return
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
console.log('🎮 初始化游戏数据...')
|
||||
|
||||
// 获取角色信息
|
||||
console.log('🎮 正在获取角色信息...')
|
||||
const roleResult = tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
console.log('🎮 角色信息请求结果:', roleResult)
|
||||
|
||||
// 获取塔信息
|
||||
console.log('🎮 正在获取塔信息...')
|
||||
const towerResult = tokenStore.sendMessage(tokenId, 'tower_getinfo')
|
||||
console.log('🎮 塔信息请求结果:', towerResult)
|
||||
|
||||
// 获取队伍信息
|
||||
console.log('🎮 正在获取队伍信息...')
|
||||
const teamResult = tokenStore.sendMessage(tokenId, 'presetteam_getteam')
|
||||
console.log('🎮 队伍信息请求结果:', teamResult)
|
||||
|
||||
console.log('🎮 游戏数据初始化请求已发送')
|
||||
} catch (error) {
|
||||
console.warn('初始化游戏数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
// WebSocket 连接由 tokenStore 管理,不需要手动清理
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.game-features-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
// 页面头部
|
||||
.page-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
padding: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--border-radius-medium);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
&.connected {
|
||||
background: rgba(24, 160, 88, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
background: rgba(208, 48, 80, 0.1);
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 反馈提示区域
|
||||
.feedback-section {
|
||||
padding: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
// 功能模块网格
|
||||
.features-grid-section {
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
// 不同功能的主题色
|
||||
&.team-challenge { border-left-color: #2080f0; }
|
||||
&.daily-tasks { border-left-color: #f0a020; }
|
||||
&.salt-robot { border-left-color: #18a058; }
|
||||
&.idle-time { border-left-color: #d03050; }
|
||||
&.power-switch { border-left-color: #7c3aed; }
|
||||
&.club-ranking { border-left-color: #f59e0b; }
|
||||
&.club-checkin { border-left-color: #10b981; }
|
||||
&.tower-challenge { border-left-color: #6366f1; }
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
background: var(--primary-color-light);
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(svg) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.feature-badge, .feature-status {
|
||||
flex-shrink: 0;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-small);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.feature-status {
|
||||
&.in-progress {
|
||||
background: rgba(240, 160, 32, 0.1);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: rgba(24, 160, 88, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.waiting {
|
||||
background: rgba(32, 128, 240, 0.1);
|
||||
color: var(--info-color);
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
|
||||
.stage-text {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
// WebSocket状态区域
|
||||
.ws-status-section {
|
||||
padding: 0 0 var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.ws-status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1024px) {
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
575
src/views/GameRoles.vue
Normal file
@@ -0,0 +1,575 @@
|
||||
<template>
|
||||
<div class="game-roles-page">
|
||||
<div class="container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<h1>游戏角色</h1>
|
||||
<p>管理您的所有游戏角色</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="showAddModal = true"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><Add /></n-icon>
|
||||
</template>
|
||||
添加角色
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<div
|
||||
v-if="gameRolesStore.gameRoles.length"
|
||||
class="roles-grid"
|
||||
>
|
||||
<div
|
||||
v-for="role in gameRolesStore.gameRoles"
|
||||
:key="role.id"
|
||||
class="role-card"
|
||||
:class="{ active: role.id === gameRolesStore.selectedRole?.id }"
|
||||
@click="selectRole(role)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="role-avatar">
|
||||
<img
|
||||
:src="role.avatar || '/icons/xiaoyugan.png'"
|
||||
:alt="role.name"
|
||||
>
|
||||
</div>
|
||||
<div class="role-actions">
|
||||
<n-dropdown
|
||||
:options="roleMenuOptions"
|
||||
@select="(key) => handleRoleAction(key, role)"
|
||||
>
|
||||
<n-button text>
|
||||
<template #icon>
|
||||
<n-icon><EllipsisHorizontal /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<h3 class="role-name">
|
||||
{{ role.name }}
|
||||
</h3>
|
||||
<p class="role-info">
|
||||
{{ role.server }} | {{ role.level }}级
|
||||
</p>
|
||||
<div class="role-tags">
|
||||
<n-tag
|
||||
size="small"
|
||||
:type="role.isActive ? 'success' : 'default'"
|
||||
>
|
||||
{{ role.isActive ? '活跃' : '离线' }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-if="role.vip"
|
||||
size="small"
|
||||
>
|
||||
VIP
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="role-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">经验</span>
|
||||
<span class="stat-value">{{ role.exp || '0' }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">金币</span>
|
||||
<span class="stat-value">{{ formatNumber(role.gold || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-else
|
||||
class="empty-state"
|
||||
>
|
||||
<n-empty
|
||||
description="暂无游戏角色"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon size="64">
|
||||
<PersonCircle />
|
||||
</n-icon>
|
||||
</template>
|
||||
<template #extra>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="showAddModal = true"
|
||||
>
|
||||
添加第一个角色
|
||||
</n-button>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑角色模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showAddModal"
|
||||
preset="card"
|
||||
title="添加游戏角色"
|
||||
style="width: 500px"
|
||||
>
|
||||
<n-form
|
||||
ref="roleFormRef"
|
||||
:model="roleForm"
|
||||
:rules="roleRules"
|
||||
label-placement="left"
|
||||
label-width="80px"
|
||||
>
|
||||
<n-form-item
|
||||
label="角色名称"
|
||||
path="name"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="roleForm.name"
|
||||
placeholder="请输入角色名称"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item
|
||||
label="服务器"
|
||||
path="server"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="roleForm.server"
|
||||
:options="serverOptions"
|
||||
placeholder="请选择服务器"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item
|
||||
label="职业"
|
||||
path="profession"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="roleForm.profession"
|
||||
:options="professionOptions"
|
||||
placeholder="请选择职业"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item
|
||||
label="等级"
|
||||
path="level"
|
||||
>
|
||||
<n-input-number
|
||||
v-model:value="roleForm.level"
|
||||
:min="1"
|
||||
:max="200"
|
||||
placeholder="角色等级"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="账号信息">
|
||||
<n-input
|
||||
v-model:value="roleForm.account"
|
||||
placeholder="游戏账号(可选)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="备注">
|
||||
<n-input
|
||||
v-model:value="roleForm.note"
|
||||
type="textarea"
|
||||
placeholder="角色备注信息(可选)"
|
||||
:rows="3"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="modal-actions">
|
||||
<n-button @click="showAddModal = false">
|
||||
取消
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="isSubmitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ editingRole ? '保存' : '添加' }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage, useDialog } from 'naive-ui'
|
||||
import { useGameRolesStore } from '@/stores/gameRoles'
|
||||
import { PersonCircle, Add, EllipsisHorizontal } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const gameRolesStore = useGameRolesStore()
|
||||
|
||||
// 响应式数据
|
||||
const showAddModal = ref(false)
|
||||
const isSubmitting = ref(false)
|
||||
const editingRole = ref(null)
|
||||
const roleFormRef = ref(null)
|
||||
|
||||
// 角色表单
|
||||
const roleForm = reactive({
|
||||
name: '',
|
||||
server: '',
|
||||
profession: '',
|
||||
level: 1,
|
||||
account: '',
|
||||
note: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const roleRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
],
|
||||
server: [
|
||||
{ required: true, message: '请选择服务器', trigger: 'change' }
|
||||
],
|
||||
profession: [
|
||||
{ required: true, message: '请选择职业', trigger: 'change' }
|
||||
],
|
||||
level: [
|
||||
{ required: true, type: 'number', message: '请输入角色等级', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 选项数据
|
||||
const serverOptions = [
|
||||
{ label: '风云服', value: '风云服' },
|
||||
{ label: '神话服', value: '神话服' },
|
||||
{ label: '传奇服', value: '传奇服' },
|
||||
{ label: '梦幻服', value: '梦幻服' },
|
||||
{ label: '英雄服', value: '英雄服' }
|
||||
]
|
||||
|
||||
const professionOptions = [
|
||||
{ label: '战士', value: '战士' },
|
||||
{ label: '法师', value: '法师' },
|
||||
{ label: '道士', value: '道士' },
|
||||
{ label: '刺客', value: '刺客' },
|
||||
{ label: '弓手', value: '弓手' },
|
||||
{ label: '牧师', value: '牧师' }
|
||||
]
|
||||
|
||||
const roleMenuOptions = [
|
||||
{ label: '编辑', key: 'edit' },
|
||||
{ label: '设为主角色', key: 'set-primary' },
|
||||
{ label: '查看详情', key: 'view-details' },
|
||||
{ type: 'divider' },
|
||||
{ label: '删除', key: 'delete' }
|
||||
]
|
||||
|
||||
// 方法
|
||||
const selectRole = (role) => {
|
||||
gameRolesStore.selectRole(role)
|
||||
message.success(`已切换到角色:${role.name}`)
|
||||
}
|
||||
|
||||
const handleRoleAction = async (key, role) => {
|
||||
switch (key) {
|
||||
case 'edit':
|
||||
editRole(role)
|
||||
break
|
||||
case 'set-primary':
|
||||
selectRole(role)
|
||||
break
|
||||
case 'view-details':
|
||||
viewRoleDetails(role)
|
||||
break
|
||||
case 'delete':
|
||||
deleteRole(role)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const editRole = (role) => {
|
||||
editingRole.value = role
|
||||
Object.assign(roleForm, role)
|
||||
showAddModal.value = true
|
||||
}
|
||||
|
||||
const viewRoleDetails = (role) => {
|
||||
message.info('角色详情功能开发中...')
|
||||
}
|
||||
|
||||
const deleteRole = (role) => {
|
||||
dialog.warning({
|
||||
title: '删除角色',
|
||||
content: `确定要删除角色 "${role.name}" 吗?此操作无法恢复。`,
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
const result = await gameRolesStore.deleteGameRole(role.id)
|
||||
if (result.success) {
|
||||
message.success(result.message)
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!roleFormRef.value) return
|
||||
|
||||
try {
|
||||
await roleFormRef.value.validate()
|
||||
isSubmitting.value = true
|
||||
|
||||
let result
|
||||
if (editingRole.value) {
|
||||
// 编辑模式
|
||||
result = await gameRolesStore.updateGameRole(editingRole.value.id, roleForm)
|
||||
} else {
|
||||
// 添加模式
|
||||
result = await gameRolesStore.addGameRole(roleForm)
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
message.success(result.message)
|
||||
showAddModal.value = false
|
||||
resetForm()
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
// 表单验证失败
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.keys(roleForm).forEach(key => {
|
||||
roleForm[key] = key === 'level' ? 1 : ''
|
||||
})
|
||||
editingRole.value = null
|
||||
}
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num >= 100000000) {
|
||||
return (num / 100000000).toFixed(1) + '亿'
|
||||
} else if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + '万'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
// 获取游戏角色列表
|
||||
if (gameRolesStore.gameRoles.length === 0) {
|
||||
await gameRolesStore.fetchGameRoles()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.game-roles-page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-secondary);
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-lg);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.roles-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.role-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
box-shadow: var(--shadow-light);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-normal);
|
||||
border: 2px solid transparent;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-lg) var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.role-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--border-light);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.role-actions {
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast);
|
||||
|
||||
.role-card:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.role-info {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.role-tags {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: var(--spacing-md) var(--spacing-lg) var(--spacing-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.role-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roles-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.role-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
605
src/views/Home.vue
Normal file
@@ -0,0 +1,605 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar glass">
|
||||
<div class="container">
|
||||
<div class="nav-content">
|
||||
<div class="nav-brand">
|
||||
<img
|
||||
src="/icons/logo.png"
|
||||
alt="XYZW"
|
||||
class="brand-logo"
|
||||
>
|
||||
<span class="brand-text">XYZW 游戏管理系统</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-actions">
|
||||
<template v-if="!authStore.isAuthenticated">
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="router.push('/login')"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="router.push('/register')"
|
||||
>
|
||||
注册
|
||||
</n-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="router.push('/dashboard')"
|
||||
>
|
||||
进入控制台
|
||||
</n-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="main-content">
|
||||
<!-- 英雄区域 -->
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<div class="hero-text">
|
||||
<h1 class="hero-title">
|
||||
专业的游戏管理平台
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
让游戏变得更简单,让管理变得更高效
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="hero-button"
|
||||
@click="router.push(authStore.isAuthenticated ? '/dashboard' : '/register')"
|
||||
>
|
||||
{{ authStore.isAuthenticated ? '进入控制台' : '立即开始' }}
|
||||
</n-button>
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
size="large"
|
||||
class="hero-button"
|
||||
@click="scrollToFeatures"
|
||||
>
|
||||
了解更多
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-visual">
|
||||
<div class="feature-cards">
|
||||
<div
|
||||
v-for="card in featureCards"
|
||||
:key="card.id"
|
||||
class="feature-card"
|
||||
>
|
||||
<div class="card-icon">
|
||||
<component :is="card.icon" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3>{{ card.title }}</h3>
|
||||
<p>{{ card.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 功能特性 -->
|
||||
<section
|
||||
ref="featuresSection"
|
||||
class="features-section"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
核心功能
|
||||
</h2>
|
||||
<p class="section-subtitle">
|
||||
为您提供全方位的游戏管理解决方案
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="features-grid">
|
||||
<div
|
||||
v-for="feature in features"
|
||||
:key="feature.id"
|
||||
class="feature-item"
|
||||
>
|
||||
<div class="feature-icon">
|
||||
<component :is="feature.icon" />
|
||||
</div>
|
||||
<h3 class="feature-title">
|
||||
{{ feature.title }}
|
||||
</h3>
|
||||
<p class="feature-description">
|
||||
{{ feature.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<section class="stats-section">
|
||||
<div class="container">
|
||||
<div class="stats-grid">
|
||||
<div
|
||||
v-for="stat in stats"
|
||||
:key="stat.id"
|
||||
class="stat-item"
|
||||
>
|
||||
<div class="stat-number">
|
||||
{{ stat.number }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
{{ stat.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<img
|
||||
src="/icons/logo.png"
|
||||
alt="XYZW"
|
||||
class="footer-logo"
|
||||
>
|
||||
<span class="footer-text">XYZW 游戏管理系统</span>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a
|
||||
href="#"
|
||||
class="footer-link"
|
||||
>关于我们</a>
|
||||
<a
|
||||
href="#"
|
||||
class="footer-link"
|
||||
>隐私政策</a>
|
||||
<a
|
||||
href="#"
|
||||
class="footer-link"
|
||||
>服务条款</a>
|
||||
<a
|
||||
href="#"
|
||||
class="footer-link"
|
||||
>联系我们</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2024 XYZW. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
PersonCircle,
|
||||
Cube,
|
||||
Ribbon,
|
||||
Home,
|
||||
Settings
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const featuresSection = ref(null)
|
||||
|
||||
// 功能卡片数据
|
||||
const featureCards = ref([
|
||||
{
|
||||
id: 1,
|
||||
icon: PersonCircle,
|
||||
title: '角色管理',
|
||||
description: '统一管理游戏角色'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: Cube,
|
||||
title: '任务系统',
|
||||
description: '自动化日常任务'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: Ribbon,
|
||||
title: '数据统计',
|
||||
description: '全面的数据分析'
|
||||
}
|
||||
])
|
||||
|
||||
// 功能特性数据
|
||||
const features = ref([
|
||||
{
|
||||
id: 1,
|
||||
icon: PersonCircle,
|
||||
title: '角色管理',
|
||||
description: '轻松管理多个游戏角色,统一查看角色信息、等级进度和装备状态'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: Cube,
|
||||
title: '任务自动化',
|
||||
description: '智能日常任务系统,自动完成重复性任务,节省您的宝贵时间'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: Ribbon,
|
||||
title: '数据分析',
|
||||
description: '详细的数据统计和分析报告,帮助您更好地了解游戏进度'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: Settings,
|
||||
title: '个性化设置',
|
||||
description: '灵活的配置选项,根据您的需求定制最适合的管理方案'
|
||||
}
|
||||
])
|
||||
|
||||
// 统计数据
|
||||
const stats = ref([
|
||||
{ id: 1, number: '1000+', label: '活跃用户' },
|
||||
{ id: 2, number: '50K+', label: '管理角色' },
|
||||
{ id: 3, number: '100K+', label: '完成任务' },
|
||||
{ id: 4, number: '99.9%', label: '系统稳定性' }
|
||||
])
|
||||
|
||||
// 滚动到功能区域
|
||||
const scrollToFeatures = () => {
|
||||
if (featuresSection.value) {
|
||||
featuresSection.value.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化认证状态
|
||||
authStore.initAuth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
// 导航栏
|
||||
.navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: var(--z-fixed);
|
||||
padding: var(--spacing-md) 0;
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
// 主要内容
|
||||
.main-content {
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
// 英雄区域
|
||||
.hero-section {
|
||||
padding: var(--spacing-2xl) 0;
|
||||
min-height: 80vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-2xl);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3.5rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height-tight);
|
||||
margin-bottom: var(--spacing-md);
|
||||
background: linear-gradient(45deg, #fff, #e6f7ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: var(--font-size-xl);
|
||||
opacity: 0.9;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.hero-button {
|
||||
padding: var(--spacing-md) var(--spacing-xl);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
// 功能卡片
|
||||
.feature-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: #fff;
|
||||
margin-bottom: var(--spacing-md);
|
||||
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
color: white;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.card-content p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
// 功能特性区域
|
||||
.features-section {
|
||||
padding: var(--spacing-2xl) 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--text-secondary);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
text-align: center;
|
||||
padding: var(--spacing-xl);
|
||||
border-radius: var(--border-radius-large);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-heavy);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto var(--spacing-lg);
|
||||
color: var(--primary-color);
|
||||
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
color: var(--text-secondary);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
// 统计区域
|
||||
.stats-section {
|
||||
padding: var(--spacing-2xl) 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 3rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-size-lg);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
// 页脚
|
||||
.footer {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
text-align: center;
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.hero-content {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
554
src/views/Login.vue
Normal file
@@ -0,0 +1,554 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-container">
|
||||
<!-- 登录表单卡片 -->
|
||||
<div class="login-card glass">
|
||||
<div class="card-header">
|
||||
<div class="brand">
|
||||
<img
|
||||
src="/icons/logo.png"
|
||||
alt="XYZW"
|
||||
class="brand-logo"
|
||||
>
|
||||
<h1 class="brand-title">
|
||||
XYZW 游戏管理系统
|
||||
</h1>
|
||||
</div>
|
||||
<p class="welcome-text">
|
||||
欢迎回来,请登录您的账户
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<n-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
:show-label="false"
|
||||
>
|
||||
<n-form-item path="username">
|
||||
<n-input
|
||||
v-model:value="loginForm.username"
|
||||
placeholder="用户名或邮箱"
|
||||
:input-props="{ autocomplete: 'username' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<PersonCircle />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
v-model:value="loginForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
:input-props="{ autocomplete: 'current-password' }"
|
||||
@keydown.enter="handleLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<Lock />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<div class="form-options">
|
||||
<n-checkbox v-model:checked="loginForm.rememberMe">
|
||||
记住我
|
||||
</n-checkbox>
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="router.push('/forgot-password')"
|
||||
>
|
||||
忘记密码?
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="authStore.isLoading"
|
||||
class="login-button"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
</n-form>
|
||||
|
||||
<n-divider>
|
||||
<span class="divider-text">其他登录方式</span>
|
||||
</n-divider>
|
||||
|
||||
<div class="social-login">
|
||||
<n-button
|
||||
size="large"
|
||||
class="social-button"
|
||||
@click="handleSocialLogin('qq')"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<PersonCircle />
|
||||
</n-icon>
|
||||
</template>
|
||||
QQ登录
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
size="large"
|
||||
class="social-button"
|
||||
@click="handleSocialLogin('wechat')"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<PersonCircle />
|
||||
</n-icon>
|
||||
</template>
|
||||
微信登录
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="register-prompt">
|
||||
<span>还没有账户?</span>
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="router.push('/register')"
|
||||
>
|
||||
立即注册
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能展示 -->
|
||||
<div class="features-showcase">
|
||||
<div class="showcase-header">
|
||||
<h2>为什么选择 XYZW?</h2>
|
||||
<p>专业的游戏管理平台,让游戏变得更轻松</p>
|
||||
</div>
|
||||
|
||||
<div class="features-list">
|
||||
<div
|
||||
v-for="feature in features"
|
||||
:key="feature.id"
|
||||
class="feature-item"
|
||||
>
|
||||
<div class="feature-icon">
|
||||
<component :is="feature.icon" />
|
||||
</div>
|
||||
<div class="feature-content">
|
||||
<h3>{{ feature.title }}</h3>
|
||||
<p>{{ feature.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 背景装饰 -->
|
||||
<div class="background-decoration">
|
||||
<div class="decoration-circle circle-1" />
|
||||
<div class="decoration-circle circle-2" />
|
||||
<div class="decoration-circle circle-3" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { PersonCircle, Lock, Cube, Ribbon, Settings } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
const loginFormRef = ref(null)
|
||||
|
||||
// 登录表单数据
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const loginRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入用户名或邮箱',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: '密码长度不能少于6位',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 功能特性数据
|
||||
const features = [
|
||||
{
|
||||
id: 1,
|
||||
icon: PersonCircle,
|
||||
title: '多角色管理',
|
||||
description: '统一管理多个游戏角色,随时切换查看'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: Cube,
|
||||
title: '任务自动化',
|
||||
description: '智能执行日常任务,解放双手节省时间'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: Ribbon,
|
||||
title: '数据统计',
|
||||
description: '详细的进度统计,让游戏数据一目了然'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: Settings,
|
||||
title: '个性化配置',
|
||||
description: '灵活的设置选项,打造专属管理方案'
|
||||
}
|
||||
]
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
try {
|
||||
await loginFormRef.value.validate()
|
||||
|
||||
const result = await authStore.login({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password,
|
||||
rememberMe: loginForm.rememberMe
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
message.success('登录成功')
|
||||
|
||||
// 跳转到dashboard或之前访问的页面
|
||||
const redirect = router.currentRoute.value.query.redirect || '/dashboard'
|
||||
router.push(redirect)
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
// 表单验证失败
|
||||
console.error('Login validation failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理社交登录
|
||||
const handleSocialLogin = (provider) => {
|
||||
message.info(`${provider === 'qq' ? 'QQ' : '微信'}登录功能开发中...`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 如果已经登录,直接跳转
|
||||
if (authStore.isAuthenticated) {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-2xl);
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-2xl);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
.n-form {
|
||||
.n-form-item {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 48px;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.social-login {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.social-button {
|
||||
height: 44px;
|
||||
border: 1px solid var(--border-light);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.register-prompt {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
|
||||
span {
|
||||
margin-right: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
// 功能展示区域
|
||||
.features-showcase {
|
||||
color: white;
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.showcase-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-lg);
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.features-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--border-radius-large);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
transform: translateX(8px);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
p {
|
||||
opacity: 0.8;
|
||||
line-height: var(--line-height-relaxed);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 背景装饰
|
||||
.background-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.decoration-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
top: 10%;
|
||||
right: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
bottom: 20%;
|
||||
left: 15%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
top: 60%;
|
||||
right: 20%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1024px) {
|
||||
.login-container {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.features-showcase {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.showcase-header h2 {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.login-container {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.social-login {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.decoration-circle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
116
src/views/NotFound.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="not-found-page">
|
||||
<div class="container">
|
||||
<div class="error-content">
|
||||
<div class="error-visual">
|
||||
<div class="error-number">
|
||||
404
|
||||
</div>
|
||||
<div class="error-icon">
|
||||
<n-icon size="120">
|
||||
<Search />
|
||||
</n-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error-text">
|
||||
<h1>页面未找到</h1>
|
||||
<p>抱歉,您访问的页面不存在或已被移除。</p>
|
||||
</div>
|
||||
|
||||
<div class="error-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
返回首页
|
||||
</n-button>
|
||||
<n-button
|
||||
size="large"
|
||||
@click="router.back()"
|
||||
>
|
||||
返回上一页
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Search } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.not-found-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.error-content {
|
||||
text-align: center;
|
||||
color: white;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.error-visual {
|
||||
position: relative;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.error-number {
|
||||
font-size: 12rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
opacity: 0.1;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-lg);
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.error-number {
|
||||
font-size: 8rem;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
558
src/views/Profile.vue
Normal file
@@ -0,0 +1,558 @@
|
||||
<template>
|
||||
<div class="profile-page">
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1>个人资料</h1>
|
||||
<p>管理您的账户信息和偏好设置</p>
|
||||
</div>
|
||||
|
||||
<div class="profile-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="profile-section">
|
||||
<h2>基本信息</h2>
|
||||
<div class="info-card">
|
||||
<div class="avatar-section">
|
||||
<n-avatar
|
||||
size="large"
|
||||
:src="userInfo.avatar"
|
||||
fallback-src="/icons/xiaoyugan.png"
|
||||
/>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="changeAvatar"
|
||||
>
|
||||
更换头像
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="info-form">
|
||||
<n-form
|
||||
:model="userInfo"
|
||||
label-placement="left"
|
||||
label-width="80px"
|
||||
>
|
||||
<n-form-item label="用户名">
|
||||
<n-input
|
||||
v-model:value="userInfo.username"
|
||||
readonly
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱">
|
||||
<n-input v-model:value="userInfo.email" />
|
||||
</n-form-item>
|
||||
<n-form-item label="昵称">
|
||||
<n-input
|
||||
v-model:value="userInfo.nickname"
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="手机">
|
||||
<n-input
|
||||
v-model:value="userInfo.phone"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<div class="form-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="saveProfile"
|
||||
>
|
||||
保存更改
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码修改 -->
|
||||
<div class="profile-section">
|
||||
<h2>密码修改</h2>
|
||||
<div class="info-card">
|
||||
<n-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
label-placement="left"
|
||||
label-width="100px"
|
||||
>
|
||||
<n-form-item
|
||||
label="当前密码"
|
||||
path="currentPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="passwordForm.currentPassword"
|
||||
type="password"
|
||||
placeholder="请输入当前密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="新密码"
|
||||
path="newPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="passwordForm.newPassword"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="确认新密码"
|
||||
path="confirmPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<div class="form-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="changePassword"
|
||||
>
|
||||
修改密码
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统偏好 -->
|
||||
<div class="profile-section">
|
||||
<h2>系统偏好</h2>
|
||||
<div class="info-card">
|
||||
<div class="preferences-grid">
|
||||
<div class="preference-item">
|
||||
<div class="preference-label">
|
||||
<h3>主题设置</h3>
|
||||
<p>选择您喜欢的界面主题</p>
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="preferences.theme"
|
||||
:options="themeOptions"
|
||||
@update:value="updateTheme"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="preference-item">
|
||||
<div class="preference-label">
|
||||
<h3>语言设置</h3>
|
||||
<p>选择界面显示语言</p>
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="preferences.language"
|
||||
:options="languageOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="preference-item">
|
||||
<div class="preference-label">
|
||||
<h3>通知设置</h3>
|
||||
<p>接收任务完成通知</p>
|
||||
</div>
|
||||
<n-switch v-model:value="preferences.notifications" />
|
||||
</div>
|
||||
|
||||
<div class="preference-item">
|
||||
<div class="preference-label">
|
||||
<h3>自动执行</h3>
|
||||
<p>默认开启任务自动执行</p>
|
||||
</div>
|
||||
<n-switch v-model:value="preferences.autoExecute" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="savePreferences"
|
||||
>
|
||||
保存偏好
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token管理 -->
|
||||
<div class="profile-section">
|
||||
<h2>Token管理</h2>
|
||||
<TokenManager />
|
||||
</div>
|
||||
|
||||
<!-- 账户安全 -->
|
||||
<div class="profile-section">
|
||||
<h2>账户安全</h2>
|
||||
<div class="info-card">
|
||||
<div class="security-items">
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<h3>两步验证</h3>
|
||||
<p>为您的账户添加额外的安全保护</p>
|
||||
</div>
|
||||
<n-button @click="setupTwoFactor">
|
||||
设置
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<h3>登录历史</h3>
|
||||
<p>查看最近的登录记录</p>
|
||||
</div>
|
||||
<n-button @click="viewLoginHistory">
|
||||
查看
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<h3>数据导出</h3>
|
||||
<p>导出您的所有数据</p>
|
||||
</div>
|
||||
<n-button @click="exportData">
|
||||
导出
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="security-item danger">
|
||||
<div class="security-info">
|
||||
<h3>删除账户</h3>
|
||||
<p>永久删除您的账户和所有数据</p>
|
||||
</div>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="deleteAccount"
|
||||
>
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage, useDialog } from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import TokenManager from '@/components/TokenManager.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const authStore = useAuthStore()
|
||||
const passwordFormRef = ref(null)
|
||||
|
||||
// 用户信息
|
||||
const userInfo = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
avatar: ''
|
||||
})
|
||||
|
||||
// 密码表单
|
||||
const passwordForm = reactive({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 系统偏好
|
||||
const preferences = reactive({
|
||||
theme: 'auto',
|
||||
language: 'zh-CN',
|
||||
notifications: true,
|
||||
autoExecute: false
|
||||
})
|
||||
|
||||
// 密码验证规则
|
||||
const passwordRules = {
|
||||
currentPassword: [
|
||||
{ required: true, message: '请输入当前密码', trigger: 'blur' }
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value) => value === passwordForm.newPassword,
|
||||
message: '两次输入的密码不一致',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 选项数据
|
||||
const themeOptions = [
|
||||
{ label: '跟随系统', value: 'auto' },
|
||||
{ label: '浅色主题', value: 'light' },
|
||||
{ label: '深色主题', value: 'dark' }
|
||||
]
|
||||
|
||||
const languageOptions = [
|
||||
{ label: '简体中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' }
|
||||
]
|
||||
|
||||
// 方法
|
||||
const saveProfile = async () => {
|
||||
try {
|
||||
// 这里应该调用API保存用户信息
|
||||
message.success('个人信息保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const changePassword = async () => {
|
||||
if (!passwordFormRef.value) return
|
||||
|
||||
try {
|
||||
await passwordFormRef.value.validate()
|
||||
|
||||
// 这里应该调用API修改密码
|
||||
message.success('密码修改成功')
|
||||
|
||||
// 清空表单
|
||||
Object.keys(passwordForm).forEach(key => {
|
||||
passwordForm[key] = ''
|
||||
})
|
||||
} catch (error) {
|
||||
// 验证失败
|
||||
}
|
||||
}
|
||||
|
||||
const savePreferences = () => {
|
||||
// 保存偏好设置
|
||||
localStorage.setItem('userPreferences', JSON.stringify(preferences))
|
||||
message.success('偏好设置保存成功')
|
||||
}
|
||||
|
||||
const updateTheme = (theme) => {
|
||||
preferences.theme = theme
|
||||
// 这里可以实际应用主题变更
|
||||
}
|
||||
|
||||
const changeAvatar = () => {
|
||||
message.info('头像更换功能开发中...')
|
||||
}
|
||||
|
||||
const setupTwoFactor = () => {
|
||||
message.info('两步验证设置功能开发中...')
|
||||
}
|
||||
|
||||
const viewLoginHistory = () => {
|
||||
message.info('登录历史查看功能开发中...')
|
||||
}
|
||||
|
||||
const exportData = () => {
|
||||
message.info('数据导出功能开发中...')
|
||||
}
|
||||
|
||||
const deleteAccount = () => {
|
||||
dialog.warning({
|
||||
title: '删除账户',
|
||||
content: '此操作将永久删除您的账户和所有数据,且无法恢复。确定要继续吗?',
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
message.error('账户删除功能暂未开放')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 加载用户信息
|
||||
if (authStore.userInfo) {
|
||||
Object.assign(userInfo, authStore.userInfo)
|
||||
}
|
||||
|
||||
// 加载用户偏好
|
||||
const savedPreferences = localStorage.getItem('userPreferences')
|
||||
if (savedPreferences) {
|
||||
try {
|
||||
Object.assign(preferences, JSON.parse(savedPreferences))
|
||||
} catch (error) {
|
||||
console.error('解析用户偏好失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-secondary);
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-lg);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-section {
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
padding-bottom: var(--spacing-xl);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.info-form {
|
||||
.n-form-item {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: var(--spacing-xl);
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.preferences-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-xl);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.preference-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.preference-label {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.security-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.security-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-lg);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--border-radius-medium);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
border-color: var(--error-color);
|
||||
background: rgba(208, 48, 80, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.security-info {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-card {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.preference-item,
|
||||
.security-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.preference-item .n-select,
|
||||
.preference-item .n-switch {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
333
src/views/Register.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="register-page">
|
||||
<div class="register-container">
|
||||
<div class="register-card glass">
|
||||
<div class="card-header">
|
||||
<div class="brand">
|
||||
<img
|
||||
src="/icons/logo.png"
|
||||
alt="XYZW"
|
||||
class="brand-logo"
|
||||
>
|
||||
<h1 class="brand-title">
|
||||
注册 XYZW 账户
|
||||
</h1>
|
||||
</div>
|
||||
<p class="welcome-text">
|
||||
加入我们,开始您的游戏管理之旅
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<n-form
|
||||
ref="registerFormRef"
|
||||
:model="registerForm"
|
||||
:rules="registerRules"
|
||||
size="large"
|
||||
:show-label="false"
|
||||
>
|
||||
<n-form-item path="username">
|
||||
<n-input
|
||||
v-model:value="registerForm.username"
|
||||
placeholder="用户名"
|
||||
:input-props="{ autocomplete: 'username' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><PersonCircle /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="email">
|
||||
<n-input
|
||||
v-model:value="registerForm.email"
|
||||
placeholder="邮箱地址"
|
||||
:input-props="{ autocomplete: 'email' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><Mail /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
v-model:value="registerForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
:input-props="{ autocomplete: 'new-password' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><Lock /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="confirmPassword">
|
||||
<n-input
|
||||
v-model:value="registerForm.confirmPassword"
|
||||
type="password"
|
||||
placeholder="确认密码"
|
||||
:input-props="{ autocomplete: 'new-password' }"
|
||||
@keydown.enter="handleRegister"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><Lock /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<div class="form-options">
|
||||
<n-checkbox v-model:checked="registerForm.agreeTerms">
|
||||
我已阅读并同意
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="showTerms = true"
|
||||
>
|
||||
服务条款
|
||||
</n-button>
|
||||
和
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="showPrivacy = true"
|
||||
>
|
||||
隐私政策
|
||||
</n-button>
|
||||
</n-checkbox>
|
||||
</div>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="authStore.isLoading"
|
||||
:disabled="!registerForm.agreeTerms"
|
||||
class="register-button"
|
||||
@click="handleRegister"
|
||||
>
|
||||
注册账户
|
||||
</n-button>
|
||||
</n-form>
|
||||
|
||||
<div class="login-prompt">
|
||||
<span>已有账户?</span>
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="router.push('/login')"
|
||||
>
|
||||
立即登录
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { PersonCircle, Lock, Mail } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
const registerFormRef = ref(null)
|
||||
|
||||
// 注册表单数据
|
||||
const registerForm = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agreeTerms: false
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const registerRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入用户名',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
min: 3,
|
||||
max: 20,
|
||||
message: '用户名长度应在3-20个字符之间',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
email: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入邮箱地址',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
message: '请输入正确的邮箱格式',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: '密码长度不能少于6位',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
confirmPassword: [
|
||||
{
|
||||
required: true,
|
||||
message: '请确认密码',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
return value === registerForm.password
|
||||
},
|
||||
message: '两次输入的密码不一致',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 处理注册
|
||||
const handleRegister = async () => {
|
||||
if (!registerFormRef.value) return
|
||||
|
||||
try {
|
||||
await registerFormRef.value.validate()
|
||||
|
||||
if (!registerForm.agreeTerms) {
|
||||
message.warning('请先同意服务条款和隐私政策')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await authStore.register({
|
||||
username: registerForm.username,
|
||||
email: registerForm.email,
|
||||
password: registerForm.password
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
message.success('注册成功,请登录')
|
||||
router.push('/login')
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration validation failed:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.register-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.register-container {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.register-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-2xl);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
.n-form {
|
||||
.n-form-item {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-options {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
|
||||
:deep(.n-checkbox) {
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
.register-button {
|
||||
height: 48px;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.login-prompt {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
|
||||
span {
|
||||
margin-right: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.register-card {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1044
src/views/TokenImport.vue
Normal file
36
vite.config.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'@components': path.resolve(__dirname, 'src/components'),
|
||||
'@views': path.resolve(__dirname, 'src/views'),
|
||||
'@assets': path.resolve(__dirname, 'src/assets'),
|
||||
'@utils': path.resolve(__dirname, 'src/utils'),
|
||||
'@api': path.resolve(__dirname, 'src/api'),
|
||||
'@stores': path.resolve(__dirname, 'src/stores')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://xyzw.my',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path
|
||||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@use "@/assets/styles/variables.scss" as vars;'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||