1.0
25
xyzw_web_helper-main开源源码更新/.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'
|
||||
}
|
||||
};
|
||||
66
xyzw_web_helper-main开源源码更新/.gitignore
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
# 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/
|
||||
|
||||
.github
|
||||
70
xyzw_web_helper-main开源源码更新/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
xyzw_web_helper-main开源源码更新/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
xyzw_web_helper-main开源源码更新/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.
|
||||
155
xyzw_web_helper-main开源源码更新/LOCAL_TOKEN_CHANGES.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# 本地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. Token与配置已持久化到浏览器 IndexedDB(库:原生 API,自实现封装)
|
||||
2. 首次初始化会自动从 localStorage 迁移到 IndexedDB(若 DB 为空)
|
||||
3. 清除站点数据会同时清除 IndexedDB,建议定期导出备份
|
||||
4. WebSocket连接使用模拟URL,需要根据实际情况修改
|
||||
|
||||
## 持久化实现说明(IndexedDB)
|
||||
|
||||
- 存储结构:
|
||||
- `kv` 表:保存 `userToken`
|
||||
- `gameTokens` 表:按 `roleId` 分条存储 token 记录
|
||||
- 读写方式:
|
||||
- Store 内存态与 UI 同步,写操作异步持久化到 DB(不阻塞界面)
|
||||
- `initTokenManager()` 异步加载 DB 数据并填充内存态
|
||||
- 兼容迁移:
|
||||
- 若 DB 为空且 localStorage 有旧数据,则一次性迁移至 DB
|
||||
- 迁移后仍保留 localStorage 数据以避免意外数据丢失(可后续手动清理)
|
||||
616
xyzw_web_helper-main开源源码更新/README.md
Normal file
@@ -0,0 +1,616 @@
|
||||
# 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管理系统
|
||||
- **双重导入方式**:支持手动输入和URL接口获取两种Token导入方式
|
||||
- **Base64解码支持**:自动识别和解析多种Base64格式的游戏Token
|
||||
- **多角色管理**:同时管理多个游戏账号,支持角色信息展示
|
||||
- **本地存储**:安全的本地数据存储,无需后端服务器
|
||||
- **Token验证**:自动验证Token有效性和格式完整性
|
||||
- **自动刷新**:支持URL获取的Token自动刷新功能
|
||||
|
||||
### 🌐 WebSocket通信
|
||||
- **BON协议支持**:内置Binary Object Notation协议编解码
|
||||
- **多重加密**:支持LX、X、XTM等多种加密方式
|
||||
- **自动重连**:智能断线重连机制,确保连接稳定
|
||||
- **消息队列**:内置消息队列系统,支持批量发送和响应处理
|
||||
|
||||
### 🎮 游戏功能
|
||||
- **日常任务管理**:自动化日常任务执行和奖励领取
|
||||
- **角色状态监控**:实时显示角色等级、职业、服务器等信息
|
||||
- **团队管理**:队伍状态查看和管理功能
|
||||
- **爬塔进度**:爬塔状态追踪和数据分析
|
||||
|
||||
### 🛠️ 开发工具
|
||||
- **消息测试器**:BON协议消息编码/解码测试工具
|
||||
- **WebSocket调试**:实时WebSocket连接和消息调试
|
||||
- **协议验证**:游戏协议消息格式验证工具
|
||||
|
||||
### 🎨 主题系统
|
||||
- **智能主题切换**:支持深浅主题无缝切换,自动适应系统主题偏好
|
||||
- **实时响应**:主题切换立即生效,无需刷新页面
|
||||
- **全组件覆盖**:完整支持Naive UI组件库的深色主题
|
||||
- **记忆偏好**:自动保存用户主题选择,下次访问自动应用
|
||||
- **统一设计**:所有页面使用统一的圆形主题切换按钮
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 前端技术栈
|
||||
```
|
||||
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 # 本地存储管理
|
||||
│
|
||||
├── 🔧 composables/ # Vue 3组合式函数
|
||||
│ └── useTheme.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管理器
|
||||
├── ThemeToggle.vue # 主题切换按钮
|
||||
├── 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导入与管理
|
||||
|
||||
#### 支持的导入方式
|
||||
|
||||
##### 方式一:手动输入
|
||||
支持多种Base64格式的Token字符串:
|
||||
|
||||
```javascript
|
||||
// 纯Base64格式
|
||||
"eyJ0b2tlbiI6ImFiY2QxMjM0In0="
|
||||
|
||||
// 带前缀格式
|
||||
"token:eyJ0b2tlbiI6ImFiY2QxMjM0In0="
|
||||
```
|
||||
|
||||
##### 方式二:URL接口获取
|
||||
通过API接口自动获取Token,支持定时刷新:
|
||||
|
||||
```javascript
|
||||
// API接口返回格式
|
||||
{
|
||||
"token": "eyJ0b2tlbiI6ImFiY2QxMjM0In0=", // 必需字段
|
||||
"server": "风云服" // 可选字段
|
||||
}
|
||||
```
|
||||
|
||||
#### 导入步骤
|
||||
1. 进入 **Token管理** 页面
|
||||
2. 选择导入方式:
|
||||
- **手动输入**:粘贴Base64编码的Token字符串
|
||||
- **URL获取**:输入Token获取接口地址
|
||||
3. 系统自动解析和验证Token格式
|
||||
4. 设置角色名称和基本信息
|
||||
5. 保存到本地存储
|
||||
|
||||
#### Token刷新功能
|
||||
- 通过URL方式导入的Token支持一键刷新
|
||||
- 刷新时会重新请求原API接口获取最新Token
|
||||
- 自动重新建立WebSocket连接
|
||||
- 保持角色信息和配置不变
|
||||
|
||||
### 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. 主题系统使用
|
||||
|
||||
#### 主题切换功能
|
||||
- **位置**:在页面右上角可以找到圆形的主题切换按钮
|
||||
- **图标说明**:
|
||||
- ☀️ 太阳图标:当前为浅色主题,点击切换到深色主题
|
||||
- 🌙 月亮图标:当前为深色主题,点击切换到浅色主题
|
||||
- **自动记忆**:用户的主题选择会自动保存,下次访问时自动应用
|
||||
- **系统同步**:首次访问时会自动检测系统主题偏好
|
||||
|
||||
#### 技术实现特点
|
||||
```javascript
|
||||
// 使用主题切换组件
|
||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
const { isDark, toggleTheme } = useTheme()
|
||||
```
|
||||
|
||||
### 5. 游戏功能使用
|
||||
|
||||
#### 日常任务自动化
|
||||
- 自动签到奖励领取
|
||||
- 日常任务完成状态检查
|
||||
- 奖励自动领取
|
||||
- 任务进度实时追踪
|
||||
|
||||
#### 角色状态监控
|
||||
- 实时等级和经验显示
|
||||
- 职业和技能信息
|
||||
- 服务器状态监控
|
||||
- 在线时长统计
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 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.1.1 (Current - 2025.10.01)
|
||||
- 🎮 **月度任务系统**
|
||||
- 新增月度任务进度跟踪面板,实时显示钓鱼和竞技场进度
|
||||
- 实现钓鱼自动补齐功能,智能优先使用免费次数
|
||||
- 添加竞技场自动补齐,采用贪心策略估算战斗次数
|
||||
- 支持一键完成月度任务和进度刷新功能
|
||||
|
||||
- 👥 **角色身份卡系统**
|
||||
- 新增角色身份卡组件,展示角色头像、等级、战力等详细信息
|
||||
- 实现完整的战力段位系统,包含10个段位等级和专属样式
|
||||
- 添加角色头像默认图片库和智能加载失败处理
|
||||
- 优化队伍状态展示,增加炫光边框动画效果
|
||||
|
||||
- ⚡ **性能与稳定性优化**
|
||||
- 优化WebSocket连接状态监听和错误处理机制
|
||||
- 移除大量调试日志输出,减少控制台噪音
|
||||
- 改进Token过期检测和用户友好提示机制
|
||||
- 调整游戏命令结构,统一消息响应格式
|
||||
|
||||
- 🎨 **界面体验提升**
|
||||
- 完善战力数值格式化显示(支持亿、万单位转换)
|
||||
- 实现段位进度条计算和可视化展示
|
||||
- 添加深色主题下的身份卡样式完美适配
|
||||
- 优化移动端显示效果,调整元素尺寸和间距
|
||||
|
||||
- 🐛 **Bug修复**
|
||||
- 修复若干已知问题和代码错误
|
||||
- 清理临时文档文件,保持项目结构整洁
|
||||
- 优化角色信息获取逻辑,提升数据获取可靠性
|
||||
|
||||
### v2.1.0 (2025.09.04)
|
||||
- 🌓 **全新主题系统**
|
||||
- 全局深浅主题切换功能,支持系统主题自动检测
|
||||
- 优雅的圆形切换按钮,与界面设计完美融合
|
||||
- 实时热切换,无需刷新页面即可切换主题
|
||||
- 完整支持Naive UI组件的深色主题
|
||||
- 智能记忆用户主题偏好设置
|
||||
|
||||
- ⚡ **响应式体验优化**
|
||||
- 修复主题切换按钮状态不实时更新的问题
|
||||
- 解决弹框等Portal组件字体颜色不热切换的问题
|
||||
- 基于MutationObserver的DOM变化监听
|
||||
- 事件驱动的主题状态同步机制
|
||||
|
||||
- 🎨 **界面统一性提升**
|
||||
- 统一TokenImport和Dashboard页面的主题切换组件
|
||||
- 使用太阳/月亮图标直观表示主题状态
|
||||
- 添加按钮hover动画效果
|
||||
- 完善的CSS深色主题适配
|
||||
|
||||
- 🔧 **技术架构改进**
|
||||
- 重构useTheme composable,使用响应式ref替代computed
|
||||
- 双重DOM监听确保状态同步(html + body)
|
||||
- 支持data-theme属性和class类名双重主题检测
|
||||
- 优化的事件系统和状态管理
|
||||
|
||||
### v2.0.0 (Legacy)
|
||||
- 🎉 重构Token管理系统,支持多角色管理
|
||||
- 🔧 升级WebSocket客户端,支持更多游戏协议
|
||||
- 🎨 全新UI设计,基于Naive UI组件库
|
||||
- ⚡ 优化BON协议处理,提升消息编解码性能
|
||||
- 🛡️ 增强安全性,支持多种加密方式
|
||||
- 🧪 添加完整的测试和调试工具
|
||||
|
||||
### v1.x.x (Legacy)
|
||||
- 基础Token管理功能
|
||||
- 简单WebSocket连接
|
||||
- 基础游戏功能支持
|
||||
|
||||
---
|
||||
|
||||
## 🗓️ 版本更新计划
|
||||
|
||||
### v2.2.0 (计划中 - Q1 2026)
|
||||
- 🎯 **自动化增强**
|
||||
- [ ] 智能任务调度系统
|
||||
- [ ] 增加账号批量管理界面
|
||||
- [ ] 支持每日任务一键完成
|
||||
- [ ] 支持定时任务抢购符咒
|
||||
|
||||
- 🔧 **功能扩展**
|
||||
- [ ] 支持自定义脚本生成
|
||||
- [ ] 添加数据统计面板
|
||||
- [ ] 增强游戏状态监控
|
||||
- [ ] 支持多服务器管理
|
||||
|
||||
### v2.1.0 已完成功能 ✅
|
||||
- 🎨 **用户界面**
|
||||
- [x] 全局深浅主题切换系统
|
||||
- [x] 实时热切换,无需刷新页面
|
||||
- [x] 优雅的圆形主题切换按钮
|
||||
- [x] 完整的Naive UI深色主题支持
|
||||
- [x] 界面已有bug修复
|
||||
|
||||
- 🔧 **技术优化**
|
||||
- [x] 支持远端获取Token(URL接口方式)
|
||||
- [x] 支持Token自动刷新功能
|
||||
- [x] 响应式状态管理优化
|
||||
- [x] DOM变化监听和事件驱动更新
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目基于 [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)
|
||||
- **联系邮箱**:[发邮件给我](mailto:stevefeng59@gmail.com)
|
||||
- **TG群组**:[欢迎加入](https://t.me/+SEDhXWN_OpNiMGI1)
|
||||
|
||||
---
|
||||
|
||||
## 👏 赞赏
|
||||
<img src="https://github.com/w1249178256/xyzw_web_helper/blob/main/public/IMG_8007.JPG" width="200" height="200">
|
||||
|
||||
<div align="center">
|
||||
|
||||
**⭐ 如果这个项目对你有帮助,请给它一个星标!**
|
||||
|
||||
Made with ❤️ by FF Team
|
||||
|
||||
</div>
|
||||
41
xyzw_web_helper-main开源源码更新/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
xyzw_web_helper-main开源源码更新/package-lock.json
generated
Normal file
44
xyzw_web_helper-main开源源码更新/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
xyzw_web_helper-main开源源码更新/public/1733492491706148.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/1733492491706152.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/1736425783912140.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/173746572831736.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/174023274867420.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/174061875626614.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/IMG_8007.JPG
Normal file
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
2332
xyzw_web_helper-main开源源码更新/public/answer.json
Normal file
BIN
xyzw_web_helper-main开源源码更新/public/icons/1733492491706148.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/icons/1733492491706152.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/icons/1736425783912140.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/icons/173746572831736.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/icons/174023274867420.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/icons/174061875626614.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/icons/ta.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/icons/xiaoyugan.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/ta.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
xyzw_web_helper-main开源源码更新/public/xiaoyugan.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
245
xyzw_web_helper-main开源源码更新/src/App.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<n-config-provider :theme="naiveTheme">
|
||||
<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 { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
const { isDark, initTheme, setupSystemThemeListener, updateReactiveState } = useTheme()
|
||||
|
||||
// Naive UI 主题
|
||||
const naiveTheme = computed(() => {
|
||||
return isDark.value ? darkTheme : null
|
||||
})
|
||||
|
||||
// 监听主题变化事件
|
||||
const handleThemeChange = () => {
|
||||
// 确保响应式状态同步
|
||||
updateReactiveState()
|
||||
// 强制重新渲染
|
||||
setTimeout(() => {
|
||||
updateReactiveState()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTheme()
|
||||
setupSystemThemeListener()
|
||||
|
||||
// 监听自定义主题变化事件
|
||||
window.addEventListener('theme-change', handleThemeChange)
|
||||
|
||||
// 初始化时更新状态
|
||||
updateReactiveState()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('theme-change', handleThemeChange)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 主题变量 */
|
||||
:root {
|
||||
--app-background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--text-color: #333;
|
||||
--text-secondary: #666;
|
||||
--text-tertiary: #999;
|
||||
--bg-color: #ffffff;
|
||||
--border-color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 深色主题变量 */
|
||||
.dark {
|
||||
--app-background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%);
|
||||
--text-color: #ffffff !important;
|
||||
--text-secondary: #cbd5e0 !important;
|
||||
--text-tertiary: #a0aec0 !important;
|
||||
--bg-color: #1a202c !important;
|
||||
--border-color: #4a5568 !important;
|
||||
}
|
||||
|
||||
/* 深色主题样式优化 - 针对Naive UI组件 */
|
||||
html.dark,
|
||||
html[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* 全局深色主题文字颜色 */
|
||||
html.dark *,
|
||||
html[data-theme="dark"] * {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Naive UI 表单组件 */
|
||||
html.dark .n-form-item-label,
|
||||
html.dark .n-form-item-label__text,
|
||||
html[data-theme="dark"] .n-form-item-label,
|
||||
html[data-theme="dark"] .n-form-item-label__text {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Naive UI 输入组件 */
|
||||
html.dark .n-input,
|
||||
html.dark .n-input__input,
|
||||
html.dark .n-input__textarea,
|
||||
html[data-theme="dark"] .n-input,
|
||||
html[data-theme="dark"] .n-input__input,
|
||||
html[data-theme="dark"] .n-input__textarea {
|
||||
color: #ffffff !important;
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Naive UI 弹框组件 */
|
||||
html.dark .n-modal,
|
||||
html.dark .n-drawer,
|
||||
html.dark .n-popover,
|
||||
html.dark .n-dropdown,
|
||||
html.dark .n-tooltip,
|
||||
html.dark .n-dialog,
|
||||
html[data-theme="dark"] .n-modal,
|
||||
html[data-theme="dark"] .n-drawer,
|
||||
html[data-theme="dark"] .n-popover,
|
||||
html[data-theme="dark"] .n-dropdown,
|
||||
html[data-theme="dark"] .n-tooltip,
|
||||
html[data-theme="dark"] .n-dialog {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Naive UI 弹框内容 */
|
||||
html.dark .n-modal .n-card,
|
||||
html.dark .n-drawer-content,
|
||||
html.dark .n-popover-content,
|
||||
html.dark .n-dropdown-option,
|
||||
html.dark .n-dialog__content,
|
||||
html[data-theme="dark"] .n-modal .n-card,
|
||||
html[data-theme="dark"] .n-drawer-content,
|
||||
html[data-theme="dark"] .n-popover-content,
|
||||
html[data-theme="dark"] .n-dropdown-option,
|
||||
html[data-theme="dark"] .n-dialog__content {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Naive UI 下拉选项 */
|
||||
html.dark .n-dropdown-option__label,
|
||||
html.dark .n-select-option,
|
||||
html.dark .n-menu-item-content,
|
||||
html[data-theme="dark"] .n-dropdown-option__label,
|
||||
html[data-theme="dark"] .n-select-option,
|
||||
html[data-theme="dark"] .n-menu-item-content {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* 其他组件 */
|
||||
html.dark .n-collapse-item__header,
|
||||
html.dark .n-radio-button,
|
||||
html.dark .n-card,
|
||||
html.dark .n-card__content,
|
||||
html.dark .n-button,
|
||||
html.dark .n-tag,
|
||||
html[data-theme="dark"] .n-collapse-item__header,
|
||||
html[data-theme="dark"] .n-radio-button,
|
||||
html[data-theme="dark"] .n-card,
|
||||
html[data-theme="dark"] .n-card__content,
|
||||
html[data-theme="dark"] .n-button,
|
||||
html[data-theme="dark"] .n-tag {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* 标题和文本 */
|
||||
html.dark h1,
|
||||
html.dark h2,
|
||||
html.dark h3,
|
||||
html.dark h4,
|
||||
html.dark h5,
|
||||
html.dark h6,
|
||||
html.dark p,
|
||||
html.dark span,
|
||||
html.dark div,
|
||||
html.dark label,
|
||||
html[data-theme="dark"] h1,
|
||||
html[data-theme="dark"] h2,
|
||||
html[data-theme="dark"] h3,
|
||||
html[data-theme="dark"] h4,
|
||||
html[data-theme="dark"] h5,
|
||||
html[data-theme="dark"] h6,
|
||||
html[data-theme="dark"] p,
|
||||
html[data-theme="dark"] span,
|
||||
html[data-theme="dark"] div,
|
||||
html[data-theme="dark"] label {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* 占位符文本 */
|
||||
html.dark .n-input__placeholder,
|
||||
html.dark ::placeholder,
|
||||
html[data-theme="dark"] .n-input__placeholder,
|
||||
html[data-theme="dark"] ::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6) !important;
|
||||
}
|
||||
|
||||
/* 确保Portal渲染的组件也应用深色主题 */
|
||||
body.dark .n-modal-container,
|
||||
body.dark .n-drawer-container,
|
||||
body.dark .n-popover-container,
|
||||
body[data-theme="dark"] .n-modal-container,
|
||||
body[data-theme="dark"] .n-drawer-container,
|
||||
body[data-theme="dark"] .n-popover-container {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
background: var(--app-background);
|
||||
color: var(--text-color);
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
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;
|
||||
color: var(--text-color);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-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
xyzw_web_helper-main开源源码更新/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
xyzw_web_helper-main开源源码更新/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
xyzw_web_helper-main开源源码更新/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;
|
||||
}
|
||||
639
xyzw_web_helper-main开源源码更新/src/components/ClubBattleRecords.vue
Normal file
@@ -0,0 +1,639 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Inline 模式:卡片渲染 -->
|
||||
<div v-if="inline" class="inline-wrapper">
|
||||
<div class="inline-header">
|
||||
<div class="inline-title">俱乐部盐场战绩</div>
|
||||
<div class="header-actions">
|
||||
<n-button size="small" :disabled="loading" @click="handleRefresh">
|
||||
<template #icon>
|
||||
<n-icon><Refresh /></n-icon>
|
||||
</template>
|
||||
刷新
|
||||
</n-button>
|
||||
<n-button type="primary" size="small" :disabled="!battleRecords || loading" @click="handleExport">
|
||||
<template #icon>
|
||||
<n-icon><Copy /></n-icon>
|
||||
</template>
|
||||
导出
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="battle-records-content">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<n-spin size="large">
|
||||
<template #description>正在加载战绩数据...</template>
|
||||
</n-spin>
|
||||
</div>
|
||||
|
||||
<!-- 战绩列表 -->
|
||||
<div v-else-if="battleRecords && battleRecords.roleDetailsList" class="records-list">
|
||||
<div class="records-info">
|
||||
<n-tag type="info">查询日期: {{ queryDate }}</n-tag>
|
||||
<n-tag type="success">总成员: {{ battleRecords.roleDetailsList.length }}</n-tag>
|
||||
</div>
|
||||
|
||||
<div v-for="member in battleRecords.roleDetailsList" :key="member.roleId" class="member-card">
|
||||
<div class="member-header">
|
||||
<div class="member-info">
|
||||
<img v-if="member.headImg" :src="member.headImg" :alt="member.name" class="member-avatar" @error="handleImageError">
|
||||
<div v-else class="member-avatar-placeholder">{{ member.name?.charAt(0) || '?' }}</div>
|
||||
<span class="member-name">{{ member.name }}</span>
|
||||
</div>
|
||||
<div class="member-stats-inline">
|
||||
<span class="stat-inline win">击杀 {{ member.winCnt || 0 }}</span>
|
||||
<span class="stat-inline loss">死亡 {{ member.loseCnt || 0 }}</span>
|
||||
<span class="stat-inline siege">攻城 {{ member.buildingCnt || 0 }}</span>
|
||||
</div>
|
||||
<n-button text size="small" class="details-button" @click="toggleMemberDetails(member.roleId)">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ChevronDown v-if="!expandedMembers.has(member.roleId)" />
|
||||
<ChevronUp v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 战斗详情(可展开) -->
|
||||
<n-collapse-transition :show="expandedMembers.has(member.roleId)">
|
||||
<div class="battle-details">
|
||||
<div v-if="member.targetRoleList && member.targetRoleList.length > 0" class="battles-list">
|
||||
<div v-for="(battle, index) in member.targetRoleList" :key="index" class="battle-item" :class="getBattleClass(battle)">
|
||||
<div class="battle-participants">
|
||||
<div class="participant attacker">
|
||||
<img v-if="battle.roleInfo?.headImg" :src="battle.roleInfo.headImg" :alt="battle.roleInfo.name" class="participant-avatar" @error="handleImageError">
|
||||
<span class="participant-name">{{ battle.roleInfo?.name || '未知' }}</span>
|
||||
</div>
|
||||
<div class="battle-vs">
|
||||
<n-tag :type="battle.attackType === 0 ? 'warning' : 'info'" size="small">{{ parseAttackType(battle.attackType) }}</n-tag>
|
||||
<n-tag :type="battle.newWinFlag === 2 ? 'success' : 'error'" size="small">{{ parseBattleResult(battle.newWinFlag) }}</n-tag>
|
||||
</div>
|
||||
<div class="participant defender">
|
||||
<img v-if="battle.targetRoleInfo?.headImg" :src="battle.targetRoleInfo.headImg" :alt="battle.targetRoleInfo.name" class="participant-avatar" @error="handleImageError">
|
||||
<span class="participant-name">{{ battle.targetRoleInfo?.name || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="battle-time">{{ formatTimestamp(battle.timestamp) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-battles">
|
||||
<n-empty description="暂无战斗记录" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</n-collapse-transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-state">
|
||||
<n-empty description="暂无战绩数据" size="large">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<DocumentText />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal 模式 -->
|
||||
<n-modal
|
||||
v-else
|
||||
v-model:show="showModal"
|
||||
preset="card"
|
||||
title="俱乐部盐场战绩"
|
||||
style="width: 90%; max-width: 800px"
|
||||
@after-leave="handleClose"
|
||||
>
|
||||
<template #header-extra>
|
||||
<div class="header-actions">
|
||||
<n-button size="small" :disabled="loading" @click="handleRefresh">
|
||||
<template #icon>
|
||||
<n-icon><Refresh /></n-icon>
|
||||
</template>
|
||||
刷新
|
||||
</n-button>
|
||||
<n-button type="primary" size="small" :disabled="!battleRecords || loading" @click="handleExport">
|
||||
<template #icon>
|
||||
<n-icon><Copy /></n-icon>
|
||||
</template>
|
||||
导出
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="battle-records-content">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<n-spin size="large">
|
||||
<template #description>正在加载战绩数据...</template>
|
||||
</n-spin>
|
||||
</div>
|
||||
|
||||
<!-- 战绩列表 -->
|
||||
<div v-else-if="battleRecords && battleRecords.roleDetailsList" class="records-list">
|
||||
<div class="records-info">
|
||||
<n-tag type="info">查询日期: {{ queryDate }}</n-tag>
|
||||
<n-tag type="success">总成员: {{ battleRecords.roleDetailsList.length }}</n-tag>
|
||||
</div>
|
||||
|
||||
<div v-for="member in battleRecords.roleDetailsList" :key="member.roleId" class="member-card">
|
||||
<div class="member-header">
|
||||
<div class="member-info">
|
||||
<img v-if="member.headImg" :src="member.headImg" :alt="member.name" class="member-avatar" @error="handleImageError">
|
||||
<div v-else class="member-avatar-placeholder">{{ member.name?.charAt(0) || '?' }}</div>
|
||||
<span class="member-name">{{ member.name }}</span>
|
||||
</div>
|
||||
<div class="member-stats-inline">
|
||||
<span class="stat-inline win">击杀 {{ member.winCnt || 0 }}</span>
|
||||
<span class="stat-inline loss">死亡 {{ member.loseCnt || 0 }}</span>
|
||||
<span class="stat-inline siege">攻城 {{ member.buildingCnt || 0 }}</span>
|
||||
</div>
|
||||
<n-button text size="small" class="details-button" @click="toggleMemberDetails(member.roleId)">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ChevronDown v-if="!expandedMembers.has(member.roleId)" />
|
||||
<ChevronUp v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 战斗详情(可展开) -->
|
||||
<n-collapse-transition :show="expandedMembers.has(member.roleId)">
|
||||
<div class="battle-details">
|
||||
<div v-if="member.targetRoleList && member.targetRoleList.length > 0" class="battles-list">
|
||||
<div v-for="(battle, index) in member.targetRoleList" :key="index" class="battle-item" :class="getBattleClass(battle)">
|
||||
<div class="battle-participants">
|
||||
<div class="participant attacker">
|
||||
<img v-if="battle.roleInfo?.headImg" :src="battle.roleInfo.headImg" :alt="battle.roleInfo.name" class="participant-avatar" @error="handleImageError">
|
||||
<span class="participant-name">{{ battle.roleInfo?.name || '未知' }}</span>
|
||||
</div>
|
||||
<div class="battle-vs">
|
||||
<n-tag :type="battle.attackType === 0 ? 'warning' : 'info'" size="small">{{ parseAttackType(battle.attackType) }}</n-tag>
|
||||
<n-tag :type="battle.newWinFlag === 2 ? 'success' : 'error'" size="small">{{ parseBattleResult(battle.newWinFlag) }}</n-tag>
|
||||
</div>
|
||||
<div class="participant defender">
|
||||
<img v-if="battle.targetRoleInfo?.headImg" :src="battle.targetRoleInfo.headImg" :alt="battle.targetRoleInfo.name" class="participant-avatar" @error="handleImageError">
|
||||
<span class="participant-name">{{ battle.targetRoleInfo?.name || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="battle-time">{{ formatTimestamp(battle.timestamp) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-battles">
|
||||
<n-empty description="暂无战斗记录" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</n-collapse-transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-state">
|
||||
<n-empty description="暂无战绩数据" size="large">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<DocumentText />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import {
|
||||
Trophy,
|
||||
Refresh,
|
||||
Copy,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
DocumentText
|
||||
} from '@vicons/ionicons5'
|
||||
import {
|
||||
getLastSaturday,
|
||||
formatTimestamp,
|
||||
parseBattleResult,
|
||||
parseAttackType,
|
||||
formatBattleRecordsForExport,
|
||||
copyToClipboard
|
||||
} from '@/utils/clubBattleUtils'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
const message = useMessage()
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
const showModal = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const battleRecords = ref(null)
|
||||
const expandedMembers = ref(new Set())
|
||||
const queryDate = ref('')
|
||||
|
||||
// 格式化战力
|
||||
const formatPower = (power) => {
|
||||
if (!power) return '0'
|
||||
if (power >= 100000000) {
|
||||
return (power / 100000000).toFixed(2) + '亿'
|
||||
}
|
||||
if (power >= 10000) {
|
||||
return (power / 10000).toFixed(2) + '万'
|
||||
}
|
||||
return power.toString()
|
||||
}
|
||||
|
||||
// 获取战斗样式类
|
||||
const getBattleClass = (battle) => {
|
||||
const classes = []
|
||||
if (battle.newWinFlag === 2) {
|
||||
classes.push('battle-win')
|
||||
} else {
|
||||
classes.push('battle-loss')
|
||||
}
|
||||
if (battle.attackType === 0) {
|
||||
classes.push('battle-attack')
|
||||
} else {
|
||||
classes.push('battle-defend')
|
||||
}
|
||||
return classes.join(' ')
|
||||
}
|
||||
|
||||
// 切换成员详情展开状态
|
||||
const toggleMemberDetails = (roleId) => {
|
||||
if (expandedMembers.value.has(roleId)) {
|
||||
expandedMembers.value.delete(roleId)
|
||||
} else {
|
||||
expandedMembers.value.add(roleId)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图片加载错误
|
||||
const handleImageError = (event) => {
|
||||
event.target.style.display = 'none'
|
||||
}
|
||||
|
||||
// 查询战绩
|
||||
const fetchBattleRecords = async () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择游戏角色')
|
||||
return
|
||||
}
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 检查WebSocket连接
|
||||
const wsStatus = tokenStore.getWebSocketStatus(tokenId)
|
||||
if (wsStatus !== 'connected') {
|
||||
message.error('WebSocket未连接,无法查询战绩')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
queryDate.value = getLastSaturday()
|
||||
|
||||
try {
|
||||
const result = await tokenStore.sendMessageWithPromise(
|
||||
tokenId,
|
||||
'legionwar_getdetails',
|
||||
{ date: queryDate.value },
|
||||
10000
|
||||
)
|
||||
|
||||
if (result && result.roleDetailsList) {
|
||||
battleRecords.value = result
|
||||
message.success('战绩加载成功')
|
||||
} else {
|
||||
battleRecords.value = null
|
||||
message.warning('未查询到战绩数据')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询战绩失败:', error)
|
||||
message.error(`查询失败: ${error.message}`)
|
||||
battleRecords.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新战绩
|
||||
const handleRefresh = () => {
|
||||
fetchBattleRecords()
|
||||
}
|
||||
|
||||
// 导出战绩
|
||||
const handleExport = async () => {
|
||||
if (!battleRecords.value || !battleRecords.value.roleDetailsList) {
|
||||
message.warning('没有可导出的数据')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const exportText = formatBattleRecordsForExport(
|
||||
battleRecords.value.roleDetailsList,
|
||||
queryDate.value
|
||||
)
|
||||
await copyToClipboard(exportText)
|
||||
message.success('战绩已复制到剪贴板')
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
expandedMembers.value.clear()
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
fetchBattleRecords
|
||||
})
|
||||
|
||||
// Inline 模式:挂载后自动拉取
|
||||
onMounted(() => {
|
||||
if (props.inline) {
|
||||
fetchBattleRecords()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.inline-wrapper {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
border: 1px solid var(--border-light);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.inline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.inline-title {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.battle-records-content {
|
||||
min-height: 400px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.records-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.records-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.member-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-sm);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.member-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.member-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-avatar-placeholder {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.member-stats-inline {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.details-button {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.stat-inline {
|
||||
font-size: var(--font-size-xs);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-small);
|
||||
white-space: nowrap;
|
||||
min-width: 52px;
|
||||
text-align: center;
|
||||
|
||||
&.win {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&.loss {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&.siege {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
.battle-details {
|
||||
margin-top: var(--spacing-lg);
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.battles-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.battle-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
&.battle-win {
|
||||
border-left-color: #10b981;
|
||||
}
|
||||
|
||||
&.battle-loss {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.battle-participants {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.participant {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.participant-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.participant-name {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.battle-vs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.battle-time {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-battles {
|
||||
padding: var(--spacing-xl);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.member-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.battle-participants {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.battle-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.battle-time {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
283
xyzw_web_helper-main开源源码更新/src/components/ClubInfo.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="status-card club-info">
|
||||
<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: !!club }">
|
||||
<div class="status-dot" />
|
||||
<span>{{ club ? '已加入' : '暂无俱乐部' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div v-if="!club" class="empty-club">
|
||||
<n-empty description="暂无俱乐部" />
|
||||
<div class="actions">
|
||||
<n-button size="small" @click="refreshClub">刷新</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="toolbar">
|
||||
<n-space size="small">
|
||||
<n-button size="small" @click="refreshClub">刷新</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<n-tabs v-model:value="activeTab" type="line" animated>
|
||||
<n-tab-pane name="overview" tab="概览" display-directive="show:lazy">
|
||||
<div class="overview">
|
||||
<div class="club-header">
|
||||
<n-avatar :size="48" :src="club.logo || '/icons/xiaoyugan.png'" />
|
||||
<div class="meta">
|
||||
<div class="name">{{ club.name }}</div>
|
||||
<div class="sub">ID {{ club.id }} · Lv.{{ club.level }} · 服务器 {{ club.serverId }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="item">
|
||||
<div class="label">战力</div>
|
||||
<div class="value">{{ formatNumber(clubOverview.power) }}</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="label">段位</div>
|
||||
<div class="value">{{ clubOverview.dan }}</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="label">成员数</div>
|
||||
<div class="value">{{ memberCount }}</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="label">红洗次数</div>
|
||||
<div class="value">{{ clubOverview.redQuench }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-if="club.announcement" class="announcement">
|
||||
<div class="label">公告</div>
|
||||
<div class="text">{{ club.announcement }}</div>
|
||||
</div>
|
||||
<div class="leader" v-if="leader">
|
||||
<div class="label">会长</div>
|
||||
<div class="leader-info">
|
||||
<n-avatar :size="32" :src="leader.headImg || '/icons/xiaoyugan.png'"/>
|
||||
<span class="leader-name">{{ leader.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="members" tab="成员" display-directive="show:lazy">
|
||||
<div class="members">
|
||||
<div class="members-list">
|
||||
<div v-for="m in topMembers" :key="m.roleId" class="member-row">
|
||||
<div class="left">
|
||||
<n-avatar :size="28" :src="m.headImg || '/icons/xiaoyugan.png'"/>
|
||||
<span class="name">{{ m.name }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="power">{{ formatNumber(m.power || m.custom?.s_power || 0) }}</span>
|
||||
<span class="tag">{{ jobLabel(m.job) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="memberCount > topMembers.length" class="hint">仅显示前 {{ topMembers.length }} 名(按战力)</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="records" tab="盐场战绩" display-directive="show:lazy">
|
||||
<ClubBattleRecords inline />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import ClubBattleRecords from './ClubBattleRecords.vue'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
const info = computed(() => tokenStore.gameData?.legionInfo || null)
|
||||
const club = computed(() => info.value?.info || null)
|
||||
|
||||
const membersObj = computed(() => club.value?.members || {})
|
||||
const members = computed(() => Object.values(membersObj.value || {}))
|
||||
const memberCount = computed(() => members.value.length)
|
||||
|
||||
const leader = computed(() => {
|
||||
const lid = club.value?.leaderId
|
||||
if (!lid) return null
|
||||
return members.value.find(m => Number(m.roleId) === Number(lid)) || null
|
||||
})
|
||||
|
||||
const topMembers = computed(() => {
|
||||
return [...members.value]
|
||||
.sort((a, b) => (Number(b.power || b.custom?.s_power || 0) - Number(a.power || a.custom?.s_power || 0)))
|
||||
.slice(0, 20)
|
||||
})
|
||||
|
||||
const activeTab = ref('overview')
|
||||
|
||||
// 兼容不同服务端字段:从 info.info 和顶层 info 以及 statistics 中聚合
|
||||
const clubOverview = computed(() => {
|
||||
const i = info.value || {}
|
||||
const base = i.info || {}
|
||||
const stats = i.statistics || i.stat || {}
|
||||
|
||||
const power = Number(
|
||||
base.power ?? i.power ?? base.s_power ?? i.s_power ?? 0
|
||||
)
|
||||
const dan = base.dan ?? i.dan ?? base.rank ?? i.rank ?? '-'
|
||||
const redQuench = Number(
|
||||
base.redQuenchCnt ?? i.redQuenchCnt ?? stats['red:quench'] ?? stats['red_quench'] ?? 0
|
||||
)
|
||||
const lastWarRank = (
|
||||
stats['last:war:rank'] ?? stats['lastWarRank'] ?? stats['legion:last:war:rank'] ?? '-'
|
||||
)
|
||||
const noApply = Boolean(base.noApply ?? i.noApply)
|
||||
|
||||
return { power, dan: dan ?? '-', redQuench, lastWarRank, noApply }
|
||||
})
|
||||
|
||||
const refreshClub = () => {
|
||||
const token = tokenStore.selectedToken
|
||||
if (!token) return
|
||||
tokenStore.sendMessage(token.id, 'legion_getinfo')
|
||||
}
|
||||
|
||||
const jobLabel = (job) => {
|
||||
if (job === 1) return '会长'
|
||||
if (job === 2) return '副会长'
|
||||
return '成员'
|
||||
}
|
||||
|
||||
const formatNumber = (num) => {
|
||||
const n = Number(num || 0)
|
||||
if (n >= 1e8) return (n / 1e8).toFixed(2) + '亿'
|
||||
if (n >= 1e4) return (n / 1e4).toFixed(2) + '万'
|
||||
return String(n)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.club-info {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.club-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.meta {
|
||||
.name { font-size: var(--font-size-lg); font-weight: var(--font-weight-semibold); }
|
||||
.sub { color: var(--text-secondary); font-size: var(--font-size-sm); }
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.item {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-sm);
|
||||
.label { color: var(--text-secondary); font-size: var(--font-size-xs); margin-bottom: 2px; }
|
||||
.value { font-weight: var(--font-weight-medium); }
|
||||
}
|
||||
|
||||
.announcement .label, .leader .label { color: var(--text-secondary); font-size: var(--font-size-sm); margin-bottom: 4px; }
|
||||
.announcement .text { white-space: pre-wrap; }
|
||||
.leader .leader-info { display: flex; align-items: center; gap: var(--spacing-sm); }
|
||||
|
||||
.members-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.member-row { display: flex; align-items: center; justify-content: space-between; padding: 8px; border-radius: 8px; background: var(--bg-tertiary); }
|
||||
.member-row .left { display: flex; align-items: center; gap: 8px; }
|
||||
.member-row .right { display: flex; align-items: center; gap: 8px; color: var(--text-secondary); }
|
||||
.member-row .name { font-weight: var(--font-weight-medium); }
|
||||
.member-row .power { font-feature-settings: 'tnum' 1; font-variant-numeric: tabular-nums; }
|
||||
.hint { margin-top: 8px; color: var(--text-tertiary); font-size: var(--font-size-xs); }
|
||||
|
||||
.empty-club { text-align: center; }
|
||||
.empty-club .actions { margin-top: var(--spacing-sm); }
|
||||
}
|
||||
|
||||
/* 卡片基础样式,保持与 GameStatus 一致 */
|
||||
.status-card {
|
||||
background: var(--bg-primary);
|
||||
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: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
margin-right: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-info {
|
||||
flex: 1;
|
||||
h3 { margin: 0; font-size: var(--font-size-lg); }
|
||||
p { margin: 0; color: var(--text-secondary); font-size: var(--font-size-sm); }
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.active {
|
||||
background: rgba(24, 160, 88, 0.12);
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
</style>
|
||||
582
xyzw_web_helper-main开源源码更新/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>
|
||||
1274
xyzw_web_helper-main开源源码更新/src/components/DailyTaskStatus.vue
Normal file
1334
xyzw_web_helper-main开源源码更新/src/components/GameStatus.vue
Normal file
301
xyzw_web_helper-main开源源码更新/src/components/IdentityCard.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<div v-if="embedded" class="identity-embedded">
|
||||
<div class="identity-card embedded">
|
||||
<div class="card-header">
|
||||
<img src="/icons/Ob7pyorzmHiJcbab2c25af264d0758b527bc1b61cc3b.png" alt="身份牌" class="icon" />
|
||||
<div class="info">
|
||||
<h3>身份牌</h3>
|
||||
<p>角色与资源概览</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasRole" class="role-profile-header" :class="rankInfo.class">
|
||||
<div class="role-profile-content">
|
||||
<div class="avatar-container">
|
||||
<img :src="roleAvatar" :alt="roleInfo.name || '角色'" class="role-avatar" @error="handleAvatarError" />
|
||||
</div>
|
||||
<div class="role-info-section">
|
||||
<div class="role-name">{{ roleInfo.name || '未知角色' }}</div>
|
||||
<div class="role-stats">
|
||||
<span class="level-text">Lv.{{ roleInfo.level || 1 }}</span>
|
||||
<span class="power-value">战力 {{ formatPower(roleInfo.power) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-section">
|
||||
<div class="rank-icon">{{ rankInfo.icon }}</div>
|
||||
<div class="rank-title">{{ rankInfo.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="resources" v-if="hasRole">
|
||||
<div v-for="res in resList" :key="res.label" class="res-item">
|
||||
<span class="label">{{ res.label }}</span>
|
||||
<span class="value">{{ res.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="loading">正在获取角色信息...</div>
|
||||
</div>
|
||||
</div>
|
||||
<transition v-else name="drop">
|
||||
<div v-show="visible" class="identity-overlay" @click.self="emit('close')">
|
||||
<div class="identity-card">
|
||||
<div class="strap">
|
||||
<div class="strap-tape"></div>
|
||||
<div class="strap-buckle"></div>
|
||||
</div>
|
||||
<div class="card-header">
|
||||
<img src="/icons/Ob7pyorzmHiJcbab2c25af264d0758b527bc1b61cc3b.png" alt="身份牌" class="icon" />
|
||||
<div class="info">
|
||||
<h3>身份牌</h3>
|
||||
<p>角色与战力概览</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="emit('close')">✕</button>
|
||||
</div>
|
||||
<div v-if="hasRole" class="role-profile-header" :class="rankInfo.class">
|
||||
<div class="role-profile-content">
|
||||
<div class="avatar-container">
|
||||
<img :src="roleAvatar" :alt="roleInfo.name || '角色'" class="role-avatar" @error="handleAvatarError" />
|
||||
</div>
|
||||
<div class="role-info-section">
|
||||
<div class="role-name">{{ roleInfo.name || '未知角色' }}</div>
|
||||
<div class="role-stats">
|
||||
<span class="level-text">Lv.{{ roleInfo.level || 1 }}</span>
|
||||
<span class="power-value">战力 {{ formatPower(roleInfo.power) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-section">
|
||||
<div class="rank-icon">{{ rankInfo.icon }}</div>
|
||||
<div class="rank-title">{{ rankInfo.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glow-border" />
|
||||
</div>
|
||||
<div v-else class="loading">正在获取角色信息...</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
const props = defineProps<{ visible?: boolean, embedded?: boolean }>()
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const wsStatus = computed(() => {
|
||||
if (!tokenStore.selectedToken) return 'disconnected'
|
||||
return tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
})
|
||||
|
||||
const roleInfo = computed(() => {
|
||||
const gameData = tokenStore.gameData
|
||||
const role = gameData?.roleInfo?.role
|
||||
if (!role) return {}
|
||||
return {
|
||||
roleId: role.roleId,
|
||||
name: role.name,
|
||||
headImg: role.headImg,
|
||||
level: role.level,
|
||||
power: role.power || role.fighting || 0,
|
||||
gold: role.gold ?? 0,
|
||||
diamond: role.diamond ?? 0,
|
||||
fishing: role.fishing || role.fish || null,
|
||||
items: role.items || role.itemList || role.bag?.items || role.inventory || null
|
||||
}
|
||||
})
|
||||
|
||||
const hasRole = computed(() => Object.keys(roleInfo.value || {}).length > 0)
|
||||
|
||||
const defaultAvatars = [
|
||||
'/icons/1733492491706148.png',
|
||||
'/icons/1733492491706152.png',
|
||||
'/icons/1736425783912140.png',
|
||||
'/icons/173746572831736.png',
|
||||
'/icons/174023274867420.png'
|
||||
]
|
||||
const roleAvatar = ref('')
|
||||
const selectedDefaultAvatar = ref('')
|
||||
|
||||
const powerRanks = [
|
||||
{ min: 0, max: 1_000_000, title: '初出茅庐', icon: '🌱', class: 'rank-beginner' },
|
||||
{ min: 1_000_000, max: 10_000_000, title: '小有名气', icon: '⚔️', class: 'rank-known' },
|
||||
{ min: 10_000_000, max: 100_000_000, title: '出入江湖', icon: '🗡️', class: 'rank-veteran' },
|
||||
{ min: 100_000_000, max: 500_000_000, title: '纵横四方', icon: '🏹', class: 'rank-master' },
|
||||
{ min: 500_000_000, max: 2_000_000_000, title: '盖世豪杰', icon: '⚡', class: 'rank-hero' },
|
||||
{ min: 2_000_000_000, max: 4_000_000_000, title: '一方枭雄', icon: '👑', class: 'rank-overlord' },
|
||||
{ min: 4_000_000_000, max: 6_000_000_000, title: '睥睨江湖', icon: '🔱', class: 'rank-supreme' },
|
||||
{ min: 6_000_000_000, max: 9_000_000_000, title: '独霸天下', icon: '⚜️', class: 'rank-emperor' },
|
||||
{ min: 9_000_000_000, max: 15_000_000_000, title: '不世之尊', icon: '💎', class: 'rank-legend' },
|
||||
{ min: 15_000_000_000, max: Infinity, title: '无极至尊', icon: '🌟', class: 'rank-infinite' }
|
||||
]
|
||||
|
||||
const rankInfo = computed(() => {
|
||||
const power = Number(roleInfo.value.power || 0)
|
||||
return powerRanks.find(r => power >= r.min && power < r.max) || powerRanks[0]
|
||||
})
|
||||
|
||||
const formatPower = (power: number) => {
|
||||
if (!power) return '0'
|
||||
const yi = 100_000_000
|
||||
const wan = 10_000
|
||||
if (power >= yi) return (power / yi).toFixed(1) + '亿'
|
||||
if (power >= wan) return (power / wan).toFixed(1) + '万'
|
||||
return power.toLocaleString()
|
||||
}
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
const n = Number(num || 0)
|
||||
const yi = 100_000_000
|
||||
const wan = 10_000
|
||||
if (n >= yi) return (n / yi).toFixed(1) + '亿'
|
||||
if (n >= wan) return (n / wan).toFixed(1) + '万'
|
||||
return n.toLocaleString()
|
||||
}
|
||||
|
||||
const gold = computed(() => (roleInfo.value as any).gold ?? 0)
|
||||
const diamond = computed(() => (roleInfo.value as any).diamond ?? 0)
|
||||
|
||||
// ——— 从 items 中解析数量(优先)———
|
||||
const getItemCount = (items: any, id: number): number | null => {
|
||||
if (!items) return null
|
||||
// 数组结构:[{id/ itemId, num/count/quantity}, ...]
|
||||
if (Array.isArray(items)) {
|
||||
const found = items.find((it) => Number((it as any).id ?? (it as any).itemId) === id)
|
||||
if (!found) return 0
|
||||
return Number((found as any).num ?? (found as any).count ?? (found as any).quantity ?? 0)
|
||||
}
|
||||
// 对象结构:{ '1011': 3 } 或 { '1011': { num:3 } }
|
||||
const node = (items as any)[String(id)] ?? (items as any)[id]
|
||||
if (node == null) {
|
||||
// 兼容值对象中含有 itemId/quantity 的结构:{ '2001': { itemId: 2001, quantity: 6821 } } 或 { 'X': { itemId: 2001 } }
|
||||
const match = Object.values(items as any).find((v: any) => Number(v?.itemId ?? v?.id) === id)
|
||||
if (match) return Number((match as any).num ?? (match as any).count ?? (match as any).quantity ?? 0)
|
||||
return 0
|
||||
}
|
||||
if (typeof node === 'number') return Number(node)
|
||||
if (typeof node === 'object') return Number((node as any).num ?? (node as any).count ?? (node as any).quantity ?? 0)
|
||||
return Number(node) || 0
|
||||
}
|
||||
|
||||
const items = computed(() => (roleInfo.value as any).items)
|
||||
|
||||
// 参考表:1011 普通鱼竿,1012 金鱼竿;补充:108 珍珠、1001 招募令、1006 精铁、1023 彩玉、1003 进阶石
|
||||
const normalRodFromItems = computed(() => getItemCount(items.value, 1011))
|
||||
const goldRodFromItems = computed(() => getItemCount(items.value, 1012))
|
||||
const pearlFromItems = computed(() => getItemCount(items.value, 108))
|
||||
const recruitFromItems = computed(() => getItemCount(items.value, 1001))
|
||||
const ironFromItems = computed(() => getItemCount(items.value, 1006))
|
||||
const jadeFromItems = computed(() => getItemCount(items.value, 1023))
|
||||
const advanceStoneFromItems = computed(() => getItemCount(items.value, 1003))
|
||||
|
||||
// 兼容旧字段(fishing.*)作为回退
|
||||
const normalRod = computed(() => {
|
||||
const fromItems = normalRodFromItems.value
|
||||
if (fromItems !== null && fromItems !== undefined) return fromItems
|
||||
return (roleInfo.value as any)?.fishing?.normalRod ?? (roleInfo.value as any)?.fishing?.rod ?? null
|
||||
})
|
||||
const goldRod = computed(() => {
|
||||
const fromItems = goldRodFromItems.value
|
||||
if (fromItems !== null && fromItems !== undefined) return fromItems
|
||||
return (roleInfo.value as any)?.fishing?.goldRod ?? (roleInfo.value as any)?.fishing?.vipRod ?? null
|
||||
})
|
||||
const display = (n: number | null | undefined) => n == null ? '-' : formatNumber(Number(n))
|
||||
const resList = computed(() => [
|
||||
{ label: '金币', value: formatNumber(gold.value) },
|
||||
{ label: '金砖', value: formatNumber(diamond.value) },
|
||||
{ label: '普通鱼竿', value: display(normalRod.value as any) },
|
||||
{ label: '金鱼竿', value: display(goldRod.value as any) },
|
||||
{ label: '珍珠', value: display(pearlFromItems.value as any) },
|
||||
{ label: '招募令', value: display(recruitFromItems.value as any) },
|
||||
{ label: '精铁', value: display(ironFromItems.value as any) },
|
||||
{ label: '彩玉', value: display(jadeFromItems.value as any) },
|
||||
{ label: '进阶石', value: display(advanceStoneFromItems.value as any) }
|
||||
])
|
||||
|
||||
const initializeAvatar = () => {
|
||||
if (roleInfo.value && (roleInfo.value as any).headImg) {
|
||||
roleAvatar.value = (roleInfo.value as any).headImg
|
||||
} else {
|
||||
if (!selectedDefaultAvatar.value) {
|
||||
const key = (roleInfo.value as any).roleId || (roleInfo.value as any).name || 'default'
|
||||
const hash = Array.from(String(key)).reduce((acc, ch) => acc + ch.charCodeAt(0), 0)
|
||||
selectedDefaultAvatar.value = defaultAvatars[hash % defaultAvatars.length]
|
||||
}
|
||||
roleAvatar.value = selectedDefaultAvatar.value
|
||||
}
|
||||
}
|
||||
|
||||
const handleAvatarError = () => {
|
||||
if (!selectedDefaultAvatar.value) {
|
||||
const idx = Math.floor(Math.random() * defaultAvatars.length)
|
||||
selectedDefaultAvatar.value = defaultAvatars[idx]
|
||||
}
|
||||
roleAvatar.value = selectedDefaultAvatar.value
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
initializeAvatar()
|
||||
if (tokenStore.selectedToken && wsStatus.value === 'connected') {
|
||||
try { await tokenStore.sendMessage(tokenStore.selectedToken.id, 'role_getroleinfo') } catch {}
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => roleInfo.value, initializeAvatar, { deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.identity-embedded { grid-column: 1 / -1; }
|
||||
.identity-card.embedded { width: 100%; position: relative; background: linear-gradient(180deg, var(--bg-primary), var(--bg-secondary)); border-radius: var(--border-radius-xl); padding: var(--spacing-lg); box-shadow: none; border: 1px solid var(--border-light); }
|
||||
|
||||
.identity-overlay { position: fixed; inset: 0; z-index: 2000; background: transparent; }
|
||||
|
||||
.identity-card { position: fixed; top: 0; right: 16px; width: 360px; background: var(--bg-primary); border-radius: var(--border-radius-xl); padding: var(--spacing-lg); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||
|
||||
/* 下落动画 */
|
||||
.drop-enter-from .identity-card { transform: translateY(-120%); }
|
||||
.drop-enter-active .identity-card, .drop-leave-active .identity-card { transition: transform .35s ease; }
|
||||
.drop-enter-to .identity-card { transform: translateY(0); }
|
||||
.drop-leave-to .identity-card { transform: translateY(-120%); }
|
||||
|
||||
.strap { position: absolute; top: -64px; right: 24px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
||||
.strap-tape { width: 22px; height: 56px; background: linear-gradient(180deg,#f59e0b,#fbbf24); border-radius: 6px; box-shadow: inset 0 -4px rgba(0,0,0,.15); }
|
||||
.strap-buckle { width: 36px; height: 18px; background: #6b4f2a; border-radius: 9px; box-shadow: inset 0 -2px rgba(0,0,0,.2); }
|
||||
.card-header { display: flex; align-items: center; gap: var(--spacing-md); margin-bottom: var(--spacing-md); }
|
||||
.icon { width: 32px; height: 32px; object-fit: contain; }
|
||||
.info h3 { margin: 0; font-size: var(--font-size-md); font-weight: var(--font-weight-semibold); }
|
||||
.info p { margin: 0; color: var(--text-secondary); font-size: var(--font-size-sm); }
|
||||
.close-btn { margin-left: auto; background: transparent; border: none; font-size: 16px; cursor: pointer; color: var(--text-secondary); }
|
||||
|
||||
.role-profile-header { position: relative; border-radius: var(--border-radius-large); padding: 16px; overflow: hidden; }
|
||||
.role-profile-content { position: relative; display: flex; align-items: center; gap: 16px; z-index: 3; }
|
||||
.avatar-container { width: 56px; height: 56px; flex-shrink: 0; }
|
||||
.role-avatar { width: 56px; height: 56px; border-radius: 12px; object-fit: cover; border: 2px solid rgba(255,255,255,0.6); }
|
||||
.role-name { font-weight: var(--font-weight-semibold); font-size: var(--font-size-md); }
|
||||
.role-stats { color: var(--text-secondary); font-size: var(--font-size-sm); display: flex; gap: 12px; }
|
||||
.rank-section { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
||||
.rank-icon { font-size: 14px; }
|
||||
.rank-title { font-size: 12px; font-weight: var(--font-weight-semibold); }
|
||||
|
||||
.glow-border { position: absolute; inset: 0; border-radius: var(--border-radius-large); background: linear-gradient(45deg, rgba(102,126,234,.4), rgba(118,75,162,.4), rgba(254,202,87,.4), rgba(102,126,234,.4)); background-size: 300% 300%; opacity: 0.6; z-index: 1; animation: glowAnimation 6s ease-in-out infinite; }
|
||||
@keyframes glowAnimation { 0%,100% { background-position: 0% 50% } 50% { background-position: 100% 50% } }
|
||||
|
||||
.rank-beginner .role-profile-header { background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%); }
|
||||
.rank-known .role-profile-header { background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); }
|
||||
.rank-veteran .role-profile-header { background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); }
|
||||
.rank-master .role-profile-header { background: linear-gradient(135deg, #e9d5ff 0%, #ddd6fe 100%); }
|
||||
.rank-hero .role-profile-header { background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); }
|
||||
.rank-overlord .role-profile-header { background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%); }
|
||||
.rank-supreme .role-profile-header { background: linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%); }
|
||||
.rank-emperor .role-profile-header { background: linear-gradient(135deg, #fee2e2 0%, #dc2626 20%); }
|
||||
.rank-legend .role-profile-header { background: linear-gradient(135deg, #ede9fe 0%, #7c3aed 30%); }
|
||||
.rank-infinite .role-profile-header { background: linear-gradient(135deg, #fef3c7 0%, #fbbf24 30%, #f59e0b 100%); }
|
||||
|
||||
.resources { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-top: 10px; }
|
||||
.res-item { background: var(--bg-primary); border: 1px solid var(--border-light); border-radius: 10px; padding: 8px 10px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.res-item .label { color: var(--text-secondary); font-size: 12px; }
|
||||
.res-item .value { font-weight: var(--font-weight-semibold); }
|
||||
</style>
|
||||
957
xyzw_web_helper-main开源源码更新/src/components/MessageTester.vue
Normal file
@@ -0,0 +1,957 @@
|
||||
<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>
|
||||
<n-button
|
||||
type="info"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
@click="testBONDecoding"
|
||||
>
|
||||
🔓 测试BON解码
|
||||
</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">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span>消息历史</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<n-button
|
||||
size="small"
|
||||
type="error"
|
||||
secondary
|
||||
@click="clearHistory"
|
||||
:disabled="messageHistory.length === 0"
|
||||
>
|
||||
<n-icon size="14" class="mr-1">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"/></svg>
|
||||
</n-icon>
|
||||
清空
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="info"
|
||||
secondary
|
||||
@click="exportHistory"
|
||||
:disabled="messageHistory.length === 0"
|
||||
>
|
||||
<n-icon size="14" class="mr-1">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/></svg>
|
||||
</n-icon>
|
||||
导出
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</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' :
|
||||
message.type === 'test' ? 'bg-purple-50 border-purple-200' : 'bg-green-50 border-green-200'"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-semibold">
|
||||
{{ message.type === 'sent' ? '📤 发送' : message.type === 'test' ? '🧪 测试' : '📨 接收' }}
|
||||
<span class="text-sm text-gray-500 ml-2">{{ formatTime(message.timestamp) }}</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="tertiary"
|
||||
@click="copyMessage(message)"
|
||||
title="复制消息"
|
||||
>
|
||||
<n-icon size="12">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
|
||||
</n-icon>
|
||||
</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="tertiary"
|
||||
@click="copyJSON(message.data)"
|
||||
title="复制JSON数据"
|
||||
>
|
||||
<n-icon size="12">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M5,3H7V5H5V10A2,2 0 0,1 3,8V6A2,2 0 0,1 5,4V3M19,3V4A2,2 0 0,1 21,6V8A2,2 0 0,1 19,10V5H17V3H19M16,12A2,2 0 0,1 18,10H20A2,2 0 0,1 22,12A2,2 0 0,1 20,14H18A2,2 0 0,1 16,12M20,12V14H18V12H20M4,10A2,2 0 0,1 6,12A2,2 0 0,1 4,14H2A2,2 0 0,1 0,12A2,2 0 0,1 2,10H4M2,12V10H4V12H2M5,19V21H7V19H5V14A2,2 0 0,1 3,16V18A2,2 0 0,1 5,20V19M19,19V20A2,2 0 0,1 17,18V16A2,2 0 0,1 19,14V19H21V21H19Z"/></svg>
|
||||
</n-icon>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.cmd"
|
||||
class="text-sm mb-2"
|
||||
>
|
||||
<strong>命令:</strong>
|
||||
<n-tag size="small" :type="getCommandTagType(message.cmd)">{{ message.cmd }}</n-tag>
|
||||
</div>
|
||||
|
||||
<!-- 消息预览 -->
|
||||
<div class="mb-2">
|
||||
<div class="text-xs text-gray-600 mb-1">
|
||||
消息预览 ({{ getDataSize(message.data) }}):
|
||||
</div>
|
||||
<div class="text-sm bg-gray-50 p-2 rounded border max-h-20 overflow-hidden message-preview">
|
||||
{{ getMessagePreview(message.data) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<n-collapse>
|
||||
<n-collapse-item
|
||||
:title="`详细数据 (${getDataSize(message.data)})`"
|
||||
name="detail"
|
||||
>
|
||||
<!-- 原始数据和解码数据的选项卡 -->
|
||||
<n-tabs type="card" size="small" animated>
|
||||
<n-tab-pane name="formatted" display-directive="show:lazy">
|
||||
<template #tab>
|
||||
<n-icon size="14" class="mr-1">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M14,17H7V15H14M17,13H7V11H17M17,9H7V7H17M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z"/></svg>
|
||||
</n-icon>
|
||||
格式化显示
|
||||
</template>
|
||||
<div class="json-display-container">
|
||||
<div class="json-header">
|
||||
<n-space size="small">
|
||||
<n-tag size="small" type="info">格式化</n-tag>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
ghost
|
||||
@click="copyFormattedJSON(message.data)"
|
||||
title="复制格式化JSON"
|
||||
>
|
||||
<n-icon size="12" class="mr-1">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
|
||||
</n-icon>
|
||||
复制
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<pre class="json-content formatted">{{ formatJSONSmart(message.data) }}</pre>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="raw" display-directive="show:lazy">
|
||||
<template #tab>
|
||||
<n-icon size="14" class="mr-1">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.22,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.22,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"/></svg>
|
||||
</n-icon>
|
||||
原始数据
|
||||
</template>
|
||||
<div class="json-display-container">
|
||||
<div class="json-header">
|
||||
<n-space size="small">
|
||||
<n-tag size="small" type="warning">原始</n-tag>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="warning"
|
||||
ghost
|
||||
@click="copyRawJSON(message.data)"
|
||||
title="复制原始JSON"
|
||||
>
|
||||
<n-icon size="12" class="mr-1">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
|
||||
</n-icon>
|
||||
复制
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<pre class="json-content raw">{{ JSON.stringify(message.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="compact" display-directive="show:lazy">
|
||||
<template #tab>
|
||||
<n-icon size="14" class="mr-1">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M4,6H20V16H4M20,18A2,2 0 0,0 22,16V6C22,4.89 21.1,4 20,4H4C2.89,4 2,4.89 2,6V16A2,2 0 0,0 4,18H0V20H24V18H20Z"/></svg>
|
||||
</n-icon>
|
||||
紧凑显示
|
||||
</template>
|
||||
<div class="json-display-container">
|
||||
<div class="json-header">
|
||||
<n-space size="small">
|
||||
<n-tag size="small" type="success">紧凑</n-tag>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="success"
|
||||
ghost
|
||||
@click="copyCompactJSON(message.data)"
|
||||
title="复制紧凑JSON"
|
||||
>
|
||||
<n-icon size="12" class="mr-1">
|
||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>
|
||||
</n-icon>
|
||||
复制
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<pre class="json-content compact">{{ JSON.stringify(message.data) }}</pre>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="messageHistory.length === 0"
|
||||
class="text-center text-gray-500 p-8"
|
||||
>
|
||||
<div class="text-lg mb-2">📭</div>
|
||||
<div>暂无消息历史</div>
|
||||
<div class="text-xs mt-1">发送消息后将在此显示</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useTokenStore } from '../stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const selectedTokenId = ref(null)
|
||||
const customCmd = ref('')
|
||||
const customBody = ref('{}')
|
||||
const messageHistory = ref([])
|
||||
const lastProcessedMessage = ref(null) // 追踪最后处理的消息
|
||||
|
||||
// 计算属性
|
||||
const tokenOptions = computed(() => {
|
||||
return tokenStore.gameTokens.map(token => ({
|
||||
label: token.name,
|
||||
value: token.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) {
|
||||
message.error('请先选择一个token')
|
||||
return
|
||||
}
|
||||
|
||||
const token = tokenStore.gameTokens.find(t => t.id === selectedTokenId.value)
|
||||
if (token) {
|
||||
console.log('🔧 MessageTester: 开始连接WebSocket', {
|
||||
tokenId: selectedTokenId.value,
|
||||
tokenName: token.name,
|
||||
hasToken: !!token.token
|
||||
})
|
||||
|
||||
try {
|
||||
tokenStore.selectToken(selectedTokenId.value)
|
||||
message.success('正在建立WebSocket连接...')
|
||||
} catch (error) {
|
||||
console.error('❌ MessageTester: WebSocket连接失败', error)
|
||||
message.error('WebSocket连接失败: ' + error.message)
|
||||
}
|
||||
} else {
|
||||
message.error('找不到选中的token')
|
||||
}
|
||||
}
|
||||
|
||||
const testBONDecoding = async () => {
|
||||
try {
|
||||
// 导入BON协议
|
||||
const { g_utils } = await import('../utils/bonProtocol.js')
|
||||
|
||||
// 测试一些简单的数据
|
||||
const testData = new Uint8Array([8, 2, 5, 4, 114, 111, 108, 101])
|
||||
|
||||
console.log('🧪 BON解码测试开始')
|
||||
console.log('🔍 g_utils可用性检查:', {
|
||||
hasGUtils: !!g_utils,
|
||||
hasBon: !!(g_utils && g_utils.bon),
|
||||
hasBonDecode: !!(g_utils && g_utils.bon && g_utils.bon.decode)
|
||||
})
|
||||
|
||||
if (g_utils && g_utils.bon && g_utils.bon.decode) {
|
||||
console.log('📥 测试数据:', testData)
|
||||
const decoded = g_utils.bon.decode(testData)
|
||||
console.log('✅ BON解码成功:', decoded)
|
||||
message.success(`BON解码器工作正常: ${JSON.stringify(decoded)}`)
|
||||
|
||||
// 添加测试结果到历史
|
||||
addToHistory('test', {
|
||||
testType: 'BON解码测试',
|
||||
input: Array.from(testData),
|
||||
output: decoded,
|
||||
status: 'success'
|
||||
}, 'bon_decode_test')
|
||||
} else {
|
||||
console.error('❌ BON解码器不可用')
|
||||
message.error('BON解码器不可用')
|
||||
|
||||
// 添加错误结果到历史
|
||||
addToHistory('test', {
|
||||
testType: 'BON解码测试',
|
||||
error: 'BON解码器不可用',
|
||||
status: 'error'
|
||||
}, 'bon_decode_test')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ BON解码测试失败:', error)
|
||||
message.error('BON解码测试失败: ' + error.message)
|
||||
|
||||
// 添加错误结果到历史
|
||||
addToHistory('test', {
|
||||
testType: 'BON解码测试',
|
||||
error: error.message,
|
||||
status: 'error'
|
||||
}, 'bon_decode_test')
|
||||
}
|
||||
}
|
||||
|
||||
const addToHistory = (type, data, cmd = null) => {
|
||||
// 过滤心跳消息 (但保留test类型)
|
||||
if (type !== 'test' && (cmd === '_sys/ack' || cmd === 'heartbeat')) {
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
// 不记录心跳消息到历史
|
||||
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 getCommandTagType = (cmd) => {
|
||||
if (!cmd) return 'default'
|
||||
if (cmd.includes('error') || cmd.includes('fail')) return 'error'
|
||||
if (cmd.includes('resp') || cmd.includes('response')) return 'success'
|
||||
if (cmd.includes('get') || cmd.includes('info')) return 'info'
|
||||
if (cmd.includes('send') || cmd.includes('start')) return 'primary'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
const getDataSize = (data) => {
|
||||
try {
|
||||
const jsonStr = JSON.stringify(data)
|
||||
const sizeInBytes = new TextEncoder().encode(jsonStr).length
|
||||
if (sizeInBytes < 1024) return `${sizeInBytes}B`
|
||||
if (sizeInBytes < 1024 * 1024) return `${(sizeInBytes / 1024).toFixed(1)}KB`
|
||||
return `${(sizeInBytes / 1024 / 1024).toFixed(1)}MB`
|
||||
} catch {
|
||||
return '未知大小'
|
||||
}
|
||||
}
|
||||
|
||||
const getMessagePreview = (data) => {
|
||||
if (!data) return '空数据'
|
||||
|
||||
try {
|
||||
// 先检查是否有解码后的数据
|
||||
let previewData = data
|
||||
|
||||
if (data._raw?.decodedBody || data.decodedBody) {
|
||||
previewData = data._raw?.decodedBody || data.decodedBody
|
||||
} else if (data._raw?.rawData || data.rawData) {
|
||||
previewData = data._raw?.rawData || data.rawData
|
||||
}
|
||||
|
||||
const preview = JSON.stringify(previewData)
|
||||
return preview.length > 150 ? preview.substring(0, 150) + '...' : preview
|
||||
} catch {
|
||||
return '数据解析失败'
|
||||
}
|
||||
}
|
||||
|
||||
// 清空历史
|
||||
const clearHistory = () => {
|
||||
messageHistory.value = []
|
||||
lastProcessedMessage.value = null
|
||||
message.success('消息历史已清空')
|
||||
}
|
||||
|
||||
// 导出历史
|
||||
const exportHistory = () => {
|
||||
try {
|
||||
const exportData = {
|
||||
exportTime: new Date().toISOString(),
|
||||
tokenId: selectedTokenId.value,
|
||||
messages: messageHistory.value
|
||||
}
|
||||
|
||||
const dataStr = JSON.stringify(exportData, null, 2)
|
||||
const blob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `message-history-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
message.success('消息历史已导出')
|
||||
} catch (error) {
|
||||
message.error('导出失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制相关方法
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
message.success('已复制到剪贴板')
|
||||
} catch (error) {
|
||||
// 降级方案
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
message.success('已复制到剪贴板')
|
||||
}
|
||||
}
|
||||
|
||||
const copyMessage = (msg) => {
|
||||
const text = `[${msg.type.toUpperCase()}] ${formatTime(msg.timestamp)} - ${msg.cmd || '无命令'}\n${JSON.stringify(msg.data, null, 2)}`
|
||||
copyToClipboard(text)
|
||||
}
|
||||
|
||||
const copyJSON = (data) => {
|
||||
copyToClipboard(JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
const copyFormattedJSON = (data) => {
|
||||
copyToClipboard(formatJSONSmart(data))
|
||||
}
|
||||
|
||||
const copyRawJSON = (data) => {
|
||||
copyToClipboard(JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
const copyCompactJSON = (data) => {
|
||||
copyToClipboard(JSON.stringify(data))
|
||||
}
|
||||
|
||||
// 辅助方法:格式化body描述
|
||||
const formatBodyDescription = (body) => {
|
||||
if (!body) return 'null'
|
||||
if (Array.isArray(body)) return `[Array: ${body.length} items]`
|
||||
if (body instanceof Uint8Array) return `[Uint8Array: ${body.length} bytes]`
|
||||
if (typeof body === 'object' && body.constructor === Object) {
|
||||
const keys = Object.keys(body)
|
||||
if (keys.every(key => !isNaN(parseInt(key)))) {
|
||||
return `[NumericObject: ${keys.length} entries]`
|
||||
}
|
||||
}
|
||||
return '[Unknown format]'
|
||||
}
|
||||
|
||||
// 辅助方法:判断是否是原始body数据
|
||||
const isRawBodyData = (body) => {
|
||||
if (!body) return false
|
||||
if (Array.isArray(body)) return true
|
||||
if (body instanceof Uint8Array) return true
|
||||
if (typeof body === 'object' && body.constructor === Object) {
|
||||
const keys = Object.keys(body)
|
||||
return keys.length > 0 && keys.every(key => !isNaN(parseInt(key)))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 智能JSON格式化 - 不会截断数据
|
||||
const formatJSONSmart = (data, maxDepth = 10, currentDepth = 0) => {
|
||||
try {
|
||||
if (!data) return 'null'
|
||||
|
||||
// 防止无限递归
|
||||
if (currentDepth > maxDepth) {
|
||||
return '[超出最大深度限制]'
|
||||
}
|
||||
|
||||
// 处理BON解码数据:优先显示解码后的数据
|
||||
let displayData = data
|
||||
|
||||
// 检查_raw结构中的解码数据
|
||||
const actualData = data._raw || data
|
||||
|
||||
// 如果有解码后的数据,优先显示
|
||||
if (actualData.decodedBody || data.decodedBody) {
|
||||
const decodedBody = actualData.decodedBody || data.decodedBody
|
||||
const originalBody = actualData.body || data.body
|
||||
|
||||
if (data._raw) {
|
||||
// 如果有_raw结构,更新_raw中的body
|
||||
displayData = {
|
||||
...data,
|
||||
_raw: {
|
||||
...data._raw,
|
||||
body: decodedBody,
|
||||
_originalBody: formatBodyDescription(originalBody),
|
||||
_note: 'body已自动BON解码'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接结构,更新body
|
||||
displayData = {
|
||||
...data,
|
||||
body: decodedBody,
|
||||
_originalBody: formatBodyDescription(originalBody),
|
||||
_note: 'body已自动BON解码'
|
||||
}
|
||||
}
|
||||
} else if (actualData.rawData || data.rawData) {
|
||||
// 如果是ProtoMsg格式,使用rawData
|
||||
const rawData = actualData.rawData || data.rawData
|
||||
|
||||
if (data._raw) {
|
||||
displayData = {
|
||||
...data,
|
||||
_raw: {
|
||||
...data._raw,
|
||||
body: rawData,
|
||||
_note: 'body已使用rawData解码'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
displayData = {
|
||||
...data,
|
||||
body: rawData,
|
||||
_note: 'body已使用rawData解码'
|
||||
}
|
||||
}
|
||||
} else if ((actualData.body && isRawBodyData(actualData.body)) || (data.body && isRawBodyData(data.body))) {
|
||||
// 如果body是原始数据,添加提示
|
||||
displayData = {
|
||||
...data,
|
||||
_note: 'body为原始数据,可能需要BON解码'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理循环引用的JSON序列化,但不截断
|
||||
const seen = new WeakSet()
|
||||
const replacer = (key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return '[循环引用]'
|
||||
}
|
||||
seen.add(value)
|
||||
}
|
||||
|
||||
// 不截断字符串,完整显示
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
|
||||
// 不截断大数组,完整显示
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const jsonString = JSON.stringify(displayData, replacer, 2)
|
||||
|
||||
// 不限制总体输出长度,完整返回
|
||||
return jsonString
|
||||
} catch (error) {
|
||||
return `[JSON序列化错误: ${error.message}]`
|
||||
}
|
||||
}
|
||||
|
||||
// 保留原来的formatJSON作为兼容
|
||||
const formatJSON = (data) => {
|
||||
try {
|
||||
if (!data) return 'null'
|
||||
|
||||
// 处理BON解码数据:优先显示解码后的数据
|
||||
let displayData = data
|
||||
|
||||
// 检查_raw结构中的解码数据
|
||||
const actualData = data._raw || data
|
||||
|
||||
// 如果有解码后的数据,优先显示
|
||||
if (actualData.decodedBody || data.decodedBody) {
|
||||
const decodedBody = actualData.decodedBody || data.decodedBody
|
||||
const originalBody = actualData.body || data.body
|
||||
|
||||
if (data._raw) {
|
||||
// 如果有_raw结构,更新_raw中的body
|
||||
displayData = {
|
||||
...data,
|
||||
_raw: {
|
||||
...data._raw,
|
||||
body: decodedBody,
|
||||
_originalBody: formatBodyDescription(originalBody),
|
||||
_note: 'body已自动BON解码'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接结构,更新body
|
||||
displayData = {
|
||||
...data,
|
||||
body: decodedBody,
|
||||
_originalBody: formatBodyDescription(originalBody),
|
||||
_note: 'body已自动BON解码'
|
||||
}
|
||||
}
|
||||
} else if (actualData.rawData || data.rawData) {
|
||||
// 如果是ProtoMsg格式,使用rawData
|
||||
const rawData = actualData.rawData || data.rawData
|
||||
|
||||
if (data._raw) {
|
||||
displayData = {
|
||||
...data,
|
||||
_raw: {
|
||||
...data._raw,
|
||||
body: rawData,
|
||||
_note: 'body已使用rawData解码'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
displayData = {
|
||||
...data,
|
||||
body: rawData,
|
||||
_note: 'body已使用rawData解码'
|
||||
}
|
||||
}
|
||||
} else if ((actualData.body && isRawBodyData(actualData.body)) || (data.body && isRawBodyData(data.body))) {
|
||||
// 如果body是原始数据,添加提示
|
||||
displayData = {
|
||||
...data,
|
||||
_note: 'body为原始数据,等待BON解码'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理循环引用和大型对象的JSON序列化
|
||||
const seen = new WeakSet()
|
||||
const replacer = (key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return '[循环引用]'
|
||||
}
|
||||
seen.add(value)
|
||||
}
|
||||
|
||||
// 限制字符串长度
|
||||
if (typeof value === 'string' && value.length > 200) {
|
||||
return value.substring(0, 200) + '...[截断]'
|
||||
}
|
||||
|
||||
// 处理大数组显示
|
||||
if (Array.isArray(value) && value.length > 50) {
|
||||
return `[Array: ${value.length} items] ${JSON.stringify(value.slice(0, 10))}...[显示前10项]`
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const jsonString = JSON.stringify(displayData, replacer, 2)
|
||||
|
||||
// 限制总体输出长度
|
||||
if (jsonString.length > 5000) {
|
||||
return jsonString.substring(0, 5000) + '\n...[内容过长已截断]'
|
||||
}
|
||||
|
||||
return jsonString
|
||||
} catch (error) {
|
||||
return `[JSON序列化错误: ${error.message}]`
|
||||
}
|
||||
}
|
||||
|
||||
// 监听WebSocket消息
|
||||
watch(() => tokenStore.wsConnections, (connections) => {
|
||||
if (!selectedTokenId.value || !connections[selectedTokenId.value]) return
|
||||
|
||||
const connection = connections[selectedTokenId.value]
|
||||
if (connection.lastMessage) {
|
||||
const lastMessage = connection.lastMessage
|
||||
|
||||
// 避免重复处理相同的消息
|
||||
if (lastProcessedMessage.value &&
|
||||
lastProcessedMessage.value.timestamp === lastMessage.timestamp) {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用实际的消息数据而不是简化的数据结构
|
||||
const messageData = lastMessage.data || lastMessage
|
||||
const cmd = messageData.cmd || lastMessage.cmd
|
||||
|
||||
// 过滤心跳消息
|
||||
if (cmd && cmd !== '_sys/ack' && cmd !== 'heartbeat') {
|
||||
addToHistory('received', messageData, cmd)
|
||||
lastProcessedMessage.value = lastMessage
|
||||
}
|
||||
}
|
||||
}, { 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);
|
||||
}
|
||||
|
||||
/* 优化滚动条样式 */
|
||||
.message-history::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.message-history::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message-history::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message-history::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 优化代码块样式 */
|
||||
pre {
|
||||
font-family: 'Fira Code', 'Monaco', 'Consolas', 'Ubuntu Mono', monospace;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 选项卡内的预览区域 */
|
||||
.n-tabs .n-tab-pane {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 复制按钮悬停效果 */
|
||||
.n-button[title*="复制"]:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 消息预览区域样式 */
|
||||
.message-preview {
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 限制最大高度,添加滚动 */
|
||||
.max-h-80 {
|
||||
max-height: 20rem;
|
||||
}
|
||||
|
||||
.max-h-20 {
|
||||
max-height: 5rem;
|
||||
}
|
||||
|
||||
/* 提升标签的可读性 */
|
||||
.n-tag {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
692
xyzw_web_helper-main开源源码更新/src/components/RoleProfileCard.vue
Normal file
@@ -0,0 +1,692 @@
|
||||
<template>
|
||||
<div v-if="roleInfo && Object.keys(roleInfo).length > 0" class="role-profile-card" :class="rankInfo.class">
|
||||
<div class="role-profile-content">
|
||||
<!-- 头像区域 -->
|
||||
<div class="avatar-container">
|
||||
<img
|
||||
:src="roleAvatar"
|
||||
:alt="roleInfo.name || '角色'"
|
||||
class="role-avatar"
|
||||
@error="handleAvatarError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 角色信息区域 -->
|
||||
<div class="role-info-section">
|
||||
<div class="role-name">{{ roleInfo.name || '未知角色' }}</div>
|
||||
<div class="role-stats">
|
||||
<span class="level-text">Lv.{{ roleInfo.level || 1 }}</span>
|
||||
<span class="power-value">战力 {{ formatPower(roleInfo.power) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 段位信息 -->
|
||||
<div class="rank-section">
|
||||
<div class="rank-icon">{{ rankInfo.icon }}</div>
|
||||
<div class="rank-title">{{ rankInfo.title }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-section">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: progressPercentage + '%' }"
|
||||
:class="rankInfo.class"
|
||||
></div>
|
||||
</div>
|
||||
<div class="progress-text">
|
||||
<span v-if="nextRankThreshold">{{ progressPercentage }}%</span>
|
||||
<span v-else>MAX</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 炫光边框 -->
|
||||
<div class="glow-border"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 默认头像列表(当角色头像为空时随机选择)
|
||||
const defaultAvatars = [
|
||||
'/icons/1733492491706148.png',
|
||||
'/icons/1733492491706152.png',
|
||||
'/icons/1736425783912140.png',
|
||||
'/icons/173746572831736.png',
|
||||
'/icons/174023274867420.png'
|
||||
]
|
||||
|
||||
const roleAvatar = ref('')
|
||||
const selectedDefaultAvatar = ref('')
|
||||
|
||||
// 战力段位配置
|
||||
const powerRanks = [
|
||||
{
|
||||
min: 0,
|
||||
max: 1000000,
|
||||
title: '初出茅庐',
|
||||
description: '初登江湖,尚显青涩。',
|
||||
icon: '🌱',
|
||||
class: 'rank-beginner',
|
||||
color: '#6b7280'
|
||||
},
|
||||
{
|
||||
min: 1000000,
|
||||
max: 10000000,
|
||||
title: '小有名气',
|
||||
description: '已有名声,立足江湖。',
|
||||
icon: '⚔️',
|
||||
class: 'rank-known',
|
||||
color: '#10b981'
|
||||
},
|
||||
{
|
||||
min: 10000000,
|
||||
max: 100000000,
|
||||
title: '出入江湖',
|
||||
description: '身经百战,渐成人物。',
|
||||
icon: '🗡️',
|
||||
class: 'rank-veteran',
|
||||
color: '#3b82f6'
|
||||
},
|
||||
{
|
||||
min: 100000000,
|
||||
max: 500000000,
|
||||
title: '纵横四方',
|
||||
description: '武艺精进,名震一域。',
|
||||
icon: '🏹',
|
||||
class: 'rank-master',
|
||||
color: '#8b5cf6'
|
||||
},
|
||||
{
|
||||
min: 500000000,
|
||||
max: 2000000000,
|
||||
title: '盖世豪杰',
|
||||
description: '豪迈英勇,威震四方。',
|
||||
icon: '⚡',
|
||||
class: 'rank-hero',
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
min: 2000000000,
|
||||
max: 4000000000,
|
||||
title: '一方枭雄',
|
||||
description: '才智兼备,呼风唤雨。',
|
||||
icon: '👑',
|
||||
class: 'rank-overlord',
|
||||
color: '#ef4444'
|
||||
},
|
||||
{
|
||||
min: 4000000000,
|
||||
max: 6000000000,
|
||||
title: '睥睨江湖',
|
||||
description: '实力深不可测,世人仰望。',
|
||||
icon: '🔱',
|
||||
class: 'rank-supreme',
|
||||
color: '#ec4899'
|
||||
},
|
||||
{
|
||||
min: 6000000000,
|
||||
max: 9000000000,
|
||||
title: '独霸天下',
|
||||
description: '威势登峰造极,号令天下。',
|
||||
icon: '⚜️',
|
||||
class: 'rank-emperor',
|
||||
color: '#dc2626'
|
||||
},
|
||||
{
|
||||
min: 9000000000,
|
||||
max: 15000000000,
|
||||
title: '不世之尊',
|
||||
description: '超凡入圣,江湖传说。',
|
||||
icon: '💎',
|
||||
class: 'rank-legend',
|
||||
color: '#7c3aed'
|
||||
},
|
||||
{
|
||||
min: 15000000000,
|
||||
max: Infinity,
|
||||
title: '无极至尊',
|
||||
description: '超越传说,无人能及。',
|
||||
icon: '🌟',
|
||||
class: 'rank-infinite',
|
||||
color: '#fbbf24'
|
||||
}
|
||||
]
|
||||
|
||||
// 角色信息计算属性
|
||||
const roleInfo = computed(() => {
|
||||
const gameData = tokenStore.gameData
|
||||
if (gameData && gameData.roleInfo && gameData.roleInfo.role) {
|
||||
const role = gameData.roleInfo.role
|
||||
return {
|
||||
roleId: role.roleId,
|
||||
name: role.name,
|
||||
headImg: role.headImg,
|
||||
level: role.level,
|
||||
power: role.power || role.fighting || 0, // 使用power或fighting字段作为战力
|
||||
exp: role.exp,
|
||||
vip: role.vip,
|
||||
diamond: role.diamond,
|
||||
gold: role.gold,
|
||||
energy: role.energy,
|
||||
maxEnergy: role.maxEnergy
|
||||
}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
// 计算当前段位信息
|
||||
const rankInfo = computed(() => {
|
||||
const power = roleInfo.value.power || 0
|
||||
const rank = powerRanks.find(rank => power >= rank.min && power < rank.max)
|
||||
return rank || powerRanks[0]
|
||||
})
|
||||
|
||||
// 计算下一个段位门槛
|
||||
const nextRankThreshold = computed(() => {
|
||||
const currentRankIndex = powerRanks.findIndex(rank => rank === rankInfo.value)
|
||||
if (currentRankIndex >= 0 && currentRankIndex < powerRanks.length - 1) {
|
||||
return powerRanks[currentRankIndex + 1].min
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// 计算当前段位的进度百分比
|
||||
const progressPercentage = computed(() => {
|
||||
const power = roleInfo.value.power || 0
|
||||
const currentRank = rankInfo.value
|
||||
|
||||
if (!nextRankThreshold.value) {
|
||||
return 100 // 已达最高段位
|
||||
}
|
||||
|
||||
const rangeSize = nextRankThreshold.value - currentRank.min
|
||||
const currentProgress = power - currentRank.min
|
||||
const percentage = Math.min(100, Math.max(0, (currentProgress / rangeSize) * 100))
|
||||
|
||||
return Math.round(percentage)
|
||||
})
|
||||
|
||||
// 格式化战力数值
|
||||
const formatPower = (power) => {
|
||||
if (!power || power === 0) return '0'
|
||||
|
||||
const yi = 100000000 // 1亿
|
||||
const wan = 10000 // 1万
|
||||
|
||||
if (power >= yi) {
|
||||
const value = (power / yi).toFixed(1)
|
||||
return `${value}亿`
|
||||
} else if (power >= wan) {
|
||||
const value = (power / wan).toFixed(1)
|
||||
return `${value}万`
|
||||
} else {
|
||||
return power.toLocaleString()
|
||||
}
|
||||
}
|
||||
|
||||
// 头像处理
|
||||
const initializeAvatar = () => {
|
||||
if (roleInfo.value.headImg) {
|
||||
roleAvatar.value = roleInfo.value.headImg
|
||||
} else {
|
||||
// 如果没有头像,生成一个稳定的随机头像
|
||||
if (!selectedDefaultAvatar.value) {
|
||||
const roleId = roleInfo.value.roleId || roleInfo.value.name || 'default'
|
||||
const hash = Array.from(roleId.toString()).reduce((acc, char) => {
|
||||
return acc + char.charCodeAt(0)
|
||||
}, 0)
|
||||
const index = hash % defaultAvatars.length
|
||||
selectedDefaultAvatar.value = defaultAvatars[index]
|
||||
}
|
||||
roleAvatar.value = selectedDefaultAvatar.value
|
||||
}
|
||||
}
|
||||
|
||||
// 头像加载失败处理
|
||||
const handleAvatarError = () => {
|
||||
if (!selectedDefaultAvatar.value) {
|
||||
const index = Math.floor(Math.random() * defaultAvatars.length)
|
||||
selectedDefaultAvatar.value = defaultAvatars[index]
|
||||
}
|
||||
roleAvatar.value = selectedDefaultAvatar.value
|
||||
}
|
||||
|
||||
// 初始化和数据加载
|
||||
const loadRoleData = async () => {
|
||||
if (!tokenStore.selectedToken) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
const status = tokenStore.getWebSocketStatus(tokenId)
|
||||
|
||||
if (status === 'connected') {
|
||||
// 优先请求角色信息
|
||||
try {
|
||||
await tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(async () => {
|
||||
initializeAvatar()
|
||||
await loadRoleData()
|
||||
})
|
||||
|
||||
// 监听角色信息变化
|
||||
watch(() => roleInfo.value, initializeAvatar, { deep: true, immediate: true })
|
||||
|
||||
// 监听Token变化
|
||||
watch(() => tokenStore.selectedToken, async (newToken) => {
|
||||
if (newToken) {
|
||||
await loadRoleData()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听WebSocket状态变化
|
||||
const wsStatus = computed(() => {
|
||||
if (!tokenStore.selectedToken) return 'disconnected'
|
||||
return tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
})
|
||||
|
||||
watch(wsStatus, async (newStatus) => {
|
||||
if (newStatus === 'connected' && tokenStore.selectedToken) {
|
||||
await loadRoleData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.role-profile-card {
|
||||
position: relative;
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--border-radius-large);
|
||||
background: linear-gradient(135deg,
|
||||
var(--bg-primary) 0%,
|
||||
rgba(102, 126, 234, 0.03) 100%
|
||||
);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
|
||||
.glow-border {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.role-profile-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.role-avatar {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
object-fit: cover;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.role-info-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 2px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.role-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.level-text {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.power-value {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--primary-color);
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rank-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--border-radius-medium);
|
||||
backdrop-filter: blur(10px);
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
.rank-icon {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rank-title {
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
background: linear-gradient(90deg, var(--primary-color), rgba(102, 126, 234, 0.8));
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// 炫光边框
|
||||
.glow-border {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(45deg,
|
||||
rgba(102, 126, 234, 0.4),
|
||||
rgba(118, 75, 162, 0.4),
|
||||
rgba(254, 202, 87, 0.4),
|
||||
rgba(102, 126, 234, 0.4)
|
||||
);
|
||||
background-size: 300% 300%;
|
||||
border-radius: var(--border-radius-large);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1;
|
||||
animation: glowAnimation 3s ease-in-out infinite;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: calc(var(--border-radius-large) - 2px);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowAnimation {
|
||||
0%, 100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
// 段位特定样式
|
||||
.rank-beginner {
|
||||
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, #6b7280, #9ca3af);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-known {
|
||||
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, #10b981, #34d399);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-veteran {
|
||||
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-master {
|
||||
background: linear-gradient(135deg, #e9d5ff 0%, #ddd6fe 100%);
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, #8b5cf6, #a78bfa);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-hero {
|
||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, #f59e0b, #fbbf24);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-overlord {
|
||||
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, #ef4444, #f87171);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-supreme {
|
||||
background: linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%);
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, #ec4899, #f472b6);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-emperor {
|
||||
background: linear-gradient(135deg, #fee2e2 0%, #dc2626 20%);
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, #dc2626, #ef4444);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-legend {
|
||||
background: linear-gradient(135deg, #ede9fe 0%, #7c3aed 30%);
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, #7c3aed, #8b5cf6);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-infinite {
|
||||
background: linear-gradient(135deg, #fef3c7 0%, #fbbf24 30%, #f59e0b 100%);
|
||||
animation: shimmer 3s ease-in-out infinite;
|
||||
.progress-fill {
|
||||
background: linear-gradient(90deg, #fbbf24, #f59e0b, #fbbf24);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
// 深色主题优化
|
||||
[data-theme="dark"] .role-profile-card {
|
||||
background: linear-gradient(135deg,
|
||||
var(--bg-secondary) 0%,
|
||||
rgba(102, 126, 234, 0.08) 100%
|
||||
);
|
||||
|
||||
.role-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.level-text {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.power-value {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.rank-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.rank-section {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
// 深色主题段位背景优化
|
||||
&.rank-beginner {
|
||||
background: linear-gradient(135deg, #374151 0%, #4b5563 100%);
|
||||
}
|
||||
|
||||
&.rank-known {
|
||||
background: linear-gradient(135deg, #064e3b 0%, #065f46 100%);
|
||||
}
|
||||
|
||||
&.rank-veteran {
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1d4ed8 100%);
|
||||
}
|
||||
|
||||
&.rank-master {
|
||||
background: linear-gradient(135deg, #581c87 0%, #6b21a8 100%);
|
||||
}
|
||||
|
||||
&.rank-hero {
|
||||
background: linear-gradient(135deg, #92400e 0%, #b45309 100%);
|
||||
}
|
||||
|
||||
&.rank-overlord {
|
||||
background: linear-gradient(135deg, #991b1b 0%, #dc2626 100%);
|
||||
}
|
||||
|
||||
&.rank-supreme {
|
||||
background: linear-gradient(135deg, #be185d 0%, #db2777 100%);
|
||||
}
|
||||
|
||||
&.rank-emperor {
|
||||
background: linear-gradient(135deg, #991b1b 0%, #b91c1c 100%);
|
||||
}
|
||||
|
||||
&.rank-legend {
|
||||
background: linear-gradient(135deg, #581c87 0%, #6b21a8 100%);
|
||||
}
|
||||
|
||||
&.rank-infinite {
|
||||
background: linear-gradient(135deg, #92400e 0%, #d97706 50%, #f59e0b 100%);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.role-profile-content {
|
||||
gap: var(--spacing-xs);
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.role-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.role-info-section {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.level-text, .power-value {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.rank-section {
|
||||
max-width: 70px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.rank-title {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
267
xyzw_web_helper-main开源源码更新/src/components/TeamFormation.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<div class="status-card team-formation-card">
|
||||
<div class="card-header">
|
||||
<img src="/icons/Ob7pyorzmHiJcbab2c25af264d0758b527bc1b61cc3b.png" alt="阵容" class="icon" />
|
||||
<div class="info">
|
||||
<h3>阵容</h3>
|
||||
<p>当前使用的战斗阵容</p>
|
||||
</div>
|
||||
<div class="team-selector">
|
||||
<button
|
||||
v-for="teamId in availableTeams"
|
||||
:key="teamId"
|
||||
:disabled="loading || switching"
|
||||
:class="['team-button', { active: currentTeam === teamId }]"
|
||||
@click="selectTeam(teamId)"
|
||||
>
|
||||
{{ teamId }}
|
||||
</button>
|
||||
<button class="refresh-button" :disabled="loading" title="刷新队伍数据" @click="refreshTeamData(true)">
|
||||
<svg class="refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
|
||||
<path d="M21 3v5h-5"/>
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
|
||||
<path d="M3 21v-5h5"/>
|
||||
</svg>
|
||||
<span class="refresh-text">刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="current-team-info">
|
||||
<span class="label">当前阵容</span>
|
||||
<span class="team-number">
|
||||
<template v-if="!loading">阵容 {{ currentTeam }}</template>
|
||||
<template v-else>加载中…</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="heroes-container">
|
||||
<div v-if="!loading" class="heroes-inline">
|
||||
<div v-for="hero in currentTeamHeroes" :key="hero.id || hero.name" class="hero-item">
|
||||
<div class="hero-circle">
|
||||
<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>
|
||||
</div>
|
||||
<span class="hero-name">{{ hero.name || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && !currentTeamHeroes.length" class="empty-team"><p>暂无队伍信息</p></div>
|
||||
<div v-if="loading" class="empty-team"><p>正在加载队伍信息…</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
const loading = ref(false)
|
||||
const switching = ref(false)
|
||||
const currentTeam = ref(1)
|
||||
const availableTeams = ref<number[]>([1, 2, 3, 4])
|
||||
|
||||
const HERO_DICT: Record<number, { name: string; type: string }> = {
|
||||
101: { name: '司马懿', type: '魏国' }, 102: { name: '郭嘉', type: '魏国' }, 103: { name: '关羽', type: '蜀国' },
|
||||
104: { name: '诸葛亮', type: '蜀国' }, 105: { name: '周瑜', type: '吴国' }, 106: { name: '太史慈', type: '吴国' },
|
||||
107: { name: '吕布', type: '群雄' }, 108: { name: '华佗', type: '群雄' }, 109: { name: '甄姬', type: '魏国' },
|
||||
110: { name: '黄月英', type: '蜀国' }, 111: { name: '孙策', type: '吴国' }, 112: { name: '贾诩', type: '群雄' },
|
||||
113: { name: '曹仁', type: '魏国' }, 114: { name: '姜维', type: '蜀国' }, 115: { name: '孙坚', type: '吴国' },
|
||||
116: { name: '公孙瓒', type: '群雄' }, 117: { name: '典韦', type: '魏国' }, 118: { name: '赵云', type: '蜀国' },
|
||||
119: { name: '大乔', type: '吴国' }, 120: { name: '张角', type: '群雄' }, 201: { name: '徐晃', type: '魏国' },
|
||||
202: { name: '荀彧', type: '魏国' }, 203: { name: '典韦', type: '魏国' }, 204: { name: '张飞', type: '蜀国' },
|
||||
205: { name: '赵云', type: '蜀国' }, 206: { name: '庞统', type: '蜀国' }, 207: { name: '鲁肃', type: '吴国' },
|
||||
208: { name: '陆逊', type: '吴国' }, 209: { name: '甘宁', type: '吴国' }, 210: { name: '貂蝉', type: '群雄' },
|
||||
211: { name: '董卓', type: '群雄' }, 212: { name: '张角', type: '群雄' }, 213: { name: '张辽', type: '魏国' },
|
||||
214: { name: '夏侯惇', type: '魏国' }, 215: { name: '许褚', type: '魏国' }, 216: { name: '夏侯渊', type: '魏国' },
|
||||
217: { name: '魏延', type: '蜀国' }, 218: { name: '黄忠', type: '蜀国' }, 219: { name: '马超', type: '蜀国' },
|
||||
220: { name: '马岱', type: '蜀国' }, 221: { name: '吕蒙', type: '吴国' }, 222: { name: '黄盖', type: '吴国' },
|
||||
223: { name: '蔡文姬', type: '魏国' }, 224: { name: '小乔', type: '吴国' }, 225: { name: '袁绍', type: '群雄' },
|
||||
226: { name: '华雄', type: '群雄' }, 227: { name: '颜良', type: '群雄' }, 228: { name: '文丑', type: '群雄' },
|
||||
301: { name: '周泰', type: '吴国' }, 302: { name: '许攸', type: '魏国' }, 303: { name: '于禁', type: '魏国' },
|
||||
304: { name: '张星彩', type: '蜀国' }, 305: { name: '关银屏', type: '蜀国' }, 306: { name: '关平', type: '蜀国' },
|
||||
307: { name: '程普', type: '吴国' }, 308: { name: '张昭', type: '吴国' }, 309: { name: '陆绩', type: '吴国' },
|
||||
310: { name: '吕玲绮', type: '群雄' }, 311: { name: '潘凤', type: '群雄' }, 312: { name: '邢道荣', type: '群雄' },
|
||||
313: { name: '祝融夫人', type: '群雄' }, 314: { name: '孟获', type: '群雄' }
|
||||
}
|
||||
|
||||
const wsStatus = computed(() => {
|
||||
if (!tokenStore.selectedToken) return 'disconnected'
|
||||
return tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
})
|
||||
|
||||
const presetTeamRaw = computed(() => tokenStore.gameData?.presetTeam ?? null)
|
||||
|
||||
function normalizePresetTeam(raw: any) {
|
||||
if (!raw) return { useTeamId: 1, teams: {} as Record<number, { teamInfo: Record<string, any> }> }
|
||||
const root = raw.presetTeamInfo ?? raw
|
||||
const findUseIdRec = (obj: any): number | null => {
|
||||
if (!obj || typeof obj !== 'object') return null
|
||||
if (typeof obj.useTeamId === 'number') return obj.useTeamId
|
||||
for (const k of Object.keys(obj)) {
|
||||
const v = findUseIdRec(obj[k])
|
||||
if (v) return v
|
||||
}
|
||||
return null
|
||||
}
|
||||
const useTeamId = root.useTeamId ?? root.presetTeamInfo?.useTeamId ?? findUseIdRec(root) ?? 1
|
||||
|
||||
const dict = root.presetTeamInfo ?? root
|
||||
const teams: Record<number, { teamInfo: Record<string, any> }> = {}
|
||||
const ids = Object.keys(dict || {}).filter((k) => /^\d+$/.test(k))
|
||||
for (const idStr of ids) {
|
||||
const id = Number(idStr)
|
||||
const node = dict[idStr]
|
||||
if (!node) { teams[id] = { teamInfo: {} }; continue }
|
||||
if (node.teamInfo) {
|
||||
teams[id] = { teamInfo: node.teamInfo }
|
||||
} else if (node.heroes) {
|
||||
const ti: Record<string, any> = {}
|
||||
node.heroes.forEach((h: any, idx: number) => { ti[String(idx + 1)] = h })
|
||||
teams[id] = { teamInfo: ti }
|
||||
} else if (typeof node === 'object') {
|
||||
const hasHero = Object.values(node).some((v: any) => v && typeof v === 'object' && 'heroId' in v)
|
||||
teams[id] = { teamInfo: hasHero ? node : {} }
|
||||
} else {
|
||||
teams[id] = { teamInfo: {} }
|
||||
}
|
||||
}
|
||||
return { useTeamId: Number(useTeamId) || 1, teams }
|
||||
}
|
||||
|
||||
const presetTeam = computed(() => normalizePresetTeam(presetTeamRaw.value))
|
||||
|
||||
const currentTeamHeroes = computed(() => {
|
||||
const team = (presetTeam.value.teams as any)[currentTeam.value]?.teamInfo
|
||||
if (!team) return [] as any[]
|
||||
const heroes: any[] = []
|
||||
for (const [pos, hero] of Object.entries(team)) {
|
||||
const hid = (hero as any)?.heroId ?? (hero as any)?.id
|
||||
if (!hid) continue
|
||||
const meta = HERO_DICT[Number(hid)]
|
||||
heroes.push({ id: Number(hid), name: meta?.name ?? `英雄${hid}`, type: meta?.type ?? '', position: Number(pos), level: (hero as any)?.level ?? 1, avatar: (hero as any)?.avatar })
|
||||
}
|
||||
heroes.sort((a, b) => a.position - b.position)
|
||||
return heroes
|
||||
})
|
||||
|
||||
const executeGameCommand = async (tokenId: string | number, cmd: string, params = {}, description = '', timeout = 8000) => {
|
||||
try { return await tokenStore.sendMessageWithPromise(tokenId, cmd, params, timeout) }
|
||||
catch (error: any) { if (description) message.error(`${description}失败:${error?.message ?? error}`); throw error }
|
||||
}
|
||||
|
||||
const getTeamInfoWithCache = async (force = false) => {
|
||||
if (!tokenStore.selectedToken) { message.warning('请先选择Token'); return null }
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
if (!force) {
|
||||
const cached = tokenStore.gameData?.presetTeam?.presetTeamInfo
|
||||
if (cached) return cached
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await executeGameCommand(tokenId, 'presetteam_getinfo', {}, '获取阵容信息')
|
||||
tokenStore.$patch((state: any) => { state.gameData = { ...(state.gameData ?? {}), presetTeam: result } })
|
||||
return result?.presetTeamInfo ?? null
|
||||
} catch (e) {
|
||||
console.error('获取阵容信息失败:', e)
|
||||
return null
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
const updateAvailableTeams = () => {
|
||||
const ids = Object.keys(presetTeam.value.teams).map(Number).filter(n => !Number.isNaN(n)).sort((a, b) => a - b)
|
||||
availableTeams.value = ids.length ? ids : [1, 2, 3, 4]
|
||||
}
|
||||
const updateCurrentTeam = () => { currentTeam.value = (presetTeam.value as any).useTeamId || 1 }
|
||||
|
||||
const selectTeam = async (teamId: number) => {
|
||||
if (switching.value || loading.value) return
|
||||
if (!tokenStore.selectedToken) { message.warning('请先选择Token'); return }
|
||||
const prev = currentTeam.value
|
||||
switching.value = true
|
||||
try {
|
||||
await executeGameCommand(tokenStore.selectedToken.id, 'presetteam_saveteam', { teamId }, `切换到阵容 ${teamId}`)
|
||||
currentTeam.value = teamId
|
||||
message.success(`已切换到阵容 ${teamId}`)
|
||||
await refreshTeamData(true)
|
||||
} catch (e) { currentTeam.value = prev }
|
||||
finally { switching.value = false }
|
||||
}
|
||||
|
||||
const refreshTeamData = async (force = false) => { await getTeamInfoWithCache(force) }
|
||||
|
||||
onMounted(async () => {
|
||||
if (tokenStore.selectedToken && wsStatus.value === 'connected') {
|
||||
await refreshTeamData(false)
|
||||
updateAvailableTeams(); updateCurrentTeam()
|
||||
if (!presetTeamRaw.value) {
|
||||
await refreshTeamData(true)
|
||||
updateAvailableTeams(); updateCurrentTeam()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(wsStatus, (newStatus, oldStatus) => {
|
||||
if (newStatus === 'connected' && oldStatus !== 'connected' && tokenStore.selectedToken) {
|
||||
setTimeout(async () => {
|
||||
await refreshTeamData(false)
|
||||
updateAvailableTeams(); updateCurrentTeam()
|
||||
if (!presetTeamRaw.value) {
|
||||
await refreshTeamData(true)
|
||||
updateAvailableTeams(); updateCurrentTeam()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => tokenStore.selectedToken, async (newToken, oldToken) => {
|
||||
if (newToken && newToken.id !== (oldToken as any)?.id) {
|
||||
const status = tokenStore.getWebSocketStatus(newToken.id)
|
||||
if (status === 'connected') {
|
||||
await refreshTeamData(true)
|
||||
updateAvailableTeams(); updateCurrentTeam()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => presetTeamRaw.value, () => { updateAvailableTeams(); updateCurrentTeam() }, { deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.team-formation-card { min-height: 220px; }
|
||||
.card-header { display: flex; align-items: flex-start; gap: var(--spacing-md); margin-bottom: var(--spacing-md); }
|
||||
.icon { width: 32px; height: 32px; object-fit: contain; flex-shrink: 0; }
|
||||
.info h3 { margin: 0 0 2px 0; font-size: var(--font-size-md); font-weight: var(--font-weight-semibold); }
|
||||
.info p { margin: 0; color: var(--text-secondary); font-size: var(--font-size-sm); }
|
||||
.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); }
|
||||
.team-button:hover { background: var(--bg-secondary); }
|
||||
.team-button.active { background: var(--primary-color); color: white; }
|
||||
.team-button:disabled { opacity: .6; cursor: not-allowed; }
|
||||
.refresh-button { display: flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px; border: 1px solid var(--border-color, #e5e7eb); border-radius: 8px; background: var(--bg-primary, #ffffff); color: var(--text-secondary, #6b7280); font-size: 13px; font-weight: 500; cursor: pointer; transition: all var(--transition-fast, 0.15s ease); }
|
||||
.refresh-button:hover { background: var(--bg-secondary, #f9fafb); border-color: var(--border-hover, #d1d5db); color: var(--text-primary, #374151); transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); }
|
||||
.refresh-button:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); }
|
||||
.refresh-button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
|
||||
.refresh-icon { width: 14px; height: 14px; transition: transform var(--transition-fast, 0.15s ease); }
|
||||
.refresh-button:not(:disabled):hover .refresh-icon { transform: rotate(180deg); }
|
||||
.refresh-button:disabled .refresh-icon { animation: spin 1s linear infinite; }
|
||||
@keyframes spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }
|
||||
.card-content .current-team-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-lg); }
|
||||
.card-content .label { font-size: var(--font-size-sm); color: var(--text-secondary); }
|
||||
.card-content .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: 80px; display: flex; align-items: center; justify-content: center; }
|
||||
.heroes-inline { display: flex; gap: var(--spacing-sm); align-items: center; }
|
||||
.hero-item { display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 64px; }
|
||||
.hero-circle { width: 48px; height: 48px; border-radius: 50%; background: var(--bg-primary); display: flex; align-items: center; justify-content: center; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
.hero-avatar { width: 100%; height: 100%; object-fit: cover; }
|
||||
.hero-placeholder { font-size: 12px; color: var(--text-secondary); }
|
||||
.hero-name { font-size: 12px; color: var(--text-secondary); text-align: center; min-width: 90px; max-width: 140px; white-space: nowrap; }
|
||||
.empty-team { color: var(--text-secondary); font-size: var(--font-size-sm); }
|
||||
</style>
|
||||
1090
xyzw_web_helper-main开源源码更新/src/components/TeamStatus.vue
Normal file
33
xyzw_web_helper-main开源源码更新/src/components/ThemeToggle.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<n-button
|
||||
circle
|
||||
size="medium"
|
||||
class="theme-toggle"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon v-if="isDark">
|
||||
<Sunny />
|
||||
</n-icon>
|
||||
<n-icon v-else>
|
||||
<Moon />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Moon, Sunny } from '@vicons/ionicons5'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
const { isDark, toggleTheme } = useTheme()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.theme-toggle {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}</style>
|
||||
720
xyzw_web_helper-main开源源码更新/src/components/TokenManager.vue
Normal file
@@ -0,0 +1,720 @@
|
||||
<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-dropdown
|
||||
:options="getTokenMenuOptions(tokenData)"
|
||||
trigger="click"
|
||||
@select="handleTokenAction($event, roleId, tokenData)"
|
||||
>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="tertiary"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><EllipsisHorizontal /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</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, h } from 'vue'
|
||||
import { useMessage, useDialog, NIcon } from 'naive-ui'
|
||||
import { useLocalTokenStore } from '@/stores/localTokenManager'
|
||||
import { useGameRolesStore } from '@/stores/gameRoles'
|
||||
import {
|
||||
Refresh,
|
||||
Download,
|
||||
CloudUpload,
|
||||
EllipsisHorizontal,
|
||||
Create,
|
||||
TrashBin,
|
||||
SyncCircle,
|
||||
CopyOutline
|
||||
} 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 '未连接'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Token菜单选项
|
||||
const getTokenMenuOptions = (tokenData) => {
|
||||
const options = [
|
||||
{
|
||||
label: '编辑',
|
||||
key: 'edit',
|
||||
icon: () => h(NIcon, null, { default: () => h(Create) })
|
||||
},
|
||||
{
|
||||
label: '复制Token',
|
||||
key: 'copy',
|
||||
icon: () => h(NIcon, null, { default: () => h(CopyOutline) })
|
||||
}
|
||||
]
|
||||
|
||||
// 如果是URL获取的Token,显示刷新选项
|
||||
if (tokenData.importMethod === 'url' && tokenData.sourceUrl) {
|
||||
options.unshift({
|
||||
label: '从URL刷新',
|
||||
key: 'refresh-url',
|
||||
icon: () => h(NIcon, null, { default: () => h(SyncCircle) })
|
||||
})
|
||||
} else {
|
||||
// 手动添加的Token显示重新生成选项
|
||||
options.unshift({
|
||||
label: '刷新Token',
|
||||
key: 'refresh',
|
||||
icon: () => h(NIcon, null, { default: () => h(Refresh) })
|
||||
})
|
||||
}
|
||||
|
||||
options.push(
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: '删除',
|
||||
key: 'delete',
|
||||
icon: () => h(NIcon, null, { default: () => h(TrashBin) })
|
||||
}
|
||||
)
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// 处理Token菜单操作
|
||||
const handleTokenAction = (action, roleId, tokenData) => {
|
||||
switch (action) {
|
||||
case 'edit':
|
||||
editToken(roleId, tokenData)
|
||||
break
|
||||
case 'copy':
|
||||
copyToken(tokenData.token)
|
||||
break
|
||||
case 'refresh':
|
||||
regenerateToken(roleId)
|
||||
break
|
||||
case 'refresh-url':
|
||||
refreshTokenFromUrl(roleId, tokenData)
|
||||
break
|
||||
case 'delete':
|
||||
removeToken(roleId)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
const oldTokenData = localTokenStore.getGameToken(roleId)
|
||||
if (!oldTokenData) {
|
||||
message.error('找不到对应的Token数据')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有源URL可以重新获取
|
||||
if (!oldTokenData.sourceUrl) {
|
||||
message.warning('该Token没有配置源地址,无法重新生成。请手动重新导入Token。')
|
||||
return
|
||||
}
|
||||
|
||||
dialog.info({
|
||||
title: '重新获取Token',
|
||||
content: '确定要从源地址重新获取此角色的Token吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
// 显示加载状态
|
||||
const loadingMsg = message.loading('正在重新获取Token...', { duration: 0 })
|
||||
|
||||
// 从源URL重新获取token
|
||||
let response
|
||||
const sourceUrl = oldTokenData.sourceUrl
|
||||
|
||||
// 使用与TokenImport相同的跨域处理逻辑
|
||||
const isLocalUrl = sourceUrl.startsWith(window.location.origin) ||
|
||||
sourceUrl.startsWith('/') ||
|
||||
sourceUrl.startsWith('http://localhost') ||
|
||||
sourceUrl.startsWith('http://127.0.0.1')
|
||||
|
||||
if (isLocalUrl) {
|
||||
response = await fetch(sourceUrl)
|
||||
} else {
|
||||
try {
|
||||
response = await fetch(sourceUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
mode: 'cors'
|
||||
})
|
||||
} catch (corsError) {
|
||||
throw new Error(`跨域请求被阻止。请确保目标服务器支持CORS。错误详情: ${corsError.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求失败: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.token) {
|
||||
throw new Error('返回数据中未找到token字段')
|
||||
}
|
||||
|
||||
// 更新token
|
||||
localTokenStore.updateGameToken(roleId, {
|
||||
token: data.token,
|
||||
server: data.server || oldTokenData.server,
|
||||
regeneratedAt: new Date().toISOString(),
|
||||
lastRefreshed: new Date().toISOString()
|
||||
})
|
||||
|
||||
// 如果当前token有连接,需要重新连接
|
||||
if (localTokenStore.getWebSocketStatus(roleId) === 'connected') {
|
||||
localTokenStore.closeWebSocketConnection(roleId)
|
||||
setTimeout(() => {
|
||||
localTokenStore.createWebSocketConnection(roleId, data.token, oldTokenData.wsUrl)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
loadingMsg.destroy()
|
||||
message.success('Token已成功重新获取')
|
||||
} catch (error) {
|
||||
console.error('重新获取Token失败:', error)
|
||||
message.error(error.message || 'Token重新获取失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const removeToken = (roleId) => {
|
||||
dialog.warning({
|
||||
title: '删除Token',
|
||||
content: '确定要删除此角色的游戏Token吗?这将断开相关的WebSocket连接。',
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
localTokenStore.removeGameToken(roleId)
|
||||
message.success('Token已删除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑Token(暂时显示提示信息,后续可以实现编辑功能)
|
||||
const editToken = (roleId, tokenData) => {
|
||||
message.info('编辑功能正在开发中')
|
||||
}
|
||||
|
||||
// 复制Token到剪贴板
|
||||
const copyToken = async (token) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(token)
|
||||
message.success('Token已复制到剪贴板')
|
||||
} catch (error) {
|
||||
// 降级方案
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = token
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
message.success('Token已复制到剪贴板')
|
||||
}
|
||||
}
|
||||
|
||||
// 从URL刷新Token
|
||||
const refreshTokenFromUrl = async (roleId, tokenData) => {
|
||||
if (!tokenData.sourceUrl) {
|
||||
message.warning('该Token没有配置源URL')
|
||||
return
|
||||
}
|
||||
|
||||
dialog.info({
|
||||
title: '从URL刷新Token',
|
||||
content: `确定要从源URL重新获取Token吗?\n源地址:${tokenData.sourceUrl}`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
const loadingMsg = message.loading('正在从URL获取新Token...', { duration: 0 })
|
||||
|
||||
// 使用与TokenImport相同的逻辑获取Token
|
||||
let response
|
||||
const isLocalUrl = tokenData.sourceUrl.startsWith(window.location.origin) ||
|
||||
tokenData.sourceUrl.startsWith('/') ||
|
||||
tokenData.sourceUrl.startsWith('http://localhost') ||
|
||||
tokenData.sourceUrl.startsWith('http://127.0.0.1')
|
||||
|
||||
if (isLocalUrl) {
|
||||
response = await fetch(tokenData.sourceUrl)
|
||||
} else {
|
||||
// 跨域请求,使用代理
|
||||
const proxyUrl = `/api/proxy?url=${encodeURIComponent(tokenData.sourceUrl)}`
|
||||
response = await fetch(proxyUrl)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (!data.token) {
|
||||
throw new Error('返回数据中未找到token字段')
|
||||
}
|
||||
|
||||
// 更新Token
|
||||
localTokenStore.updateGameToken(roleId, {
|
||||
token: data.token,
|
||||
lastUsed: new Date().toISOString()
|
||||
})
|
||||
|
||||
loadingMsg.destroy()
|
||||
message.success('Token刷新成功')
|
||||
} catch (error) {
|
||||
console.error('URL刷新Token失败:', error)
|
||||
message.error('刷新失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
434
xyzw_web_helper-main开源源码更新/src/components/TowerStatus.vue
Normal file
@@ -0,0 +1,434 @@
|
||||
<template>
|
||||
<div class="status-card tower-status">
|
||||
<div class="card-header">
|
||||
<img
|
||||
src="/icons/1733492491706148.png"
|
||||
alt="爬塔图标"
|
||||
class="status-icon"
|
||||
>
|
||||
<div class="status-info">
|
||||
<h3>咸将塔</h3>
|
||||
<p>一个不小心就过了</p>
|
||||
</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"
|
||||
>
|
||||
{{ isClimbing.value ? '爬塔中...' : '开始爬塔' }}
|
||||
</button>
|
||||
|
||||
<!-- 调试用的重置按钮,只在开发环境显示 -->
|
||||
<button
|
||||
v-if="isClimbing.value"
|
||||
class="reset-button"
|
||||
@click="resetClimbingState"
|
||||
>
|
||||
重置状态
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onMounted, ref, watch} from 'vue'
|
||||
import {useTokenStore} from '@/stores/tokenStore'
|
||||
import {useMessage} from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const isClimbing = ref(false)
|
||||
const climbTimeout = ref(null) // 用于超时重置状态
|
||||
const lastClimbResult = ref(null) // 最后一次爬塔结果
|
||||
|
||||
// 计算属性 - 从gameData中获取塔相关信息
|
||||
const roleInfo = computed(() => {
|
||||
const data = tokenStore.gameData?.roleInfo || null
|
||||
return data
|
||||
})
|
||||
|
||||
const currentFloor = computed(() => {
|
||||
const tower = roleInfo.value?.role?.tower
|
||||
|
||||
|
||||
if (!tower) {
|
||||
return "0 - 0"
|
||||
}
|
||||
|
||||
if (!tower.id && tower.id !== 0) {
|
||||
return "0 - 0"
|
||||
}
|
||||
|
||||
const towerId = tower.id
|
||||
const floor = Math.floor(towerId / 10) + 1
|
||||
const layer = towerId % 10 + 1
|
||||
return `${floor} - ${layer}`
|
||||
})
|
||||
|
||||
const towerEnergy = computed(() => {
|
||||
const tower = roleInfo.value?.role?.tower
|
||||
|
||||
|
||||
const energy = tower?.energy || 0
|
||||
return energy
|
||||
})
|
||||
|
||||
const canClimb = computed(() => {
|
||||
const hasEnergy = towerEnergy.value > 0
|
||||
const notClimbing = !isClimbing.value
|
||||
return hasEnergy && notClimbing
|
||||
})
|
||||
|
||||
// 方法
|
||||
const startTowerClimb = async () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
if (!canClimb.value) {
|
||||
message.warning('体力不足或正在爬塔中')
|
||||
return
|
||||
}
|
||||
|
||||
// 清除之前的超时
|
||||
if (climbTimeout.value) {
|
||||
clearTimeout(climbTimeout.value)
|
||||
climbTimeout.value = null
|
||||
}
|
||||
|
||||
// 确保在操作开始前设置状态
|
||||
isClimbing.value = true
|
||||
|
||||
// 设置超时保护,15秒后自动重置状态
|
||||
climbTimeout.value = setTimeout(() => {
|
||||
isClimbing.value = false
|
||||
climbTimeout.value = null
|
||||
}, 15000)
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 发送爬塔命令(日志精简:不输出控制台)
|
||||
|
||||
// 实际请求体将会是: {"ack":0,"body":{},"cmd":"fight_starttower","seq":XX,"time":TIMESTAMP}
|
||||
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'fight_starttower', {}, 10000)
|
||||
|
||||
message.success('爬塔命令已发送')
|
||||
|
||||
// 立即查询塔信息以获取最新状态
|
||||
await getTowerInfo()
|
||||
|
||||
// 再延迟查询一次确保数据同步
|
||||
setTimeout(async () => {
|
||||
await getTowerInfo()
|
||||
|
||||
// 清除超时并重置状态
|
||||
if (climbTimeout.value) {
|
||||
clearTimeout(climbTimeout.value)
|
||||
climbTimeout.value = null
|
||||
}
|
||||
isClimbing.value = false
|
||||
}, 2000)
|
||||
|
||||
} catch (error) {
|
||||
message.error('爬塔失败: ' + (error.message || '未知错误'))
|
||||
|
||||
// 发生错误时立即重置状态
|
||||
if (climbTimeout.value) {
|
||||
clearTimeout(climbTimeout.value)
|
||||
climbTimeout.value = null
|
||||
}
|
||||
isClimbing.value = false
|
||||
}
|
||||
|
||||
// 注意:不要在这里设置 isClimbing.value = false
|
||||
// 因为我们要等待延迟查询完成后再重置状态
|
||||
}
|
||||
|
||||
// 重置爬塔状态的方法
|
||||
const resetClimbingState = () => {
|
||||
if (climbTimeout.value) {
|
||||
clearTimeout(climbTimeout.value)
|
||||
climbTimeout.value = null
|
||||
}
|
||||
isClimbing.value = false
|
||||
message.info('爬塔状态已重置')
|
||||
}
|
||||
|
||||
const getTowerInfo = async () => {
|
||||
if (!tokenStore.selectedToken) { return }
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
// 检查WebSocket连接状态
|
||||
const wsStatus = tokenStore.getWebSocketStatus(tokenId)
|
||||
|
||||
if (wsStatus !== 'connected') {
|
||||
return
|
||||
}
|
||||
// 首先获取角色信息,这包含了塔的数据
|
||||
const roleResult = tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
// 直接请求塔信息
|
||||
const towerResult = tokenStore.sendMessage(tokenId, 'tower_getinfo')
|
||||
if (!roleResult && !towerResult) {}
|
||||
} catch (error) {
|
||||
// 获取塔信息失败:静默,避免噪声
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 监听WebSocket连接状态变化
|
||||
const wsStatus = computed(() => {
|
||||
if (!tokenStore.selectedToken) return 'disconnected'
|
||||
return tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
})
|
||||
|
||||
// 监听WebSocket连接状态,连接成功后自动获取塔信息
|
||||
watch(wsStatus, (newStatus, oldStatus) => {
|
||||
if (newStatus === 'connected' && oldStatus !== 'connected') {
|
||||
// 延迟一点时间让WebSocket完全就绪
|
||||
setTimeout(() => {
|
||||
getTowerInfo()
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听选中Token变化
|
||||
watch(() => tokenStore.selectedToken, (newToken, oldToken) => {
|
||||
if (newToken && newToken.id !== oldToken?.id) {
|
||||
// 检查WebSocket是否已连接
|
||||
const status = tokenStore.getWebSocketStatus(newToken.id)
|
||||
if (status === 'connected') {
|
||||
getTowerInfo()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听爬塔结果
|
||||
watch(() => tokenStore.gameData.towerResult, (newResult, oldResult) => {
|
||||
if (newResult && newResult.timestamp !== oldResult?.timestamp) {
|
||||
// 显示爬塔结果消息
|
||||
if (newResult.success) {
|
||||
message.success('咸将塔挑战成功!')
|
||||
|
||||
if (newResult.autoReward) {
|
||||
setTimeout(() => {
|
||||
message.success(`自动领取第${newResult.rewardFloor}层奖励`)
|
||||
}, 1000)
|
||||
}
|
||||
} else {
|
||||
message.error('咸将塔挑战失败')
|
||||
}
|
||||
|
||||
// 重置爬塔状态
|
||||
setTimeout(() => {
|
||||
if (climbTimeout.value) {
|
||||
clearTimeout(climbTimeout.value)
|
||||
climbTimeout.value = null
|
||||
}
|
||||
isClimbing.value = false
|
||||
}, 2000)
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
|
||||
|
||||
// 检查WebSocket客户端
|
||||
if (tokenStore.selectedToken) {
|
||||
const client = tokenStore.getWebSocketClient(tokenStore.selectedToken.id)
|
||||
}
|
||||
|
||||
// 组件挂载时获取塔信息
|
||||
if (tokenStore.selectedToken && wsStatus.value === 'connected') {
|
||||
getTowerInfo()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
// 使用GameStatus中的统一卡片样式
|
||||
.tower-status {
|
||||
border-left: 4px solid #6366f1; // 咸将塔专用颜色
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 240px; // 继续缩小整体高度
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.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);
|
||||
margin-left: auto; // 使小鱼干展示靠右
|
||||
}
|
||||
|
||||
.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-lg);
|
||||
margin-bottom: var(--spacing-md);
|
||||
flex: 1; // 占据可用空间,使上下分布更均衡
|
||||
display: flex;
|
||||
align-items: center; // 内容在中部更居中
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
margin-top: auto;
|
||||
padding-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
width: 100%;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border: 1px solid var(--warning-color);
|
||||
border-radius: var(--border-radius-small);
|
||||
background: transparent;
|
||||
color: var(--warning-color);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--warning-color);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
584
xyzw_web_helper-main开源源码更新/src/components/WebSocketTester.vue
Normal file
@@ -0,0 +1,584 @@
|
||||
<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-button
|
||||
type="warning"
|
||||
:disabled="!selectedCommand"
|
||||
:loading="testingConcurrency"
|
||||
@click="testConcurrentRequests"
|
||||
>
|
||||
测试并发请求
|
||||
</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 testingConcurrency = 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 testConcurrentRequests = async () => {
|
||||
if (!selectedCommand.value) {
|
||||
message.error('请选择要发送的命令')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
testingConcurrency.value = true
|
||||
|
||||
let params = {}
|
||||
if (commandParams.value.trim()) {
|
||||
params = JSON.parse(commandParams.value)
|
||||
}
|
||||
|
||||
addToLog('sent', {
|
||||
message: '开始并发测试:同时发送5个相同命令',
|
||||
command: selectedCommand.value,
|
||||
params
|
||||
})
|
||||
|
||||
// 同时发送5个相同的命令
|
||||
const promises = []
|
||||
const startTime = Date.now()
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const promise = tokenStore.sendMessageWithPromise(
|
||||
selectedRoleId.value,
|
||||
selectedCommand.value,
|
||||
{ ...params, requestIndex: i + 1 }
|
||||
).then(response => ({
|
||||
requestIndex: i + 1,
|
||||
response,
|
||||
success: true
|
||||
})).catch(error => ({
|
||||
requestIndex: i + 1,
|
||||
error: error.message,
|
||||
success: false
|
||||
}))
|
||||
|
||||
promises.push(promise)
|
||||
}
|
||||
|
||||
// 等待所有请求完成
|
||||
const results = await Promise.allSettled(promises)
|
||||
const endTime = Date.now()
|
||||
|
||||
// 记录结果
|
||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value.success).length
|
||||
const failCount = results.length - successCount
|
||||
|
||||
addToLog('received', {
|
||||
message: '并发测试完成',
|
||||
totalRequests: 5,
|
||||
successCount,
|
||||
failCount,
|
||||
duration: `${endTime - startTime}ms`,
|
||||
results: results.map(r => r.status === 'fulfilled' ? r.value : { error: r.reason })
|
||||
})
|
||||
|
||||
if (successCount === 5) {
|
||||
message.success(`并发测试成功!5个请求全部正确响应,耗时${endTime - startTime}ms`)
|
||||
} else if (successCount > 0) {
|
||||
message.warning(`并发测试部分成功:${successCount}个成功,${failCount}个失败`)
|
||||
} else {
|
||||
message.error('并发测试失败:所有请求都失败了')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('并发测试失败:', error)
|
||||
message.error('并发测试失败: ' + error.message)
|
||||
addToLog('received', {
|
||||
message: '并发测试异常',
|
||||
error: error.message
|
||||
})
|
||||
} finally {
|
||||
testingConcurrency.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>
|
||||
141
xyzw_web_helper-main开源源码更新/src/composables/useTheme.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 全局响应式主题状态
|
||||
const isDark = ref(false)
|
||||
|
||||
// 检查当前主题状态
|
||||
const checkCurrentTheme = () => {
|
||||
return document.documentElement.classList.contains('dark') ||
|
||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||
}
|
||||
|
||||
// 更新响应式状态
|
||||
const updateReactiveState = () => {
|
||||
isDark.value = checkCurrentTheme()
|
||||
}
|
||||
|
||||
// 主题管理逻辑
|
||||
export function useTheme() {
|
||||
let mutationObserver = null
|
||||
|
||||
// 初始化主题
|
||||
const initTheme = () => {
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
||||
setDarkTheme()
|
||||
} else {
|
||||
setLightTheme()
|
||||
}
|
||||
|
||||
// 立即更新响应式状态
|
||||
updateReactiveState()
|
||||
}
|
||||
|
||||
// 设置深色主题
|
||||
const setDarkTheme = () => {
|
||||
document.documentElement.classList.add('dark')
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
document.body.classList.add('dark')
|
||||
document.body.setAttribute('data-theme', 'dark')
|
||||
localStorage.setItem('theme', 'dark')
|
||||
|
||||
// 立即更新响应式状态
|
||||
isDark.value = true
|
||||
|
||||
// 触发主题更新事件
|
||||
window.dispatchEvent(new CustomEvent('theme-change', { detail: { isDark: true } }))
|
||||
}
|
||||
|
||||
// 设置浅色主题
|
||||
const setLightTheme = () => {
|
||||
document.documentElement.classList.remove('dark')
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
document.body.classList.remove('dark')
|
||||
document.body.removeAttribute('data-theme')
|
||||
localStorage.setItem('theme', 'light')
|
||||
|
||||
// 立即更新响应式状态
|
||||
isDark.value = false
|
||||
|
||||
// 触发主题更新事件
|
||||
window.dispatchEvent(new CustomEvent('theme-change', { detail: { isDark: false } }))
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
if (isDark.value) {
|
||||
setLightTheme()
|
||||
} else {
|
||||
setDarkTheme()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听系统主题变化
|
||||
const setupSystemThemeListener = () => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
mediaQuery.addListener(() => {
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
// 只有在用户没有手动设置主题时才跟随系统
|
||||
if (!savedTheme) {
|
||||
initTheme()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 设置DOM变化监听器(确保响应式状态同步)
|
||||
const setupDOMObserver = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
mutationObserver = new MutationObserver(() => {
|
||||
updateReactiveState()
|
||||
})
|
||||
|
||||
// 监听documentElement和body的变化
|
||||
mutationObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme']
|
||||
})
|
||||
|
||||
mutationObserver.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 清理监听器
|
||||
const cleanup = () => {
|
||||
if (mutationObserver) {
|
||||
mutationObserver.disconnect()
|
||||
mutationObserver = null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前主题
|
||||
const getCurrentTheme = () => {
|
||||
return isDark.value ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
setupDOMObserver()
|
||||
updateReactiveState()
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
return {
|
||||
isDark,
|
||||
initTheme,
|
||||
toggleTheme,
|
||||
setDarkTheme,
|
||||
setLightTheme,
|
||||
setupSystemThemeListener,
|
||||
getCurrentTheme,
|
||||
updateReactiveState
|
||||
}
|
||||
}
|
||||
44
xyzw_web_helper-main开源源码更新/src/main.js
Normal file
@@ -0,0 +1,44 @@
|
||||
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)
|
||||
|
||||
// 全局主题应用:从 localStorage 读取并设置 data-theme 属性
|
||||
const applyTheme = () => {
|
||||
const saved = localStorage.getItem('theme') || 'auto'
|
||||
if (saved === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
} else if (saved === 'light') {
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
} else {
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
if (prefersDark) document.documentElement.setAttribute('data-theme', 'dark')
|
||||
else document.documentElement.removeAttribute('data-theme')
|
||||
|
||||
// 跟随系统变更
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
const t = localStorage.getItem('theme') || 'auto'
|
||||
if (t === 'auto') {
|
||||
if (e.matches) document.documentElement.setAttribute('data-theme', 'dark')
|
||||
else document.documentElement.removeAttribute('data-theme')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyTheme()
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
142
xyzw_web_helper-main开源源码更新/src/router/index.js
Normal file
@@ -0,0 +1,142 @@
|
||||
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
|
||||
},
|
||||
props: route => ({
|
||||
token: route.query.token,
|
||||
name: route.query.name,
|
||||
server: route.query.server,
|
||||
wsUrl: route.query.wsUrl,
|
||||
api: route.query.api,
|
||||
auto: route.query.auto === 'true'
|
||||
})
|
||||
},
|
||||
{
|
||||
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.path === '/' && tokenStore.hasTokens) {
|
||||
// 首页重定向逻辑
|
||||
if (tokenStore.selectedToken) {
|
||||
next('/dashboard')
|
||||
} else {
|
||||
next('/tokens')
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
158
xyzw_web_helper-main开源源码更新/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
xyzw_web_helper-main开源源码更新/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
|
||||
}
|
||||
})
|
||||
470
xyzw_web_helper-main开源源码更新/src/stores/localTokenManager.js
Normal file
@@ -0,0 +1,470 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
getUserToken as dbGetUserToken,
|
||||
setUserToken as dbSetUserToken,
|
||||
clearUserToken as dbClearUserToken,
|
||||
getAllGameTokens as dbGetAllGameTokens,
|
||||
putGameToken as dbPutGameToken,
|
||||
deleteGameToken as dbDeleteGameToken,
|
||||
clearGameTokens as dbClearGameTokens,
|
||||
migrateFromLocalStorageIfNeeded
|
||||
} from '@/utils/tokenDb'
|
||||
|
||||
/**
|
||||
* 本地Token管理器
|
||||
* 用于管理用户认证token和游戏角色token的本地存储
|
||||
*/
|
||||
export const useLocalTokenStore = defineStore('localToken', () => {
|
||||
// 状态(内存态,实际持久化到 IndexedDB)
|
||||
const userToken = ref(null)
|
||||
const gameTokens = ref({})
|
||||
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
|
||||
// 持久化到 IndexedDB(异步,不阻塞 UI)
|
||||
dbSetUserToken(token).catch((e) => console.warn('保存用户Token失败:', e))
|
||||
}
|
||||
|
||||
const clearUserToken = () => {
|
||||
userToken.value = null
|
||||
dbClearUserToken().catch((e) => console.warn('清除用户Token失败:', e))
|
||||
}
|
||||
|
||||
// 游戏token管理
|
||||
const addGameToken = (roleId, tokenData) => {
|
||||
const newTokenData = {
|
||||
...tokenData,
|
||||
roleId,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsed: new Date().toISOString()
|
||||
}
|
||||
|
||||
gameTokens.value[roleId] = newTokenData
|
||||
dbPutGameToken(roleId, newTokenData).catch((e) => console.warn('保存游戏Token失败:', e))
|
||||
|
||||
return newTokenData
|
||||
}
|
||||
|
||||
const getGameToken = (roleId) => {
|
||||
const token = gameTokens.value[roleId]
|
||||
if (token) {
|
||||
// 更新最后使用时间
|
||||
token.lastUsed = new Date().toISOString()
|
||||
dbPutGameToken(roleId, token).catch((e) => console.warn('更新游戏Token失败:', e))
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
const updateGameToken = (roleId, updates) => {
|
||||
if (gameTokens.value[roleId]) {
|
||||
gameTokens.value[roleId] = {
|
||||
...gameTokens.value[roleId],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
dbPutGameToken(roleId, gameTokens.value[roleId]).catch((e) => console.warn('更新游戏Token失败:', e))
|
||||
}
|
||||
}
|
||||
|
||||
const removeGameToken = (roleId) => {
|
||||
delete gameTokens.value[roleId]
|
||||
dbDeleteGameToken(roleId).catch((e) => console.warn('删除游戏Token失败:', e))
|
||||
|
||||
// 同时断开对应的WebSocket连接
|
||||
if (wsConnections.value[roleId]) {
|
||||
closeWebSocketConnection(roleId)
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllGameTokens = () => {
|
||||
// 关闭所有WebSocket连接
|
||||
Object.keys(wsConnections.value).forEach(roleId => {
|
||||
closeWebSocketConnection(roleId)
|
||||
})
|
||||
|
||||
gameTokens.value = {}
|
||||
dbClearGameTokens().catch((e) => console.warn('清空游戏Token失败:', e))
|
||||
}
|
||||
|
||||
// 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 = () => {
|
||||
// 降噪
|
||||
|
||||
// 更新连接状态
|
||||
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) => {
|
||||
// 降噪
|
||||
|
||||
// 处理不同类型的消息
|
||||
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) => {
|
||||
// 降噪
|
||||
if (wsConnections.value[roleId]) {
|
||||
wsConnections.value[roleId].status = 'disconnected'
|
||||
}
|
||||
}
|
||||
|
||||
wsAgent.onReconnect = (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':
|
||||
// 降噪
|
||||
break
|
||||
|
||||
case 'system_getdatabundlever':
|
||||
// 降噪
|
||||
break
|
||||
|
||||
case 'task_claimdailyreward':
|
||||
// 降噪
|
||||
break
|
||||
|
||||
case 'system_signinreward':
|
||||
// 降噪
|
||||
break
|
||||
|
||||
default:
|
||||
// 降噪
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// 降噪
|
||||
return false
|
||||
}
|
||||
|
||||
if (connection.status !== 'connected') {
|
||||
// 降噪
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const { gameCommands } = connection
|
||||
|
||||
if (typeof gameCommands[commandName] === 'function') {
|
||||
const command = gameCommands[commandName](0, 0, params)
|
||||
connection.agent.send(command)
|
||||
// 降噪
|
||||
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
|
||||
})
|
||||
// 降噪
|
||||
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
|
||||
// 持久化到 DB
|
||||
Object.entries(gameTokens.value).forEach(([rid, data]) => {
|
||||
dbPutGameToken(rid, { ...data, roleId: rid }).catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// 从 DB 删除
|
||||
dbDeleteGameToken(roleId).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
gameTokens.value = cleanedTokens
|
||||
|
||||
return cleanedCount
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const initTokenManager = async () => {
|
||||
try {
|
||||
// 一次性迁移旧 localStorage 数据(如有)
|
||||
await migrateFromLocalStorageIfNeeded()
|
||||
|
||||
// 从 IndexedDB 恢复
|
||||
const [dbUser, dbTokens] = await Promise.all([
|
||||
dbGetUserToken(),
|
||||
dbGetAllGameTokens()
|
||||
])
|
||||
|
||||
if (dbUser) userToken.value = dbUser
|
||||
gameTokens.value = dbTokens || {}
|
||||
|
||||
// 清理过期token(会同步更新 DB)
|
||||
cleanExpiredTokens()
|
||||
} catch (e) {
|
||||
console.warn('初始化Token管理器失败,回退为空:', e)
|
||||
userToken.value = null
|
||||
gameTokens.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
1542
xyzw_web_helper-main开源源码更新/src/stores/tokenStore.js
Normal file
779
xyzw_web_helper-main开源源码更新/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 };
|
||||
144
xyzw_web_helper-main开源源码更新/src/utils/clubBattleUtils.js
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 俱乐部战斗工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取最近的周六日期
|
||||
* 如果今天是周六,返回今天的日期;否则返回上周六的日期
|
||||
* @returns {string} 格式化的日期字符串 YYYY/MM/DD
|
||||
*/
|
||||
export function getLastSaturday() {
|
||||
const today = new Date()
|
||||
const dayOfWeek = today.getDay() // 0=周日, 1=周一, ..., 6=周六
|
||||
|
||||
let daysToSubtract = 0
|
||||
if (dayOfWeek === 6) {
|
||||
// 今天是周六
|
||||
daysToSubtract = 0
|
||||
} else if (dayOfWeek === 0) {
|
||||
// 今天是周日,返回昨天(周六)
|
||||
daysToSubtract = 1
|
||||
} else {
|
||||
// 周一到周五,计算距离上周六的天数
|
||||
daysToSubtract = dayOfWeek + 1
|
||||
}
|
||||
|
||||
const targetDate = new Date(today)
|
||||
targetDate.setDate(today.getDate() - daysToSubtract)
|
||||
|
||||
const year = targetDate.getFullYear()
|
||||
const month = String(targetDate.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(targetDate.getDate()).padStart(2, '0')
|
||||
|
||||
return `${year}/${month}/${day}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间戳为可读时间
|
||||
* @param {number} timestamp - Unix时间戳(秒)
|
||||
* @returns {string} 格式化的时间字符串
|
||||
*/
|
||||
export function formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp * 1000)
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析战斗结果标志
|
||||
* @param {number} newWinFlag - 战斗结果标志 (1=败, 2=胜)
|
||||
* @returns {string} "胜利" 或 "失败"
|
||||
*/
|
||||
export function parseBattleResult(newWinFlag) {
|
||||
return newWinFlag === 2 ? '胜利' : '失败'
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析攻击类型
|
||||
* @param {number} attackType - 攻击类型 (0=进攻, 1=防守)
|
||||
* @returns {string} "进攻" 或 "防守"
|
||||
*/
|
||||
export function parseAttackType(attackType) {
|
||||
return attackType === 0 ? '进攻' : '防守'
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化成员战绩数据用于导出
|
||||
* @param {Array} roleDetailsList - 成员详情列表
|
||||
* @param {string} queryDate - 查询日期
|
||||
* @returns {string} 格式化的文本
|
||||
*/
|
||||
export function formatBattleRecordsForExport(roleDetailsList, queryDate) {
|
||||
if (!roleDetailsList || roleDetailsList.length === 0) {
|
||||
return '暂无战绩数据'
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`俱乐部盐场战绩 - ${queryDate}`,
|
||||
`参战人数: ${roleDetailsList.length}`,
|
||||
'─'.repeat(40),
|
||||
''
|
||||
]
|
||||
|
||||
// 按击杀数排序
|
||||
const sortedMembers = [...roleDetailsList].sort((a, b) => (b.winCnt || 0) - (a.winCnt || 0))
|
||||
|
||||
// 计算总计
|
||||
let totalKills = 0
|
||||
let totalDeaths = 0
|
||||
let totalSieges = 0
|
||||
|
||||
sortedMembers.forEach((member, index) => {
|
||||
const { name, winCnt, loseCnt, buildingCnt } = member
|
||||
totalKills += winCnt || 0
|
||||
totalDeaths += loseCnt || 0
|
||||
totalSieges += buildingCnt || 0
|
||||
|
||||
lines.push(
|
||||
`${index + 1}. ${name} 击杀${winCnt || 0} 死亡${loseCnt || 0} 攻城${buildingCnt || 0}`
|
||||
)
|
||||
})
|
||||
|
||||
lines.push('')
|
||||
lines.push('─'.repeat(40))
|
||||
lines.push(`总计 击杀${totalKills} 死亡${totalDeaths} 攻城${totalSieges}`)
|
||||
lines.push('')
|
||||
lines.push(`导出时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
* @param {string} text - 要复制的文本
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function copyToClipboard(text) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// 现代浏览器
|
||||
await navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
// 降级方案
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.left = '-999999px'
|
||||
textArea.style.top = '-999999px'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
} catch (err) {
|
||||
throw new Error('复制失败')
|
||||
} finally {
|
||||
textArea.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
702
xyzw_web_helper-main开源源码更新/src/utils/gameCommands.js
Normal file
@@ -0,0 +1,702 @@
|
||||
/**
|
||||
* 游戏命令构造器
|
||||
* 基于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: {},
|
||||
cmd: "_sys/ack",
|
||||
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,
|
||||
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,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取俱乐部战争详情
|
||||
*/
|
||||
legionwar_getdetails(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
date: "2025/10/04",
|
||||
...params
|
||||
}),
|
||||
cmd: "legionwar_getdetails",
|
||||
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
|
||||
164
xyzw_web_helper-main开源源码更新/src/utils/logger.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 智能日志管理系统
|
||||
* 支持日志级别控制和开发/生产环境区分
|
||||
*/
|
||||
|
||||
// 日志级别定义
|
||||
export const LOG_LEVELS = {
|
||||
ERROR: 0, // 错误 - 始终显示
|
||||
WARN: 1, // 警告 - 生产环境显示
|
||||
INFO: 2, // 信息 - 开发环境显示
|
||||
DEBUG: 3, // 调试 - 开发环境详细模式
|
||||
VERBOSE: 4 // 详细 - 仅在明确启用时显示
|
||||
}
|
||||
|
||||
class Logger {
|
||||
constructor(namespace = 'APP') {
|
||||
this.namespace = namespace
|
||||
this.level = this.getLogLevel()
|
||||
this.isDev = import.meta.env.DEV
|
||||
this.enableVerbose = localStorage.getItem('ws_debug_verbose') === 'true'
|
||||
}
|
||||
|
||||
getLogLevel() {
|
||||
// 生产环境默认只显示错误和警告
|
||||
if (!import.meta.env.DEV) {
|
||||
return LOG_LEVELS.WARN
|
||||
}
|
||||
|
||||
// 开发环境根据localStorage配置决定
|
||||
const saved = localStorage.getItem('ws_debug_level')
|
||||
if (saved) {
|
||||
return parseInt(saved, 10)
|
||||
}
|
||||
|
||||
return LOG_LEVELS.INFO // 开发环境默认显示信息级别
|
||||
}
|
||||
|
||||
setLevel(level) {
|
||||
this.level = level
|
||||
localStorage.setItem('ws_debug_level', level.toString())
|
||||
}
|
||||
|
||||
setVerbose(enabled) {
|
||||
this.enableVerbose = enabled
|
||||
localStorage.setItem('ws_debug_verbose', enabled.toString())
|
||||
}
|
||||
|
||||
formatMessage(level, message, ...args) {
|
||||
const timestamp = new Date().toLocaleTimeString('zh-CN', {
|
||||
hour12: false,
|
||||
millisecond: true
|
||||
})
|
||||
const levelName = Object.keys(LOG_LEVELS)[level]
|
||||
const prefix = `[${timestamp}] [${this.namespace}] [${levelName}]`
|
||||
|
||||
return [prefix, message, ...args]
|
||||
}
|
||||
|
||||
error(message, ...args) {
|
||||
if (this.level >= LOG_LEVELS.ERROR) {
|
||||
console.error(...this.formatMessage(LOG_LEVELS.ERROR, message, ...args))
|
||||
}
|
||||
}
|
||||
|
||||
warn(message, ...args) {
|
||||
if (this.level >= LOG_LEVELS.WARN) {
|
||||
console.warn(...this.formatMessage(LOG_LEVELS.WARN, message, ...args))
|
||||
}
|
||||
}
|
||||
|
||||
info(message, ...args) {
|
||||
if (this.level >= LOG_LEVELS.INFO) {
|
||||
console.info(...this.formatMessage(LOG_LEVELS.INFO, message, ...args))
|
||||
}
|
||||
}
|
||||
|
||||
debug(message, ...args) {
|
||||
if (this.level >= LOG_LEVELS.DEBUG) {
|
||||
console.log(...this.formatMessage(LOG_LEVELS.DEBUG, message, ...args))
|
||||
}
|
||||
}
|
||||
|
||||
verbose(message, ...args) {
|
||||
if (this.enableVerbose && this.level >= LOG_LEVELS.VERBOSE) {
|
||||
console.log(...this.formatMessage(LOG_LEVELS.VERBOSE, message, ...args))
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket专用的简化日志方法
|
||||
wsConnect(tokenId) {
|
||||
this.info(`🔗 WebSocket连接: ${tokenId}`)
|
||||
}
|
||||
|
||||
wsDisconnect(tokenId, reason = '') {
|
||||
this.info(`🔌 WebSocket断开: ${tokenId}${reason ? ' - ' + reason : ''}`)
|
||||
}
|
||||
|
||||
wsError(tokenId, error) {
|
||||
this.error(`❌ WebSocket错误 [${tokenId}]:`, error)
|
||||
}
|
||||
|
||||
wsMessage(tokenId, cmd, isReceived = false) {
|
||||
if (cmd === '_sys/ack') return // 过滤心跳消息
|
||||
const direction = isReceived ? '📨' : '📤'
|
||||
this.debug(`${direction} [${tokenId}] ${cmd}`)
|
||||
}
|
||||
|
||||
wsStatus(tokenId, status, details = '') {
|
||||
this.info(`📊 [${tokenId}] ${status}${details ? ' - ' + details : ''}`)
|
||||
}
|
||||
|
||||
// 连接管理专用日志
|
||||
connectionLock(tokenId, operation, acquired = true) {
|
||||
if (acquired) {
|
||||
this.debug(`🔐 获取连接锁: ${tokenId} (${operation})`)
|
||||
} else {
|
||||
this.debug(`🔓 释放连接锁: ${tokenId} (${operation})`)
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏消息处理
|
||||
gameMessage(tokenId, cmd, hasBody = false) {
|
||||
if (cmd === '_sys/ack') return
|
||||
this.debug(`🎮 [${tokenId}] ${cmd}${hasBody ? ' ✓' : ' ✗'}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建命名空间的日志实例
|
||||
export const createLogger = (namespace) => new Logger(namespace)
|
||||
|
||||
// 预定义的日志实例
|
||||
export const wsLogger = createLogger('WS')
|
||||
export const tokenLogger = createLogger('TOKEN')
|
||||
export const gameLogger = createLogger('GAME')
|
||||
|
||||
// 全局日志控制函数
|
||||
export const setGlobalLogLevel = (level) => {
|
||||
wsLogger.setLevel(level)
|
||||
tokenLogger.setLevel(level)
|
||||
gameLogger.setLevel(level)
|
||||
}
|
||||
|
||||
export const enableVerboseLogging = (enabled = true) => {
|
||||
wsLogger.setVerbose(enabled)
|
||||
tokenLogger.setVerbose(enabled)
|
||||
gameLogger.setVerbose(enabled)
|
||||
}
|
||||
|
||||
// 开发者调试工具
|
||||
window.wsDebug = {
|
||||
setLevel: setGlobalLogLevel,
|
||||
enableVerbose: enableVerboseLogging,
|
||||
levels: LOG_LEVELS,
|
||||
// 快捷设置
|
||||
quiet: () => setGlobalLogLevel(LOG_LEVELS.WARN),
|
||||
normal: () => setGlobalLogLevel(LOG_LEVELS.INFO),
|
||||
debug: () => setGlobalLogLevel(LOG_LEVELS.DEBUG),
|
||||
verbose: () => {
|
||||
setGlobalLogLevel(LOG_LEVELS.VERBOSE)
|
||||
enableVerboseLogging(true)
|
||||
}
|
||||
}
|
||||
|
||||
console.info('🔧 WebSocket调试工具已加载,使用 wsDebug.verbose() 启用详细日志')
|
||||
547
xyzw_web_helper-main开源源码更新/src/utils/readable-xyzw-ws.js
Normal file
@@ -0,0 +1,547 @@
|
||||
// 解析后的XYZW WebSocket通信库
|
||||
// 原文件: CTx_gHj7.js (混淆版本)
|
||||
|
||||
// 导入依赖模块
|
||||
import {a$ as createRef, G as createApp} from "./DpD38Hq9.js";
|
||||
import {c as useI18n, 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
|
||||
};
|
||||
176
xyzw_web_helper-main开源源码更新/src/utils/studyQuestionsFromJSON.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 从 answer.json 文件加载题目数据的答题工具
|
||||
* 用于一键答题功能,从公共目录读取题目数据
|
||||
*/
|
||||
|
||||
let questionsData = null
|
||||
let isLoading = false
|
||||
|
||||
/**
|
||||
* 异步加载答题数据
|
||||
* @returns {Promise<Array>} 题目数据数组
|
||||
*/
|
||||
export async function loadQuestionsData() {
|
||||
if (questionsData) {
|
||||
return questionsData
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
// 如果正在加载,等待加载完成
|
||||
while (isLoading) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
return questionsData
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading = true
|
||||
// 精简日志:移除加载提示
|
||||
|
||||
// 从 public 目录加载答题数据
|
||||
const response = await fetch('/answer.json')
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
|
||||
// 由于文件格式不是标准JSON,需要特殊处理
|
||||
// 文件内容看起来像是 JavaScript 数组,需要转换为 JSON 格式
|
||||
let jsonText = text.trim()
|
||||
|
||||
try {
|
||||
// 直接尝试 JSON.parse
|
||||
questionsData = JSON.parse(jsonText)
|
||||
} catch (parseError) {
|
||||
// 降噪:解析失败不刷屏
|
||||
|
||||
// 处理 JavaScript 对象格式为 JSON 格式
|
||||
// 将 name: "..." 转换为 "name": "..."
|
||||
// 将 value: 1 转换为 "value": 1
|
||||
jsonText = jsonText
|
||||
.replace(/(\w+):\s*"/g, '"$1": "') // name: "xxx" -> "name": "xxx"
|
||||
.replace(/(\w+):\s*(\d+)/g, '"$1": $2') // value: 1 -> "value": 1
|
||||
.replace(/(\w+):\s*([^",}\]]+)/g, '"$1": "$2"') // 处理其他情况
|
||||
|
||||
try {
|
||||
questionsData = JSON.parse(jsonText)
|
||||
} catch (secondParseError) {
|
||||
// 如果还是失败,尝试使用 eval(本地文件,相对安全)
|
||||
// 降噪
|
||||
if (text.trim().startsWith('[') && text.trim().endsWith(']')) {
|
||||
try {
|
||||
// 创建一个安全的执行环境
|
||||
questionsData = Function('"use strict"; return (' + text + ')')()
|
||||
} catch (evalError) {
|
||||
console.error('所有解析方法都失败了')
|
||||
throw new Error(`数据解析失败: ${evalError.message}`)
|
||||
}
|
||||
} else {
|
||||
throw new Error('数据格式不正确,必须是数组格式')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(questionsData)) {
|
||||
throw new Error('加载的数据不是数组格式')
|
||||
}
|
||||
|
||||
// 降噪
|
||||
return questionsData
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 加载答题数据失败:', error)
|
||||
// 返回空数组,避免程序崩溃
|
||||
questionsData = []
|
||||
return questionsData
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模糊匹配函数 - 查找题目中的关键词
|
||||
* @param {string} questionFromDB - 数据库中的题目
|
||||
* @param {string} actualQuestion - 实际收到的题目
|
||||
* @param {number} threshold - 匹配阈值(1表示包含匹配)
|
||||
* @returns {boolean} - 是否匹配
|
||||
*/
|
||||
export function matchQuestion(questionFromDB, actualQuestion, threshold = 1) {
|
||||
if (!questionFromDB || !actualQuestion) return false
|
||||
|
||||
// 简单的包含匹配
|
||||
if (threshold === 1) {
|
||||
// 去除空格和特殊字符进行匹配
|
||||
const cleanDB = questionFromDB.replace(/\s+/g, '').toLowerCase()
|
||||
const cleanActual = actualQuestion.replace(/\s+/g, '').toLowerCase()
|
||||
|
||||
return cleanActual.includes(cleanDB) || cleanDB.includes(cleanActual)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找题目答案
|
||||
* @param {string} question - 题目文本
|
||||
* @returns {Promise<number|null>} - 答案选项(1-4),未找到返回null
|
||||
*/
|
||||
export async function findAnswer(question) {
|
||||
try {
|
||||
const questions = await loadQuestionsData()
|
||||
|
||||
if (!questions || questions.length === 0) {
|
||||
// 降噪
|
||||
return null
|
||||
}
|
||||
|
||||
// 遍历所有题目寻找匹配
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const item = questions[i]
|
||||
if (!item.name || !item.value) continue
|
||||
|
||||
if (matchQuestion(item.name, question, 1)) {
|
||||
// 降噪
|
||||
return item.value
|
||||
}
|
||||
}
|
||||
|
||||
// 降噪
|
||||
return null // 未找到匹配的题目
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 查找答案时出错:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已加载的题目数量
|
||||
* @returns {Promise<number>} 题目数量
|
||||
*/
|
||||
export async function getQuestionCount() {
|
||||
const questions = await loadQuestionsData()
|
||||
return questions ? questions.length : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载答题数据(可选,用于提前加载)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function preloadQuestions() {
|
||||
try {
|
||||
await loadQuestionsData()
|
||||
// 降噪
|
||||
} catch (error) {
|
||||
console.error('❌ 答题数据预加载失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存,强制重新加载(用于调试)
|
||||
*/
|
||||
export function clearCache() {
|
||||
questionsData = null
|
||||
// 降噪
|
||||
}
|
||||
57
xyzw_web_helper-main开源源码更新/src/utils/testStudyQuestions.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 测试答题数据加载的简单脚本
|
||||
* 在浏览器控制台中运行以验证数据加载
|
||||
*/
|
||||
|
||||
import { loadQuestionsData, findAnswer, getQuestionCount } from './studyQuestionsFromJSON.js'
|
||||
|
||||
// 测试函数
|
||||
export async function testQuestionLoading() {
|
||||
console.log('🧪 开始测试答题数据加载...')
|
||||
|
||||
try {
|
||||
// 测试数据加载
|
||||
const questions = await loadQuestionsData()
|
||||
console.log(`✅ 成功加载题目数据,共 ${questions.length} 道题`)
|
||||
|
||||
// 显示前5道题
|
||||
console.log('📋 前5道题目示例:')
|
||||
for (let i = 0; i < Math.min(5, questions.length); i++) {
|
||||
const q = questions[i]
|
||||
console.log(`${i + 1}. ${q.name} -> 答案: ${q.value}`)
|
||||
}
|
||||
|
||||
// 测试查找功能
|
||||
console.log('\n🔍 测试答案查找功能:')
|
||||
|
||||
const testQuestions = [
|
||||
'《三国演义》中,「大意失街亭」的是马谩?',
|
||||
'刘备三顾茅庐请诸葛亮出山',
|
||||
'中国最长的河流是',
|
||||
'不存在的题目测试'
|
||||
]
|
||||
|
||||
for (const testQ of testQuestions) {
|
||||
const answer = await findAnswer(testQ)
|
||||
console.log(`题目: "${testQ}" -> 答案: ${answer || '未找到'}`)
|
||||
}
|
||||
|
||||
// 测试题目数量
|
||||
const count = await getQuestionCount()
|
||||
console.log(`\n📊 题目总数: ${count}`)
|
||||
|
||||
console.log('🎉 测试完成!')
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接运行这个文件
|
||||
if (typeof window !== 'undefined') {
|
||||
// 浏览器环境,将测试函数挂载到 window 对象
|
||||
window.testStudyQuestions = testQuestionLoading
|
||||
console.log('🛠️ 测试函数已挂载到 window.testStudyQuestions,可在控制台运行')
|
||||
}
|
||||
133
xyzw_web_helper-main开源源码更新/src/utils/tokenDb.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// Lightweight IndexedDB wrapper for token persistence
|
||||
|
||||
const DB_NAME = 'xyzw_token_db'
|
||||
const DB_VERSION = 1
|
||||
const STORE_KV = 'kv'
|
||||
const STORE_GAME_TOKENS = 'gameTokens'
|
||||
|
||||
function openDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, DB_VERSION)
|
||||
|
||||
req.onupgradeneeded = (event) => {
|
||||
const db = req.result
|
||||
if (!db.objectStoreNames.contains(STORE_KV)) {
|
||||
db.createObjectStore(STORE_KV, { keyPath: 'key' })
|
||||
}
|
||||
if (!db.objectStoreNames.contains(STORE_GAME_TOKENS)) {
|
||||
db.createObjectStore(STORE_GAME_TOKENS, { keyPath: 'roleId' })
|
||||
}
|
||||
}
|
||||
|
||||
req.onsuccess = () => resolve(req.result)
|
||||
req.onerror = () => reject(req.error)
|
||||
})
|
||||
}
|
||||
|
||||
async function withStore(storeName, mode, fn) {
|
||||
const db = await openDB()
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, mode)
|
||||
const store = tx.objectStore(storeName)
|
||||
const result = fn(store)
|
||||
tx.oncomplete = () => resolve(result)
|
||||
tx.onerror = () => reject(tx.error)
|
||||
tx.onabort = () => reject(tx.error)
|
||||
})
|
||||
}
|
||||
|
||||
// KV helpers
|
||||
export async function getKV(key) {
|
||||
return withStore(STORE_KV, 'readonly', (store) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = store.get(key)
|
||||
req.onsuccess = () => resolve(req.result ? req.result.value : undefined)
|
||||
req.onerror = () => reject(req.error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function setKV(key, value) {
|
||||
return withStore(STORE_KV, 'readwrite', (store) => {
|
||||
store.put({ key, value })
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteKV(key) {
|
||||
return withStore(STORE_KV, 'readwrite', (store) => {
|
||||
store.delete(key)
|
||||
})
|
||||
}
|
||||
|
||||
// User token
|
||||
export async function getUserToken() { return getKV('userToken') }
|
||||
export async function setUserToken(token) { return setKV('userToken', token) }
|
||||
export async function clearUserToken() { return deleteKV('userToken') }
|
||||
|
||||
// Game tokens (per role)
|
||||
export async function getAllGameTokens() {
|
||||
return withStore(STORE_GAME_TOKENS, 'readonly', (store) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = store.getAll()
|
||||
req.onsuccess = () => {
|
||||
const arr = req.result || []
|
||||
const map = {}
|
||||
arr.forEach((t) => { if (t && t.roleId) map[t.roleId] = t })
|
||||
resolve(map)
|
||||
}
|
||||
req.onerror = () => reject(req.error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function putGameToken(roleId, tokenData) {
|
||||
return withStore(STORE_GAME_TOKENS, 'readwrite', (store) => {
|
||||
store.put({ ...tokenData, roleId })
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteGameToken(roleId) {
|
||||
return withStore(STORE_GAME_TOKENS, 'readwrite', (store) => {
|
||||
store.delete(roleId)
|
||||
})
|
||||
}
|
||||
|
||||
export async function clearGameTokens() {
|
||||
return withStore(STORE_GAME_TOKENS, 'readwrite', (store) => {
|
||||
store.clear()
|
||||
})
|
||||
}
|
||||
|
||||
// Migration from localStorage for backward compatibility
|
||||
export async function migrateFromLocalStorageIfNeeded() {
|
||||
try {
|
||||
const existing = await getAllGameTokens()
|
||||
const hasAny = existing && Object.keys(existing).length > 0
|
||||
const userTok = await getUserToken()
|
||||
const hasUser = !!userTok
|
||||
|
||||
// If DB already has data, skip
|
||||
if (hasAny || hasUser) return { migrated: false }
|
||||
|
||||
// Try migrate from localStorage
|
||||
const lsUser = localStorage.getItem('userToken')
|
||||
const lsGameTokensRaw = localStorage.getItem('gameTokens')
|
||||
let lsGameTokens = {}
|
||||
try { lsGameTokens = lsGameTokensRaw ? JSON.parse(lsGameTokensRaw) : {} } catch { lsGameTokens = {} }
|
||||
|
||||
const lsHasAny = lsUser || (lsGameTokens && Object.keys(lsGameTokens).length > 0)
|
||||
if (!lsHasAny) return { migrated: false }
|
||||
|
||||
if (lsUser) await setUserToken(lsUser)
|
||||
for (const [roleId, tokenData] of Object.entries(lsGameTokens || {})) {
|
||||
await putGameToken(roleId, tokenData)
|
||||
}
|
||||
|
||||
// Optional: do not remove localStorage to avoid surprises
|
||||
return { migrated: true }
|
||||
} catch (e) {
|
||||
console.warn('Token DB migration skipped:', e)
|
||||
return { migrated: false, error: e?.message }
|
||||
}
|
||||
}
|
||||
|
||||
444
xyzw_web_helper-main开源源码更新/src/utils/wsAgent.js
Normal file
@@ -0,0 +1,444 @@
|
||||
/**
|
||||
* 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: {},
|
||||
cmd: '_sys/ack',
|
||||
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
|
||||
}
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
/**
|
||||
* 原始发送数据
|
||||
* @private
|
||||
*/
|
||||
_rawSend(packet) {
|
||||
try {
|
||||
// 发送前日志(仅标准五段)
|
||||
if (packet?.cmd && packet.cmd !== '_sys/ack') {
|
||||
const bodyForLog = (packet.body instanceof Uint8Array || Array.isArray(packet.body)) ? '[BON]' : (packet.body || {})
|
||||
console.info('📤 发送报文', {
|
||||
cmd: packet.cmd,
|
||||
ack: packet.ack ?? 0,
|
||||
seq: packet.seq ?? 0,
|
||||
time: packet.time,
|
||||
body: bodyForLog
|
||||
})
|
||||
}
|
||||
// 使用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
|
||||
758
xyzw_web_helper-main开源源码更新/src/utils/xyzwWebSocket.js
Normal file
@@ -0,0 +1,758 @@
|
||||
/**
|
||||
* XYZW WebSocket 客户端
|
||||
* 基于 readable-xyzw-ws.js 重构,适配本项目架构
|
||||
*/
|
||||
|
||||
import { bonProtocol, g_utils } from './bonProtocol.js'
|
||||
import { wsLogger, gameLogger } from './logger.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,
|
||||
time: Date.now(),
|
||||
body: this.encoder?.bon?.encode
|
||||
? this.encoder.bon.encode({ ...defaultBody, ...params })
|
||||
: { ...defaultBody, ...params },
|
||||
}))
|
||||
return this
|
||||
}
|
||||
|
||||
/** 特例:系统心跳的 ack 用的是 "_sys/ack" */
|
||||
registerHeartbeat() {
|
||||
this.commands.set("heart_beat", (ack, seq) => ({
|
||||
cmd: "_sys/ack",
|
||||
ack,
|
||||
seq,
|
||||
time: Date.now(),
|
||||
body: {},
|
||||
}))
|
||||
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("system_mysharecallback", { isSkipShareCard: true, type: 2 })
|
||||
|
||||
// 任务相关
|
||||
.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_purchase", { goodsId: 1 })
|
||||
.register("store_refresh", { storeId: 1 })
|
||||
|
||||
// 军团
|
||||
.register("legion_getinfo")
|
||||
.register("legion_signin")
|
||||
.register("legion_getwarrank")
|
||||
.register("legionwar_getdetails")
|
||||
|
||||
// 邮件
|
||||
.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("genie_sweep", { genieId: 1 })
|
||||
.register("genie_buysweep")
|
||||
|
||||
// 礼包相关
|
||||
.register("discount_claimreward", { discountId: 1 })
|
||||
.register("card_claimreward", { cardId: 1 })
|
||||
|
||||
// 爬塔相关
|
||||
.register("tower_getinfo")
|
||||
.register("tower_claimreward")
|
||||
|
||||
// 队伍相关
|
||||
.register("presetteam_getinfo")
|
||||
.register("presetteam_getinfo")
|
||||
.register("presetteam_setteam")
|
||||
.register("presetteam_saveteam", { teamId: 1 })
|
||||
.register("role_gettargetteam")
|
||||
|
||||
// 排名相关
|
||||
.register("rank_getroleinfo")
|
||||
|
||||
// 梦魇相关
|
||||
.register("nightmare_getroleinfo")
|
||||
// 活动/任务
|
||||
.register("activity_get")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = 0
|
||||
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.isReconnecting = false // 重连状态标志
|
||||
|
||||
this.promises = Object.create(null)
|
||||
this.registry = registerDefaultCommands(new CommandRegistry(this.utils, this.enc))
|
||||
|
||||
// WebSocket客户端初始化
|
||||
|
||||
// 状态回调
|
||||
this.onConnect = null
|
||||
this.onDisconnect = null
|
||||
this.onError = null
|
||||
}
|
||||
|
||||
/** 初始化连接 */
|
||||
init() {
|
||||
wsLogger.info(`连接: ${this.url.split('?')[0]}`)
|
||||
|
||||
this.socket = new WebSocket(this.url)
|
||||
|
||||
this.socket.onopen = () => {
|
||||
wsLogger.info('连接成功')
|
||||
this.connected = true
|
||||
// 启动心跳机制
|
||||
this._setupHeartbeat()
|
||||
// 启动消息队列处理
|
||||
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
|
||||
|
||||
// 移除特定命令的控制台直出日志,统一用 wsLogger/gameLogger 控制
|
||||
} else if (evt.data instanceof Blob) {
|
||||
// 处理Blob数据
|
||||
// 收到Blob数据
|
||||
evt.data.arrayBuffer().then(buffer => {
|
||||
try {
|
||||
packet = this.utils?.parse ? this.utils.parse(buffer, "auto") : buffer
|
||||
// Blob解析完成
|
||||
|
||||
// 处理消息体解码(ProtoMsg会自动解码)
|
||||
if (packet instanceof Object && packet.rawData !== undefined) {
|
||||
gameLogger.verbose('ProtoMsg Blob消息,使用rawData:', packet.rawData)
|
||||
} else if (packet.body && this.shouldDecodeBody(packet.body)) {
|
||||
try {
|
||||
if (this.utils && this.utils.bon && this.utils.bon.decode) {
|
||||
// 转换body数据为Uint8Array
|
||||
const bodyBytes = this.convertToUint8Array(packet.body)
|
||||
if (bodyBytes) {
|
||||
const decodedBody = this.utils.bon.decode(bodyBytes)
|
||||
gameLogger.debug('BON Blob解码成功:', packet.cmd, decodedBody)
|
||||
// 不修改packet.body,而是创建一个新的属性存储解码后的数据
|
||||
packet.decodedBody = decodedBody
|
||||
}
|
||||
} else {
|
||||
gameLogger.warn('BON解码器不可用 (Blob)')
|
||||
}
|
||||
} catch (error) {
|
||||
gameLogger.error('BON Blob消息体解码失败:', error.message, packet.cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 ack 为服务端最新的 seq(若存在)
|
||||
const actualPacket = packet._raw || packet
|
||||
const incomingSeq = (typeof actualPacket?.seq === 'number') ? actualPacket.seq :
|
||||
(typeof packet?.seq === 'number') ? packet.seq : undefined
|
||||
if (typeof incomingSeq === 'number' && incomingSeq >= 0) {
|
||||
this.ack = incomingSeq
|
||||
}
|
||||
|
||||
if (this.showMsg) {
|
||||
// 收到Blob消息
|
||||
}
|
||||
|
||||
// 回调处理
|
||||
if (this.messageListener) {
|
||||
this.messageListener(packet)
|
||||
}
|
||||
|
||||
// Promise 响应处理
|
||||
this._handlePromiseResponse(packet)
|
||||
|
||||
} catch (error) {
|
||||
gameLogger.error('Blob解析失败:', error.message)
|
||||
}
|
||||
})
|
||||
return // 异步处理,直接返回
|
||||
} else {
|
||||
gameLogger.warn('未知数据类型:', typeof evt.data, evt.data)
|
||||
packet = evt.data
|
||||
}
|
||||
|
||||
if (this.showMsg) {
|
||||
gameLogger.verbose('收到消息:', packet)
|
||||
}
|
||||
|
||||
// 处理消息体解码(ProtoMsg会自动解码)
|
||||
if (packet instanceof Object && packet.rawData !== undefined) {
|
||||
gameLogger.verbose('ProtoMsg消息,使用rawData:', packet.rawData)
|
||||
} else {
|
||||
// 处理可能存在_raw包装的情况
|
||||
const actualPacket = packet._raw || packet
|
||||
|
||||
// 更新 ack 为服务端最新的 seq(若存在)
|
||||
const incomingSeq = (typeof actualPacket.seq === 'number') ? actualPacket.seq :
|
||||
(typeof packet.seq === 'number') ? packet.seq : undefined
|
||||
if (typeof incomingSeq === 'number' && incomingSeq >= 0) {
|
||||
this.ack = incomingSeq
|
||||
}
|
||||
|
||||
if (actualPacket.body && this.shouldDecodeBody(actualPacket.body)) {
|
||||
try {
|
||||
if (this.utils && this.utils.bon && this.utils.bon.decode) {
|
||||
// 转换body数据为Uint8Array
|
||||
const bodyBytes = this.convertToUint8Array(actualPacket.body)
|
||||
if (bodyBytes) {
|
||||
const decodedBody = this.utils.bon.decode(bodyBytes)
|
||||
gameLogger.debug('BON解码成功:', actualPacket.cmd || packet.cmd, decodedBody)
|
||||
// 将解码后的数据存储到原始packet中
|
||||
packet.decodedBody = decodedBody
|
||||
// 如果有_raw结构,也存储到_raw中
|
||||
if (packet._raw) {
|
||||
packet._raw.decodedBody = decodedBody
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gameLogger.warn('BON解码器不可用')
|
||||
}
|
||||
} catch (error) {
|
||||
gameLogger.error('BON消息体解码失败:', error.message, actualPacket.cmd || packet.cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回调处理
|
||||
if (this.messageListener) {
|
||||
this.messageListener(packet)
|
||||
}
|
||||
|
||||
// Promise 响应处理
|
||||
this._handlePromiseResponse(packet)
|
||||
|
||||
} catch (error) {
|
||||
gameLogger.error('消息处理失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
this.socket.onclose = (evt) => {
|
||||
wsLogger.info(`WebSocket 连接关闭: ${evt.code} ${evt.reason || ''}`)
|
||||
wsLogger.debug('关闭详情:', {
|
||||
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) => {
|
||||
wsLogger.error('WebSocket 错误:', error)
|
||||
this.connected = false
|
||||
this._clearTimers()
|
||||
if (this.onError) this.onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 注册消息回调 */
|
||||
setMessageListener(fn) {
|
||||
this.messageListener = fn
|
||||
}
|
||||
|
||||
/** 控制台消息开关 */
|
||||
setShowMsg(val) {
|
||||
this.showMsg = !!val
|
||||
}
|
||||
|
||||
/** 判断是否需要解码body */
|
||||
shouldDecodeBody(body) {
|
||||
if (!body) return false
|
||||
|
||||
// Uint8Array或Array格式
|
||||
if (body instanceof Uint8Array || Array.isArray(body)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 对象格式的数字数组(从图片中看到的格式)
|
||||
if (typeof body === 'object' && body.constructor === Object) {
|
||||
// 检查是否是数字键的对象(例如 {"0": 8, "1": 2, ...})
|
||||
const keys = Object.keys(body)
|
||||
return keys.length > 0 && keys.every(key => !isNaN(parseInt(key)))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/** 转换body为Uint8Array */
|
||||
convertToUint8Array(body) {
|
||||
if (!body) return null
|
||||
|
||||
if (body instanceof Uint8Array) {
|
||||
return body
|
||||
}
|
||||
|
||||
if (Array.isArray(body)) {
|
||||
return new Uint8Array(body)
|
||||
}
|
||||
|
||||
// 对象格式的数字数组转换为Uint8Array
|
||||
if (typeof body === 'object' && body.constructor === Object) {
|
||||
const keys = Object.keys(body).map(k => parseInt(k)).sort((a, b) => a - b)
|
||||
if (keys.length > 0) {
|
||||
const maxIndex = Math.max(...keys)
|
||||
const arr = new Array(maxIndex + 1).fill(0)
|
||||
for (const [key, value] of Object.entries(body)) {
|
||||
const index = parseInt(key)
|
||||
if (!isNaN(index) && typeof value === 'number') {
|
||||
arr[index] = value
|
||||
}
|
||||
}
|
||||
gameLogger.debug('转换对象格式body为Uint8Array:', arr.length, 'bytes')
|
||||
return new Uint8Array(arr)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/** 重连(防重复连接版本) */
|
||||
reconnect() {
|
||||
// 防止重复重连
|
||||
if (this.isReconnecting) {
|
||||
wsLogger.debug('重连已在进行中,跳过此次重连请求')
|
||||
return
|
||||
}
|
||||
|
||||
this.isReconnecting = true
|
||||
wsLogger.info('开始WebSocket重连...')
|
||||
|
||||
// 先断开现有连接
|
||||
this.disconnect()
|
||||
|
||||
// 延迟重连,避免过于频繁
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.init()
|
||||
} finally {
|
||||
// 无论成功或失败都重置重连状态
|
||||
setTimeout(() => {
|
||||
this.isReconnecting = false
|
||||
}, 2000) // 2秒后允许下次重连
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/** 断开连接 */
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close()
|
||||
this.socket = null
|
||||
}
|
||||
this.connected = false
|
||||
this._clearTimers()
|
||||
}
|
||||
|
||||
/** 发送消息 */
|
||||
send(cmd, params = {}, options = {}) {
|
||||
if (!this.connected) {
|
||||
wsLogger.warn(`WebSocket 未连接,消息已入队: ${cmd}`)
|
||||
// 防止频繁重连
|
||||
if (!this.dialogStatus && !this.isReconnecting) {
|
||||
this.dialogStatus = true
|
||||
wsLogger.info('自动触发重连...')
|
||||
this.reconnect()
|
||||
setTimeout(() => { this.dialogStatus = false }, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除特定命令的控制台直出日志,统一用 wsLogger 控制
|
||||
|
||||
// 统一在入队时分配 seq,避免与 Promise 版本竞争导致重复
|
||||
const assignedSeq = (options.seq !== undefined)
|
||||
? options.seq
|
||||
: (cmd === 'heart_beat' ? 0 : ++this.seq)
|
||||
|
||||
const task = {
|
||||
cmd,
|
||||
params,
|
||||
seq: assignedSeq,
|
||||
respKey: options.respKey || cmd,
|
||||
sleep: options.sleep || 0,
|
||||
onSent: options.onSent
|
||||
}
|
||||
|
||||
this.sendQueue.push(task)
|
||||
return task
|
||||
}
|
||||
|
||||
/** Promise 版发送 */
|
||||
sendWithPromise(cmd, params = {}, timeoutMs = 5000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.connected && !this.socket) {
|
||||
return reject(new Error("WebSocket 连接已关闭"))
|
||||
}
|
||||
|
||||
// 为此请求生成唯一的seq值
|
||||
const requestSeq = ++this.seq
|
||||
|
||||
// 设置 Promise 状态,使用seq作为键
|
||||
this.promises[requestSeq] = { resolve, reject, originalCmd: cmd }
|
||||
|
||||
// 超时处理
|
||||
const timer = setTimeout(() => {
|
||||
delete this.promises[requestSeq]
|
||||
reject(new Error(`请求超时: ${cmd} (${timeoutMs}ms)`))
|
||||
}, timeoutMs)
|
||||
|
||||
// 发送消息,直接传递seq
|
||||
this.send(cmd, params, {
|
||||
seq: requestSeq,
|
||||
onSent: () => {
|
||||
// 消息发送成功后,不要清除超时器,让它继续等待响应
|
||||
// 只有在收到响应或超时时才清除
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 发送心跳 */
|
||||
sendHeartbeat() {
|
||||
wsLogger.verbose('发送心跳消息')
|
||||
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) {
|
||||
wsLogger.debug('开始发送首次心跳')
|
||||
this.sendHeartbeat()
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
// 设置定期心跳
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
|
||||
this.sendHeartbeat()
|
||||
} else {
|
||||
wsLogger.warn('心跳检查失败: 连接状态异常')
|
||||
}
|
||||
}, 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 {
|
||||
// 直接使用任务指定的 seq(已在入队时分配)
|
||||
const raw = this.registry.build(task.cmd, this.ack, task.seq, task.params)
|
||||
|
||||
// 发送前日志(仅标准五段)
|
||||
if (raw && raw.cmd !== '_sys/ack') {
|
||||
let bodyForLog
|
||||
try {
|
||||
if (raw.body instanceof Uint8Array || Array.isArray(raw.body)) {
|
||||
bodyForLog = '[BON]'
|
||||
} else if (raw.body && typeof raw.body === 'object' && raw.body.constructor === Object && Object.keys(raw.body).every(k => !isNaN(parseInt(k)))) {
|
||||
bodyForLog = '[BON]'
|
||||
} else {
|
||||
bodyForLog = raw.body || {}
|
||||
}
|
||||
} catch {
|
||||
bodyForLog = '[BODY]'
|
||||
}
|
||||
wsLogger.info('📤 发送报文', {
|
||||
cmd: raw.cmd,
|
||||
ack: raw.ack ?? 0,
|
||||
seq: raw.seq ?? 0,
|
||||
time: raw.time,
|
||||
body: bodyForLog
|
||||
})
|
||||
}
|
||||
|
||||
// 自增逻辑已在入队时统一处理,这里不再修改 this.seq
|
||||
|
||||
// 编码并发送
|
||||
const bin = this.registry.encodePacket(raw)
|
||||
this.socket?.send(bin)
|
||||
|
||||
if (this.showMsg || task.cmd === "heart_beat") {
|
||||
wsLogger.wsMessage('local', task.cmd, false)
|
||||
if (this.showMsg) {
|
||||
wsLogger.verbose('原始数据:', raw)
|
||||
wsLogger.verbose('编码后数据:', bin)
|
||||
wsLogger.verbose('编码类型:', typeof bin, bin instanceof Uint8Array ? 'Uint8Array (加密)' : 'String (明文)')
|
||||
if (bin instanceof Uint8Array && bin.length > 0) {
|
||||
wsLogger.verbose(`加密验证: 前8字节 [${Array.from(bin.slice(0, 8)).join(', ')}]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发发送回调
|
||||
if (task.onSent) {
|
||||
try {
|
||||
task.onSent(task.respKey, task.cmd)
|
||||
} catch (error) {
|
||||
wsLogger.warn('发送回调执行失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 可选延时
|
||||
if (task.sleep) await sleep(task.sleep)
|
||||
|
||||
} catch (error) {
|
||||
wsLogger.error(`发送消息失败: ${task.cmd}`, error)
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
/** 处理 Promise 响应 */
|
||||
_handlePromiseResponse(packet) {
|
||||
// 优先使用resp字段进行响应匹配(新的正确方式)
|
||||
if (packet.resp !== undefined && this.promises[packet.resp]) {
|
||||
const promiseData = this.promises[packet.resp]
|
||||
delete this.promises[packet.resp]
|
||||
|
||||
// 获取响应数据,优先使用 rawData(ProtoMsg 自动解码),然后 decodedBody(手动解码),最后 body
|
||||
const responseBody = packet.rawData !== undefined ? packet.rawData :
|
||||
packet.decodedBody !== undefined ? packet.decodedBody :
|
||||
packet.body
|
||||
|
||||
if (packet.code === 0 || packet.code === undefined) {
|
||||
promiseData.resolve(responseBody || packet)
|
||||
} else {
|
||||
promiseData.reject(new Error(`服务器错误: ${packet.code} - ${packet.hint || '未知错误'}`))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 兼容旧的基于cmd名称的匹配方式(保留为向后兼容)
|
||||
const cmd = packet.cmd
|
||||
if (!cmd) return
|
||||
const respCmdKey = typeof cmd === 'string' ? cmd.toLowerCase() : cmd
|
||||
|
||||
// 命令到响应的映射 - 处理响应命令与原始命令不匹配的情况
|
||||
const responseToCommandMap = {
|
||||
// 1:1 响应映射(优先级高)
|
||||
'studyresp':'study_startgame',
|
||||
'role_getroleinforesp': 'role_getroleinfo',
|
||||
'hero_recruitresp': 'hero_recruit',
|
||||
'friend_batchresp': 'friend_batch',
|
||||
'system_claimhanguprewardresp': 'system_claimhangupreward',
|
||||
'item_openboxresp': 'item_openbox',
|
||||
'bottlehelper_claimresp': 'bottlehelper_claim',
|
||||
'bottlehelper_startresp': 'bottlehelper_start',
|
||||
'bottlehelper_stopresp': 'bottlehelper_stop',
|
||||
'legion_signinresp': 'legion_signin',
|
||||
'fight_startbossresp': 'fight_startboss',
|
||||
'fight_startlegionbossresp': 'fight_startlegionboss',
|
||||
'fight_startareaarenaresp': 'fight_startareaarena',
|
||||
'arena_startarearesp': 'arena_startarea',
|
||||
'arena_getareatargetresp': 'arena_getareatarget',
|
||||
'presetteam_saveteamresp': 'presetteam_saveteam',
|
||||
'presetteam_getinforesp': 'presetteam_getinfo',
|
||||
'mail_claimallattachmentresp': 'mail_claimallattachment',
|
||||
'store_buyresp': 'store_purchase',
|
||||
'system_getdatabundleverresp': 'system_getdatabundlever',
|
||||
'tower_claimrewardresp': 'tower_claimreward',
|
||||
'fight_starttowerresp': 'fight_starttower',
|
||||
// 军团信息
|
||||
'legion_getinforesp': 'legion_getinfo',
|
||||
'legion_getinforresp': 'legion_getinfo',
|
||||
|
||||
// 特殊响应映射 - 有些命令有独立响应,有些用同步响应
|
||||
'task_claimdailyrewardresp': 'task_claimdailyreward',
|
||||
'task_claimweekrewardresp': 'task_claimweekreward',
|
||||
|
||||
// 同步响应映射(优先级低)
|
||||
'syncresp': ['system_mysharecallback', 'task_claimdailypoint'],
|
||||
'syncrewardresp': ['system_buygold', 'discount_claimreward', 'card_claimreward',
|
||||
'artifact_lottery', 'genie_sweep', 'genie_buysweep','system_signinreward']
|
||||
}
|
||||
|
||||
// 获取原始命令名(支持一对一和一对多映射)
|
||||
// 使用小写进行映射匹配,兼容服务端大小写差异
|
||||
let originalCmds = responseToCommandMap[respCmdKey]
|
||||
if (!originalCmds) {
|
||||
originalCmds = [respCmdKey] // 如果没有映射,使用响应命令本身(小写)
|
||||
} else if (typeof originalCmds === 'string') {
|
||||
originalCmds = [originalCmds] // 转换为数组
|
||||
}
|
||||
|
||||
// 查找对应的 Promise - 遍历所有等待中的 Promise(向后兼容)
|
||||
for (const [requestId, promiseData] of Object.entries(this.promises)) {
|
||||
// 检查 Promise 是否匹配当前响应的任一原始命令
|
||||
if (originalCmds.includes(promiseData.originalCmd)) {
|
||||
delete this.promises[requestId]
|
||||
|
||||
// 获取响应数据,优先使用 rawData(ProtoMsg 自动解码),然后 decodedBody(手动解码),最后 body
|
||||
const responseBody = packet.rawData !== undefined ? packet.rawData :
|
||||
packet.decodedBody !== undefined ? packet.decodedBody :
|
||||
packet.body
|
||||
|
||||
if (packet.code === 0 || packet.code === undefined) {
|
||||
promiseData.resolve(responseBody || packet)
|
||||
} else {
|
||||
promiseData.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
|
||||
845
xyzw_web_helper-main开源源码更新/src/views/DailyTasks.vue
Normal file
@@ -0,0 +1,845 @@
|
||||
<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'
|
||||
}
|
||||
]
|
||||
|
||||
// 等待WebSocket连接并加载阵容数据
|
||||
const loadTeamDataWithConnection = async (tokenId, maxRetries = 3, retryDelay = 2000) => {
|
||||
// 降噪
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
// 检查WebSocket连接状态
|
||||
const wsStatus = tokenStore.getWebSocketStatus(tokenId)
|
||||
// 降噪
|
||||
|
||||
if (wsStatus !== 'connected') {
|
||||
// 降噪
|
||||
|
||||
// 尝试建立WebSocket连接
|
||||
const tokenData = tokenStore.gameTokens.find(t => t.id === tokenId)
|
||||
if (tokenData && tokenData.token) {
|
||||
// 触发WebSocket连接
|
||||
tokenStore.createWebSocketConnection(tokenId, tokenData.token, tokenData.wsUrl)
|
||||
|
||||
// 等待连接建立
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay))
|
||||
|
||||
// 再次检查连接状态
|
||||
const newStatus = tokenStore.getWebSocketStatus(tokenId)
|
||||
if (newStatus !== 'connected') {
|
||||
if (attempt < maxRetries) {
|
||||
// 降噪
|
||||
continue
|
||||
} else {
|
||||
throw new Error('WebSocket连接超时')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('未找到有效的Token数据或WebSocket URL')
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket已连接,开始加载阵容数据
|
||||
// 降噪
|
||||
const result = await tokenStore.sendMessageWithPromise(
|
||||
tokenId,
|
||||
'presetteam_getinfo',
|
||||
{},
|
||||
8000
|
||||
)
|
||||
|
||||
if (result) {
|
||||
// 更新到游戏数据缓存中
|
||||
tokenStore.$patch((state) => {
|
||||
state.gameData = { ...(state.gameData ?? {}), presetTeam: result }
|
||||
})
|
||||
// 降噪
|
||||
message.success('阵容数据已更新')
|
||||
return result
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`第${attempt}次尝试失败:`, error)
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
// 降噪
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay))
|
||||
} else {
|
||||
console.error('所有重试均失败,阵容数据加载失败')
|
||||
message.warning(`阵容数据加载失败: ${error.message || '未知错误'}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 方法
|
||||
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执行任务
|
||||
// 降噪
|
||||
|
||||
// 更新本地任务状态
|
||||
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()
|
||||
}
|
||||
|
||||
// 页面进入时手动调用阵容加载接口,确保WebSocket连接后再调用
|
||||
if (tokenStore.selectedToken) {
|
||||
await loadTeamDataWithConnection(tokenStore.selectedToken.id)
|
||||
}
|
||||
|
||||
// 设置默认选中的角色
|
||||
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%);
|
||||
}
|
||||
|
||||
/* 深色主题下背景 */
|
||||
[data-theme="dark"] .daily-tasks-page {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1f2937 100%);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: var(--spacing-xl) 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 深色主题下头部渐变 */
|
||||
[data-theme="dark"] .page-header {
|
||||
background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
|
||||
}
|
||||
|
||||
.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: var(--bg-primary);
|
||||
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: var(--bg-primary);
|
||||
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>
|
||||
866
xyzw_web_helper-main开源源码更新/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,866 @@
|
||||
<template>
|
||||
<div class="dashboard-page">
|
||||
<!-- 顶部导航 -->
|
||||
<nav class="dashboard-nav">
|
||||
<div class="nav-container">
|
||||
<div class="nav-brand">
|
||||
<img
|
||||
src="/icons/xiaoyugan.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">
|
||||
<!-- 主题切换按钮 -->
|
||||
<ThemeToggle />
|
||||
|
||||
<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="handleManageTokens"
|
||||
>
|
||||
管理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 ThemeToggle from '@/components/ThemeToggle.vue'
|
||||
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 handleManageTokens = () => {
|
||||
// 降噪
|
||||
/* 当前Token状态:
|
||||
hasTokens: tokenStore.hasTokens,
|
||||
selectedToken: tokenStore.selectedToken?.name,
|
||||
tokenCount: tokenStore.gameTokens.length
|
||||
*/
|
||||
|
||||
try {
|
||||
router.push('/tokens')
|
||||
// 降噪
|
||||
} catch (error) {
|
||||
console.error('❌ 导航失败:', error)
|
||||
message.error('导航到Token管理页面失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuickAction = (action) => {
|
||||
switch (action.action) {
|
||||
case 'game-features':
|
||||
router.push('/game-features')
|
||||
break
|
||||
case 'add-token':
|
||||
handleManageTokens()
|
||||
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: var(--bg-primary);
|
||||
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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.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: var(--bg-primary);
|
||||
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: var(--bg-primary);
|
||||
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: var(--bg-primary);
|
||||
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>
|
||||
589
xyzw_web_helper-main开源源码更新/src/views/GameFeatures.vue
Normal file
@@ -0,0 +1,589 @@
|
||||
<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 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, watch } 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 isConnected = computed(() => {
|
||||
return connectionStatus.value === 'connected'
|
||||
})
|
||||
|
||||
|
||||
// 方法
|
||||
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('开始爬塔挑战...')
|
||||
// 关键业务:只提示 UI,不打印冗余日志
|
||||
// 实际请求体: {"ack":0,"body":{},"cmd":"fight_starttower","seq":XX,"time":TIMESTAMP}
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听当前选中 Token 的连接错误(如 token 过期)并给出明确提示
|
||||
watch(
|
||||
() => {
|
||||
if (!tokenStore.selectedToken) return { status: 'disconnected', lastError: null }
|
||||
const conn = tokenStore.wsConnections[tokenStore.selectedToken.id]
|
||||
return { status: conn?.status, lastError: conn?.lastError }
|
||||
},
|
||||
(cur) => {
|
||||
if (!cur) return
|
||||
if (cur.status === 'error' && cur.lastError) {
|
||||
const err = String(cur.lastError.error || '').toLowerCase()
|
||||
if (err.includes('token') && err.includes('expired')) {
|
||||
message.error('当前 Token 已过期,请重新导入后再试')
|
||||
router.push('/tokens')
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 初始化游戏数据
|
||||
const initializeGameData = async () => {
|
||||
if (!tokenStore.selectedToken) return
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
// 获取初始化数据(静默)
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
tokenStore.sendMessage(tokenId, 'tower_getinfo')
|
||||
tokenStore.sendMessage(tokenId, 'presetteam_getinfo')
|
||||
} catch (error) {
|
||||
// 静默处理初始化异常
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
// WebSocket 连接由 tokenStore 管理,不需要手动清理
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.game-features-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
/* 深色主题下背景 */
|
||||
[data-theme="dark"] .game-features-page {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1f2937 100%);
|
||||
}
|
||||
|
||||
// 页面头部
|
||||
.page-header {
|
||||
background: var(--bg-primary);
|
||||
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: var(--bg-primary);
|
||||
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: var(--bg-primary);
|
||||
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
xyzw_web_helper-main开源源码更新/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: var(--bg-primary);
|
||||
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>
|
||||
609
xyzw_web_helper-main开源源码更新/src/views/Home.vue
Normal file
@@ -0,0 +1,609 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar glass">
|
||||
<div class="container">
|
||||
<div class="nav-content">
|
||||
<div class="nav-brand">
|
||||
<img
|
||||
src="/icons/xiaoyugan.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/xiaoyugan.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, var(--bg-primary), var(--primary-color-light));
|
||||
-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: var(--bg-primary);
|
||||
}
|
||||
|
||||
.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%);
|
||||
}
|
||||
/* 深色主题下统计区背景 */
|
||||
[data-theme="dark"] .stats-section {
|
||||
background: linear-gradient(135deg, #111827 0%, #1f2937 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>
|
||||
565
xyzw_web_helper-main开源源码更新/src/views/Login.vue
Normal file
@@ -0,0 +1,565 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-container">
|
||||
<!-- 登录表单卡片 -->
|
||||
<div class="login-card glass">
|
||||
<div class="card-header">
|
||||
<div class="brand">
|
||||
<img
|
||||
src="/icons/xiaoyugan.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;
|
||||
}
|
||||
|
||||
/* 深色主题下背景 */
|
||||
[data-theme="dark"] .login-page {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1f2937 100%);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 深色主题下登录卡片 */
|
||||
[data-theme="dark"] .login-card {
|
||||
background: rgba(17, 24, 39, 0.85);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.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>
|
||||
121
xyzw_web_helper-main开源源码更新/src/views/NotFound.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<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);
|
||||
}
|
||||
|
||||
/* 深色主题下背景 */
|
||||
[data-theme="dark"] .not-found-page {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1f2937 100%);
|
||||
}
|
||||
|
||||
.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>
|
||||
567
xyzw_web_helper-main开源源码更新/src/views/Profile.vue
Normal file
@@ -0,0 +1,567 @@
|
||||
<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
|
||||
localStorage.setItem('theme', theme)
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
} else if (theme === 'light') {
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
} else {
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
if (prefersDark) document.documentElement.setAttribute('data-theme', 'dark')
|
||||
else document.documentElement.removeAttribute('data-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: var(--bg-primary);
|
||||
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>
|
||||
344
xyzw_web_helper-main开源源码更新/src/views/Register.vue
Normal file
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<div class="register-page">
|
||||
<div class="register-container">
|
||||
<div class="register-card glass">
|
||||
<div class="card-header">
|
||||
<div class="brand">
|
||||
<img
|
||||
src="/icons/xiaoyugan.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);
|
||||
}
|
||||
|
||||
/* 深色主题下背景 */
|
||||
[data-theme="dark"] .register-page {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1f2937 100%);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 深色主题下注册卡片 */
|
||||
[data-theme="dark"] .register-card {
|
||||
background: rgba(17, 24, 39, 0.85);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.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>
|
||||
1831
xyzw_web_helper-main开源源码更新/src/views/TokenImport.vue
Normal file
18
xyzw_web_helper-main开源源码更新/staticwebapp.config.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"navigationFallback": {
|
||||
"rewrite": "/index.html",
|
||||
"exclude": ["/images/*.{png,jpg,gif}", "/css/*", "/js/*", "/*.{ico,svg,woff,woff2,ttf,eot}"]
|
||||
},
|
||||
"mimeTypes": {
|
||||
".json": "text/json"
|
||||
},
|
||||
"globalHeaders": {
|
||||
"Cache-Control": "no-cache"
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"route": "/api/*",
|
||||
"allowedRoles": ["anonymous"]
|
||||
}
|
||||
]
|
||||
}
|
||||
29
xyzw_web_helper-main开源源码更新/vite.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
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,
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@use "@/assets/styles/variables.scss" as vars;'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||