Initial public release
This commit is contained in:
78
src/App.vue
Normal file
78
src/App.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<n-config-provider :theme="theme">
|
||||
<n-message-provider>
|
||||
<n-loading-bar-provider>
|
||||
<n-notification-provider>
|
||||
<n-dialog-provider>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</n-dialog-provider>
|
||||
</n-notification-provider>
|
||||
</n-loading-bar-provider>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
|
||||
// 主题控制
|
||||
const theme = ref(null)
|
||||
|
||||
// 检查用户偏好的主题
|
||||
const checkThemePreference = () => {
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
||||
theme.value = darkTheme
|
||||
} else {
|
||||
theme.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkThemePreference()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
background: var(--app-background, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
|
||||
}
|
||||
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
134
src/api/index.js
Normal file
134
src/api/index.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import axios from 'axios'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// 创建axios实例
|
||||
const request = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.token) {
|
||||
config.headers.Authorization = `Bearer ${authStore.token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data
|
||||
|
||||
// 统一处理响应格式
|
||||
if (data.success !== undefined) {
|
||||
return data
|
||||
}
|
||||
|
||||
// 兼容不同的响应格式
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
message: 'success'
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 处理HTTP错误
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
// 未授权,清除登录状态
|
||||
authStore.logout()
|
||||
window.location.href = '/login'
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: '登录已过期,请重新登录'
|
||||
})
|
||||
case 403:
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: '没有权限访问'
|
||||
})
|
||||
case 404:
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: '请求的资源不存在'
|
||||
})
|
||||
case 500:
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
})
|
||||
default:
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: data?.message || '请求失败'
|
||||
})
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 网络错误
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: '网络连接失败,请检查网络'
|
||||
})
|
||||
} else {
|
||||
// 其他错误
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
message: error.message || '未知错误'
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// API接口定义
|
||||
const api = {
|
||||
// 认证相关
|
||||
auth: {
|
||||
login: (credentials) => request.post('/auth/login', credentials),
|
||||
register: (userInfo) => request.post('/auth/register', userInfo),
|
||||
logout: () => request.post('/auth/logout'),
|
||||
getUserInfo: () => request.get('/auth/user'),
|
||||
refreshToken: () => request.post('/auth/refresh')
|
||||
},
|
||||
|
||||
// 游戏角色相关
|
||||
gameRoles: {
|
||||
getList: () => request.get('/gamerole_list'),
|
||||
add: (roleData) => request.post('/gameroles', roleData),
|
||||
update: (roleId, roleData) => request.put(`/gameroles/${roleId}`, roleData),
|
||||
delete: (roleId) => request.delete(`/gameroles/${roleId}`),
|
||||
getDetail: (roleId) => request.get(`/gameroles/${roleId}`)
|
||||
},
|
||||
|
||||
// 日常任务相关
|
||||
dailyTasks: {
|
||||
getList: (roleId) => request.get(`/daily-tasks?roleId=${roleId}`),
|
||||
getStatus: (roleId) => request.get(`/daily-tasks/status?roleId=${roleId}`),
|
||||
complete: (taskId, roleId) => request.post(`/daily-tasks/${taskId}/complete`, { roleId }),
|
||||
getHistory: (roleId, page = 1, limit = 20) => request.get(`/daily-tasks/history?roleId=${roleId}&page=${page}&limit=${limit}`)
|
||||
},
|
||||
|
||||
// 用户相关
|
||||
user: {
|
||||
getProfile: () => request.get('/user/profile'),
|
||||
updateProfile: (profileData) => request.put('/user/profile', profileData),
|
||||
changePassword: (passwordData) => request.put('/user/password', passwordData),
|
||||
getStats: () => request.get('/user/stats')
|
||||
}
|
||||
}
|
||||
|
||||
export default api
|
||||
314
src/assets/styles/global.scss
Normal file
314
src/assets/styles/global.scss
Normal file
@@ -0,0 +1,314 @@
|
||||
// 全局样式重置
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: var(--font-size-md);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// 链接样式
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮重置
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
// 输入框重置
|
||||
input, textarea, select {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// 列表重置
|
||||
ul, ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
// 图片
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// 工具类
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// 间距工具类
|
||||
.m-0 { margin: 0; }
|
||||
.m-1 { margin: var(--spacing-xs); }
|
||||
.m-2 { margin: var(--spacing-sm); }
|
||||
.m-3 { margin: var(--spacing-md); }
|
||||
.m-4 { margin: var(--spacing-lg); }
|
||||
.m-5 { margin: var(--spacing-xl); }
|
||||
|
||||
.mt-0 { margin-top: 0; }
|
||||
.mt-1 { margin-top: var(--spacing-xs); }
|
||||
.mt-2 { margin-top: var(--spacing-sm); }
|
||||
.mt-3 { margin-top: var(--spacing-md); }
|
||||
.mt-4 { margin-top: var(--spacing-lg); }
|
||||
.mt-5 { margin-top: var(--spacing-xl); }
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-1 { margin-bottom: var(--spacing-xs); }
|
||||
.mb-2 { margin-bottom: var(--spacing-sm); }
|
||||
.mb-3 { margin-bottom: var(--spacing-md); }
|
||||
.mb-4 { margin-bottom: var(--spacing-lg); }
|
||||
.mb-5 { margin-bottom: var(--spacing-xl); }
|
||||
|
||||
.ml-0 { margin-left: 0; }
|
||||
.ml-1 { margin-left: var(--spacing-xs); }
|
||||
.ml-2 { margin-left: var(--spacing-sm); }
|
||||
.ml-3 { margin-left: var(--spacing-md); }
|
||||
.ml-4 { margin-left: var(--spacing-lg); }
|
||||
.ml-5 { margin-left: var(--spacing-xl); }
|
||||
|
||||
.mr-0 { margin-right: 0; }
|
||||
.mr-1 { margin-right: var(--spacing-xs); }
|
||||
.mr-2 { margin-right: var(--spacing-sm); }
|
||||
.mr-3 { margin-right: var(--spacing-md); }
|
||||
.mr-4 { margin-right: var(--spacing-lg); }
|
||||
.mr-5 { margin-right: var(--spacing-xl); }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-1 { padding: var(--spacing-xs); }
|
||||
.p-2 { padding: var(--spacing-sm); }
|
||||
.p-3 { padding: var(--spacing-md); }
|
||||
.p-4 { padding: var(--spacing-lg); }
|
||||
.p-5 { padding: var(--spacing-xl); }
|
||||
|
||||
.pt-0 { padding-top: 0; }
|
||||
.pt-1 { padding-top: var(--spacing-xs); }
|
||||
.pt-2 { padding-top: var(--spacing-sm); }
|
||||
.pt-3 { padding-top: var(--spacing-md); }
|
||||
.pt-4 { padding-top: var(--spacing-lg); }
|
||||
.pt-5 { padding-top: var(--spacing-xl); }
|
||||
|
||||
.pb-0 { padding-bottom: 0; }
|
||||
.pb-1 { padding-bottom: var(--spacing-xs); }
|
||||
.pb-2 { padding-bottom: var(--spacing-sm); }
|
||||
.pb-3 { padding-bottom: var(--spacing-md); }
|
||||
.pb-4 { padding-bottom: var(--spacing-lg); }
|
||||
.pb-5 { padding-bottom: var(--spacing-xl); }
|
||||
|
||||
.pl-0 { padding-left: 0; }
|
||||
.pl-1 { padding-left: var(--spacing-xs); }
|
||||
.pl-2 { padding-left: var(--spacing-sm); }
|
||||
.pl-3 { padding-left: var(--spacing-md); }
|
||||
.pl-4 { padding-left: var(--spacing-lg); }
|
||||
.pl-5 { padding-left: var(--spacing-xl); }
|
||||
|
||||
.pr-0 { padding-right: 0; }
|
||||
.pr-1 { padding-right: var(--spacing-xs); }
|
||||
.pr-2 { padding-right: var(--spacing-sm); }
|
||||
.pr-3 { padding-right: var(--spacing-md); }
|
||||
.pr-4 { padding-right: var(--spacing-lg); }
|
||||
.pr-5 { padding-right: var(--spacing-xl); }
|
||||
|
||||
// 文字大小
|
||||
.text-xs { font-size: var(--font-size-xs); }
|
||||
.text-sm { font-size: var(--font-size-sm); }
|
||||
.text-md { font-size: var(--font-size-md); }
|
||||
.text-lg { font-size: var(--font-size-lg); }
|
||||
.text-xl { font-size: var(--font-size-xl); }
|
||||
.text-2xl { font-size: var(--font-size-2xl); }
|
||||
.text-3xl { font-size: var(--font-size-3xl); }
|
||||
|
||||
// 文字颜色
|
||||
.text-primary { color: var(--text-primary); }
|
||||
.text-secondary { color: var(--text-secondary); }
|
||||
.text-tertiary { color: var(--text-tertiary); }
|
||||
.text-success { color: var(--success-color); }
|
||||
.text-warning { color: var(--warning-color); }
|
||||
.text-error { color: var(--error-color); }
|
||||
.text-info { color: var(--info-color); }
|
||||
|
||||
// 字重
|
||||
.font-light { font-weight: var(--font-weight-light); }
|
||||
.font-normal { font-weight: var(--font-weight-normal); }
|
||||
.font-medium { font-weight: var(--font-weight-medium); }
|
||||
.font-semibold { font-weight: var(--font-weight-semibold); }
|
||||
.font-bold { font-weight: var(--font-weight-bold); }
|
||||
|
||||
// 圆角
|
||||
.rounded-sm { border-radius: var(--border-radius-small); }
|
||||
.rounded { border-radius: var(--border-radius-medium); }
|
||||
.rounded-lg { border-radius: var(--border-radius-large); }
|
||||
.rounded-xl { border-radius: var(--border-radius-xl); }
|
||||
.rounded-full { border-radius: 50%; }
|
||||
|
||||
// 阴影
|
||||
.shadow-sm { box-shadow: var(--shadow-light); }
|
||||
.shadow { box-shadow: var(--shadow-medium); }
|
||||
.shadow-lg { box-shadow: var(--shadow-heavy); }
|
||||
|
||||
// 动画
|
||||
.transition {
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.transition-fast {
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.transition-slow {
|
||||
transition: all var(--transition-slow);
|
||||
}
|
||||
|
||||
// 布局
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.container-sm {
|
||||
max-width: 768px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.container-lg {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
// 玻璃效果
|
||||
.glass {
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
// 悬停效果
|
||||
.hover-scale {
|
||||
transition: transform var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式
|
||||
@media (max-width: 768px) {
|
||||
.container,
|
||||
.container-sm,
|
||||
.container-lg {
|
||||
padding: 0 var(--spacing-sm);
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
}
|
||||
102
src/assets/styles/variables.scss
Normal file
102
src/assets/styles/variables.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
// 颜色变量
|
||||
:root {
|
||||
// 主题色
|
||||
--primary-color: #667eea;
|
||||
--primary-color-hover: #5a67d8;
|
||||
--primary-color-light: #e6f7ff;
|
||||
|
||||
// 辅助色
|
||||
--secondary-color: #764ba2;
|
||||
--success-color: #18a058;
|
||||
--warning-color: #f5a623;
|
||||
--error-color: #d03050;
|
||||
--info-color: #2080f0;
|
||||
|
||||
// 中性色
|
||||
--text-primary: #333333;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #999999;
|
||||
--text-disabled: #cccccc;
|
||||
|
||||
// 背景色
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f5f7fa;
|
||||
--bg-tertiary: #f0f2f5;
|
||||
--bg-overlay: rgba(0, 0, 0, 0.5);
|
||||
|
||||
// 边框色
|
||||
--border-light: #e5e7eb;
|
||||
--border-medium: #d1d5db;
|
||||
--border-dark: #9ca3af;
|
||||
|
||||
// 阴影
|
||||
--shadow-light: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-heavy: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
|
||||
// 圆角
|
||||
--border-radius-small: 4px;
|
||||
--border-radius-medium: 8px;
|
||||
--border-radius-large: 12px;
|
||||
--border-radius-xl: 16px;
|
||||
|
||||
// 间距
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
--spacing-2xl: 48px;
|
||||
|
||||
// 字体
|
||||
--font-size-xs: 12px;
|
||||
--font-size-sm: 14px;
|
||||
--font-size-md: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-size-xl: 20px;
|
||||
--font-size-2xl: 24px;
|
||||
--font-size-3xl: 32px;
|
||||
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
// 行高
|
||||
--line-height-tight: 1.2;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.75;
|
||||
|
||||
// 动画
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-normal: 0.3s ease;
|
||||
--transition-slow: 0.5s ease;
|
||||
|
||||
// Z-index
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1020;
|
||||
--z-fixed: 1030;
|
||||
--z-modal-backdrop: 1040;
|
||||
--z-modal: 1050;
|
||||
--z-popover: 1060;
|
||||
--z-tooltip: 1070;
|
||||
--z-toast: 1080;
|
||||
}
|
||||
|
||||
// 暗色主题
|
||||
[data-theme="dark"] {
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-tertiary: #9ca3af;
|
||||
--text-disabled: #6b7280;
|
||||
|
||||
--bg-primary: #1f2937;
|
||||
--bg-secondary: #374151;
|
||||
--bg-tertiary: #4b5563;
|
||||
--bg-overlay: rgba(0, 0, 0, 0.7);
|
||||
|
||||
--border-light: #4b5563;
|
||||
--border-medium: #6b7280;
|
||||
--border-dark: #9ca3af;
|
||||
}
|
||||
582
src/components/DailyTaskCard.vue
Normal file
582
src/components/DailyTaskCard.vue
Normal file
@@ -0,0 +1,582 @@
|
||||
<template>
|
||||
<div
|
||||
class="daily-task-card"
|
||||
:class="{ completed: task.completed }"
|
||||
>
|
||||
<!-- 卡片头部 -->
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<img
|
||||
:src="task.icon || '/icons/ta.png'"
|
||||
:alt="task.title"
|
||||
class="task-icon"
|
||||
>
|
||||
<div class="title-container">
|
||||
<h3 class="title">
|
||||
{{ task.title }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="task.subtitle"
|
||||
class="subtitle"
|
||||
>
|
||||
{{ task.subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="{ completed: task.completed }"
|
||||
@click="toggleStatus"
|
||||
>
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="{ completed: task.completed }"
|
||||
/>
|
||||
<span>{{ task.completed ? '已完成' : '待完成' }}</span>
|
||||
</div>
|
||||
|
||||
<n-button
|
||||
text
|
||||
class="settings-button"
|
||||
@click="showSettings = true"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon class="settings-icon">
|
||||
<Settings />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度信息 -->
|
||||
<div
|
||||
v-if="task.progress"
|
||||
class="progress-container"
|
||||
>
|
||||
<div class="info-container">
|
||||
<div class="info-item">
|
||||
<span class="info-label">当前进度</span>
|
||||
<span class="info-value">{{ task.progress.current }}/{{ task.progress.total }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.reward"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">奖励</span>
|
||||
<span class="info-value">{{ task.reward }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.nextReset"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">重置时间</span>
|
||||
<span class="info-value">{{ formatResetTime(task.nextReset) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div
|
||||
v-if="!task.completed"
|
||||
class="actions-container"
|
||||
>
|
||||
<n-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="isExecuting"
|
||||
:disabled="!task.canExecute"
|
||||
class="complete-button"
|
||||
@click="executeTask"
|
||||
>
|
||||
{{ getButtonText() }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 设置模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showSettings"
|
||||
preset="card"
|
||||
title="任务设置"
|
||||
style="width: 480px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon class="modal-icon">
|
||||
<Settings />
|
||||
</n-icon>
|
||||
<span>{{ task.title }} - 设置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="settings-content">
|
||||
<div class="settings-grid">
|
||||
<div class="setting-item">
|
||||
<n-checkbox
|
||||
v-model:checked="taskSettings.autoExecute"
|
||||
@update:checked="updateSetting('autoExecute', $event)"
|
||||
>
|
||||
自动执行
|
||||
</n-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">执行延迟 (秒)</label>
|
||||
<n-input-number
|
||||
v-model:value="taskSettings.delay"
|
||||
:min="0"
|
||||
:max="300"
|
||||
@update:value="updateSetting('delay', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<n-checkbox
|
||||
v-model:checked="taskSettings.notification"
|
||||
@update:checked="updateSetting('notification', $event)"
|
||||
>
|
||||
完成通知
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务详情 -->
|
||||
<div
|
||||
v-if="task.details"
|
||||
class="task-details"
|
||||
>
|
||||
<h4>任务详情</h4>
|
||||
<div class="task-list">
|
||||
<div
|
||||
v-for="detail in task.details"
|
||||
:key="detail.id"
|
||||
class="task-item"
|
||||
>
|
||||
<div class="task-item-left">
|
||||
<n-icon
|
||||
class="task-status-icon"
|
||||
:class="{ completed: detail.completed }"
|
||||
>
|
||||
<CheckCircle v-if="detail.completed" />
|
||||
<Clock v-else />
|
||||
</n-icon>
|
||||
<span class="task-name">{{ detail.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 执行日志 -->
|
||||
<div
|
||||
v-if="task.logs && task.logs.length"
|
||||
class="execution-log"
|
||||
>
|
||||
<h4>执行日志</h4>
|
||||
<div class="log-container">
|
||||
<div
|
||||
v-for="log in task.logs.slice(-5)"
|
||||
:key="log.id"
|
||||
class="log-item"
|
||||
>
|
||||
<span class="log-time">{{ formatLogTime(log.timestamp) }}</span>
|
||||
<span
|
||||
class="log-message"
|
||||
:class="{
|
||||
error: log.type === 'error',
|
||||
success: log.type === 'success'
|
||||
}"
|
||||
>
|
||||
{{ log.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { Settings, Checkmark as CheckCircle, Time as Clock } from '@vicons/ionicons5'
|
||||
|
||||
const props = defineProps({
|
||||
task: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:task', 'execute', 'toggle-status'])
|
||||
|
||||
const message = useMessage()
|
||||
const showSettings = ref(false)
|
||||
const isExecuting = ref(false)
|
||||
|
||||
// 任务设置
|
||||
const taskSettings = ref({
|
||||
autoExecute: props.task.settings?.autoExecute || false,
|
||||
delay: props.task.settings?.delay || 0,
|
||||
notification: props.task.settings?.notification || true
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const getButtonText = () => {
|
||||
if (isExecuting.value) return '执行中...'
|
||||
if (!props.task.canExecute) return '不可执行'
|
||||
return '立即执行'
|
||||
}
|
||||
|
||||
// 方法
|
||||
const toggleStatus = () => {
|
||||
emit('toggle-status', props.task.id)
|
||||
}
|
||||
|
||||
const executeTask = async () => {
|
||||
if (isExecuting.value || !props.task.canExecute) return
|
||||
|
||||
try {
|
||||
isExecuting.value = true
|
||||
await emit('execute', props.task.id)
|
||||
|
||||
if (taskSettings.value.notification) {
|
||||
message.success(`任务 "${props.task.title}" 执行成功`)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(`任务执行失败: ${error.message}`)
|
||||
} finally {
|
||||
isExecuting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateSetting = (key, value) => {
|
||||
taskSettings.value[key] = value
|
||||
|
||||
// 发出设置更新事件
|
||||
emit('update:task', {
|
||||
...props.task,
|
||||
settings: { ...taskSettings.value }
|
||||
})
|
||||
}
|
||||
|
||||
const formatResetTime = (timestamp) => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const formatLogTime = (timestamp) => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 监听任务设置变化
|
||||
watch(() => props.task.settings, (newSettings) => {
|
||||
if (newSettings) {
|
||||
taskSettings.value = { ...taskSettings.value, ...newSettings }
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.daily-task-card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--border-radius-large);
|
||||
box-shadow: var(--shadow-light);
|
||||
min-height: 184px;
|
||||
padding: var(--spacing-lg);
|
||||
position: relative;
|
||||
transition: all var(--transition-normal);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
border-left-color: var(--success-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: 9999px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background-color: rgba(24, 160, 88, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--text-secondary);
|
||||
border-radius: 50%;
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
&.completed {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
padding: var(--spacing-xs);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
color: var(--text-secondary);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.complete-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
&.n-button--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// 模态框样式
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-details,
|
||||
.execution-log {
|
||||
margin-top: var(--spacing-xl);
|
||||
|
||||
h4 {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.task-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-status-icon {
|
||||
color: var(--text-tertiary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&.completed {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.task-name {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
font-size: var(--font-size-sm);
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 640px) {
|
||||
.daily-task-card {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.info-container {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
751
src/components/DailyTaskStatus.vue
Normal file
751
src/components/DailyTaskStatus.vue
Normal file
@@ -0,0 +1,751 @@
|
||||
<template>
|
||||
<div class="daily-task-container">
|
||||
<div class="task-header">
|
||||
<div class="header-left">
|
||||
<img src="/icons/174023274867420.png" alt="每日任务" class="task-icon">
|
||||
<div class="title-container">
|
||||
<h3>每日任务</h3>
|
||||
<p>当前进度</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="{ completed: isAllCompleted }"
|
||||
@click="showTaskDetails = true"
|
||||
>
|
||||
<div class="status-dot" :class="{ completed: isAllCompleted }"></div>
|
||||
<span>任务详情</span>
|
||||
</div>
|
||||
|
||||
<button class="settings-button" @click="showSettings = true">
|
||||
<n-icon><Settings /></n-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-container">
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="progressPercentage"
|
||||
:height="8"
|
||||
:border-radius="4"
|
||||
:color="progressColor"
|
||||
rail-color="#f3f4f6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="info-container">
|
||||
右上角小齿轮有惊喜
|
||||
</div>
|
||||
|
||||
<!-- 一键执行按钮 -->
|
||||
<button
|
||||
class="execute-button"
|
||||
:disabled="isExecuting"
|
||||
@click="executeAllTasks"
|
||||
>
|
||||
<span v-if="isExecuting" class="loading-text">
|
||||
<svg class="loading-icon" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 22c5.421 0 10-4.579 10-10h-2c0 4.337-3.663 8-8 8s-8-3.663-8-8c0-4.336 3.663-8 8-8V2C6.579 2 2 6.58 2 12c0 5.421 4.579 10 10 10z"/>
|
||||
</svg>
|
||||
执行中...
|
||||
</span>
|
||||
<span v-else>一键补差</span>
|
||||
</button>
|
||||
|
||||
<!-- 任务设置模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showSettings"
|
||||
preset="card"
|
||||
title="任务设置"
|
||||
style="width: 400px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon><Settings /></n-icon>
|
||||
<span>任务设置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="settings-content">
|
||||
<div class="settings-grid">
|
||||
<!-- 竞技场设置 -->
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">竞技场阵容</label>
|
||||
<n-select
|
||||
v-model:value="taskSettings.arenaFormation"
|
||||
:options="formationOptions"
|
||||
size="small"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- BOSS设置 -->
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">BOSS阵容</label>
|
||||
<n-select
|
||||
v-model:value="taskSettings.bossFormation"
|
||||
:options="formationOptions"
|
||||
size="small"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- BOSS次数 -->
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">BOSS次数</label>
|
||||
<n-select
|
||||
v-model:value="taskSettings.bossTimes"
|
||||
:options="bossTimesOptions"
|
||||
size="small"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 功能开关 -->
|
||||
<div class="setting-switches">
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">领罐子</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.claimBottle"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">领挂机</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.claimHangUp"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">竞技场</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.arenaEnable"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">开宝箱</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.openBox"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">领取邮件奖励</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.claimEmail"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">付费招募</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.payRecruit"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<!-- 任务详情模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showTaskDetails"
|
||||
preset="card"
|
||||
title="每日任务详情"
|
||||
style="width: 400px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon><Calendar /></n-icon>
|
||||
<span>每日任务详情</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="task-list">
|
||||
<div
|
||||
v-for="task in dailyTasks"
|
||||
:key="task.id"
|
||||
class="task-item"
|
||||
>
|
||||
<div class="task-item-left">
|
||||
<n-icon
|
||||
class="task-status-icon"
|
||||
:class="{ completed: task.completed }"
|
||||
>
|
||||
<CheckmarkCircle v-if="task.completed" />
|
||||
<EllipseOutline v-else />
|
||||
</n-icon>
|
||||
<span class="task-name">{{ task.name }}</span>
|
||||
</div>
|
||||
<n-tag
|
||||
:type="task.completed ? 'success' : 'default'"
|
||||
size="small"
|
||||
>
|
||||
{{ task.completed ? '已完成' : '未完成' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<!-- 执行日志模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showLog"
|
||||
preset="card"
|
||||
title="任务执行日志"
|
||||
style="width: 500px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon><DocumentText /></n-icon>
|
||||
<span>任务执行日志</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="log-container" ref="logContainer">
|
||||
<div
|
||||
v-for="log in executionLogs"
|
||||
:key="log.id"
|
||||
class="log-item"
|
||||
>
|
||||
<span class="log-time">{{ formatTime(log.time) }}</span>
|
||||
<span
|
||||
class="log-message"
|
||||
:class="{
|
||||
error: log.type === 'error',
|
||||
success: log.type === 'success'
|
||||
}"
|
||||
>
|
||||
{{ log.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import {
|
||||
Settings,
|
||||
Calendar,
|
||||
CheckmarkCircle,
|
||||
EllipseOutline,
|
||||
DocumentText
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const showSettings = ref(false)
|
||||
const showTaskDetails = ref(false)
|
||||
const showLog = ref(false)
|
||||
const isExecuting = ref(false)
|
||||
const logContainer = ref(null)
|
||||
const executionLogs = ref([])
|
||||
|
||||
// 任务设置
|
||||
const taskSettings = ref({
|
||||
arenaFormation: 1,
|
||||
bossFormation: 1,
|
||||
bossTimes: 4,
|
||||
claimBottle: true,
|
||||
payRecruit: true,
|
||||
openBox: true,
|
||||
arenaEnable: true,
|
||||
claimHangUp: true,
|
||||
claimEmail: true
|
||||
})
|
||||
|
||||
// 每日任务列表
|
||||
const dailyTasks = ref([
|
||||
{ id: 1, name: '登录一次游戏', completed: false, loading: false },
|
||||
{ id: 2, name: '分享一次游戏', completed: false, loading: false },
|
||||
{ id: 3, name: '赠送好友3次金币', completed: false, loading: false },
|
||||
{ id: 4, name: '进行2次招募', completed: false, loading: false },
|
||||
{ id: 5, name: '领取5次挂机奖励', completed: false, loading: false },
|
||||
{ id: 6, name: '进行3次点金', completed: false, loading: false },
|
||||
{ id: 7, name: '开启3次宝箱', completed: false, loading: false },
|
||||
{ id: 12, name: '黑市购买1次物品(请设置采购清单)', completed: false, loading: false },
|
||||
{ id: 13, name: '进行1场竞技场战斗', completed: false, loading: false },
|
||||
{ id: 14, name: '收获1个任意盐罐', completed: false, loading: false }
|
||||
])
|
||||
|
||||
// 选项配置
|
||||
const formationOptions = [
|
||||
{ label: '阵容1', value: 1 },
|
||||
{ label: '阵容2', value: 2 },
|
||||
{ label: '阵容3', value: 3 },
|
||||
{ label: '阵容4', value: 4 }
|
||||
]
|
||||
|
||||
const bossTimesOptions = [
|
||||
{ label: '0次', value: 0 },
|
||||
{ label: '1次', value: 1 },
|
||||
{ label: '2次', value: 2 },
|
||||
{ label: '3次', value: 3 },
|
||||
{ label: '4次', value: 4 }
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const roleInfo = computed(() => {
|
||||
return tokenStore.gameData?.roleInfo
|
||||
})
|
||||
|
||||
const dailyTaskData = computed(() => {
|
||||
return roleInfo.value?.role?.dailyTask
|
||||
})
|
||||
|
||||
const progressPercentage = computed(() => {
|
||||
const current = dailyTaskData.value?.dailyPoint || 0
|
||||
return current > 100 ? 100 : current
|
||||
})
|
||||
|
||||
const isAllCompleted = computed(() => {
|
||||
return progressPercentage.value === 100
|
||||
})
|
||||
|
||||
const progressColor = computed(() => {
|
||||
return progressPercentage.value === 100 ? '#10b981' : '#3b82f6'
|
||||
})
|
||||
|
||||
// 更新任务完成状态
|
||||
const updateTaskStatus = () => {
|
||||
if (!dailyTaskData.value?.complete) return
|
||||
|
||||
const completed = dailyTaskData.value.complete
|
||||
dailyTasks.value.forEach(task => {
|
||||
const taskStatus = completed[task.id.toString()]
|
||||
task.completed = taskStatus === -1 // -1 表示已完成
|
||||
})
|
||||
}
|
||||
|
||||
// 添加日志
|
||||
const addLog = (message, type = 'info') => {
|
||||
const log = {
|
||||
id: Date.now() + Math.random(),
|
||||
time: new Date().toLocaleTimeString(),
|
||||
message,
|
||||
type
|
||||
}
|
||||
executionLogs.value.push(log)
|
||||
|
||||
// 自动滚动到底部
|
||||
nextTick(() => {
|
||||
if (logContainer.value) {
|
||||
logContainer.value.scrollTop = logContainer.value.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const saveSettings = () => {
|
||||
// 这里可以保存到 localStorage 或发送到服务器
|
||||
localStorage.setItem('taskSettings', JSON.stringify(taskSettings.value))
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeString) => {
|
||||
return timeString
|
||||
}
|
||||
|
||||
// 执行所有任务
|
||||
const executeAllTasks = async () => {
|
||||
if (!tokenStore.selectedToken || isExecuting.value) return
|
||||
|
||||
isExecuting.value = true
|
||||
showLog.value = true
|
||||
executionLogs.value = []
|
||||
|
||||
addLog('开始执行任务...')
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 获取角色信息
|
||||
addLog('获取角色信息...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'role_getroleinfo')
|
||||
addLog('获取角色信息成功', 'success')
|
||||
|
||||
// 执行各种任务
|
||||
if (taskSettings.value.claimHangUp) {
|
||||
addLog('领取挂机奖励...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'system_claimhangupreward')
|
||||
}
|
||||
|
||||
if (taskSettings.value.claimBottle) {
|
||||
addLog('领取盐罐奖励...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'bottlehelper_claim')
|
||||
}
|
||||
|
||||
if (taskSettings.value.payRecruit) {
|
||||
addLog('进行招募...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'hero_recruit', {
|
||||
byClub: false,
|
||||
recruitNumber: 1,
|
||||
recruitType: 3
|
||||
})
|
||||
}
|
||||
|
||||
if (taskSettings.value.openBox) {
|
||||
addLog('开启宝箱...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'item_openbox', {
|
||||
itemId: 2001,
|
||||
number: 3
|
||||
})
|
||||
}
|
||||
|
||||
if (taskSettings.value.arenaEnable) {
|
||||
addLog('进行竞技场战斗...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'fight_startareaarena', {
|
||||
targetId: 530479307
|
||||
})
|
||||
}
|
||||
|
||||
if (taskSettings.value.claimEmail) {
|
||||
addLog('领取邮件奖励...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'mail_claimallattachment', {
|
||||
category: 0
|
||||
})
|
||||
}
|
||||
|
||||
// 最后再次获取角色信息更新状态
|
||||
addLog('更新角色信息...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'role_getroleinfo')
|
||||
|
||||
addLog('任务执行完成!', 'success')
|
||||
message.success('任务处理完成')
|
||||
|
||||
} catch (error) {
|
||||
addLog(`任务执行失败: ${error.message}`, 'error')
|
||||
message.error('任务执行失败')
|
||||
} finally {
|
||||
isExecuting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听角色信息变化
|
||||
watch(dailyTaskData, () => {
|
||||
updateTaskStatus()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 初始化设置
|
||||
const initSettings = () => {
|
||||
const saved = localStorage.getItem('taskSettings')
|
||||
if (saved) {
|
||||
try {
|
||||
taskSettings.value = { ...taskSettings.value, ...JSON.parse(saved) }
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initSettings()
|
||||
updateTaskStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.daily-task-container {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-full);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
padding: var(--spacing-xs);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-medium);
|
||||
transition: background var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-container {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.execute-button {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-medium);
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 模态框样式
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.setting-switches {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.switch-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.task-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-status-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
&.completed {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) 0;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--text-tertiary);
|
||||
min-width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.task-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
673
src/components/GameStatus.vue
Normal file
673
src/components/GameStatus.vue
Normal file
@@ -0,0 +1,673 @@
|
||||
<template>
|
||||
<div class="game-status-container">
|
||||
<!-- 队伍状态 -->
|
||||
<TeamStatus />
|
||||
|
||||
<!-- 每日任务状态 -->
|
||||
<DailyTaskStatus />
|
||||
|
||||
<!-- 咸将塔状态 -->
|
||||
<TowerStatus />
|
||||
|
||||
<!-- 其他游戏状态卡片 -->
|
||||
<!-- 盐罐机器人状态 -->
|
||||
<div class="status-card bottle-helper">
|
||||
<div class="card-header">
|
||||
<img src="/icons/173746572831736.png" alt="盐罐图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>盐罐机器人</h3>
|
||||
<p>剩余时间</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: bottleHelper.isRunning }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ bottleHelper.isRunning ? '运行中' : '已停止' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="time-display">
|
||||
{{ formatTime(bottleHelper.remainingTime) }}
|
||||
</div>
|
||||
<button
|
||||
class="action-button"
|
||||
:class="{ active: bottleHelper.isRunning }"
|
||||
@click="handleBottleHelper"
|
||||
>
|
||||
{{ bottleHelper.isRunning ? '重启服务' : '启动服务' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 挂机状态 -->
|
||||
<div class="status-card hang-up">
|
||||
<div class="card-header">
|
||||
<img src="/icons/174061875626614.png" alt="挂机图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>挂机时间</h3>
|
||||
<p>已挂机:{{ formatTime(hangUp.elapsedTime) }}</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: hangUp.isActive }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ hangUp.isActive ? '挂机中' : '已完成' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="time-display">
|
||||
{{ formatTime(hangUp.remainingTime) }}
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button class="action-button secondary" @click="extendHangUp">
|
||||
加钟
|
||||
</button>
|
||||
<button class="action-button primary" @click="claimHangUpReward">
|
||||
领取奖励
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 俱乐部排位 -->
|
||||
<div class="status-card legion-match">
|
||||
<div class="card-header">
|
||||
<img src="/icons/1733492491706152.png" alt="俱乐部图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>俱乐部排位</h3>
|
||||
<p>赛事状态</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: legionMatch.isRegistered }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ legionMatch.isRegistered ? '已报名' : '未报名' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="description">
|
||||
每逢周三周四周五有比赛<br>
|
||||
立即报名参与精彩对决!
|
||||
</p>
|
||||
<button
|
||||
class="action-button"
|
||||
:disabled="legionMatch.isRegistered"
|
||||
@click="registerLegionMatch"
|
||||
>
|
||||
{{ legionMatch.isRegistered ? '已报名' : '立即报名' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 俱乐部签到 -->
|
||||
<div class="status-card legion-signin">
|
||||
<div class="card-header">
|
||||
<img src="/icons/1733492491706148.png" alt="签到图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>俱乐部签到</h3>
|
||||
<p>每日签到状态</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: legionSignin.isSignedIn }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ legionSignin.isSignedIn ? '已签到' : '待签到' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="club-name" v-if="legionSignin.clubName">
|
||||
当前俱乐部<br>
|
||||
<strong>{{ legionSignin.clubName }}</strong>
|
||||
</p>
|
||||
<p class="description" v-else>
|
||||
尚未加入任何俱乐部
|
||||
</p>
|
||||
<button
|
||||
class="action-button"
|
||||
:disabled="legionSignin.isSignedIn"
|
||||
@click="signInLegion"
|
||||
>
|
||||
{{ legionSignin.isSignedIn ? '已签到' : '立即签到' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 咸鱼大冲关 -->
|
||||
<div class="status-card study">
|
||||
<div class="card-header">
|
||||
<img src="/icons/1736425783912140.png" alt="学习图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>咸鱼大冲关</h3>
|
||||
<p>每日知识挑战</p>
|
||||
</div>
|
||||
<div class="status-badge weekly">
|
||||
<div class="status-dot"></div>
|
||||
<span>每周任务</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="description">
|
||||
没有什么可以阻挡我求知的欲望!
|
||||
</p>
|
||||
<button
|
||||
class="action-button"
|
||||
:disabled="study.isAnswering"
|
||||
@click="startStudy"
|
||||
>
|
||||
<span v-if="study.isAnswering" class="loading-text">
|
||||
<svg class="loading-icon" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 22c5.421 0 10-4.579 10-10h-2c0 4.337-3.663 8-8 8s-8-3.663-8-8c0-4.336 3.663-8 8-8V2C6.579 2 2 6.58 2 12c0 5.421 4.579 10 10 10z"/>
|
||||
</svg>
|
||||
答题中...
|
||||
</span>
|
||||
<span v-else>一键答题</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import TeamStatus from './TeamStatus.vue'
|
||||
import DailyTaskStatus from './DailyTaskStatus.vue'
|
||||
import TowerStatus from './TowerStatus.vue'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const bottleHelper = ref({
|
||||
isRunning: false,
|
||||
remainingTime: 0,
|
||||
stopTime: 0
|
||||
})
|
||||
|
||||
const hangUp = ref({
|
||||
isActive: false,
|
||||
remainingTime: 0,
|
||||
elapsedTime: 0,
|
||||
lastTime: 0,
|
||||
hangUpTime: 0
|
||||
})
|
||||
|
||||
const legionMatch = ref({
|
||||
isRegistered: false
|
||||
})
|
||||
|
||||
const legionSignin = ref({
|
||||
isSignedIn: false,
|
||||
clubName: ''
|
||||
})
|
||||
|
||||
const study = ref({
|
||||
isAnswering: false
|
||||
})
|
||||
|
||||
|
||||
// 计算属性
|
||||
const roleInfo = computed(() => {
|
||||
return tokenStore.gameData?.roleInfo || null
|
||||
})
|
||||
|
||||
// 格式化时间 - 确保显示到秒
|
||||
const formatTime = (seconds) => {
|
||||
// 确保传入值为数字,并向下取整到秒
|
||||
const totalSeconds = Math.floor(Number(seconds) || 0)
|
||||
|
||||
if (totalSeconds <= 0) return '00:00:00'
|
||||
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const secs = totalSeconds % 60
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
const updateGameStatus = () => {
|
||||
if (!roleInfo.value) return
|
||||
|
||||
const role = roleInfo.value.role
|
||||
|
||||
// 更新盐罐机器人状态
|
||||
if (role.bottleHelpers) {
|
||||
const now = Date.now() / 1000
|
||||
bottleHelper.value.stopTime = role.bottleHelpers.helperStopTime
|
||||
bottleHelper.value.isRunning = role.bottleHelpers.helperStopTime > now
|
||||
// 确保剩余时间为整数秒
|
||||
bottleHelper.value.remainingTime = Math.max(0, Math.floor(role.bottleHelpers.helperStopTime - now))
|
||||
console.log('🤖 盐罐机器人状态更新:', {
|
||||
stopTime: role.bottleHelpers.helperStopTime,
|
||||
now: now,
|
||||
remainingTime: bottleHelper.value.remainingTime,
|
||||
isRunning: bottleHelper.value.isRunning
|
||||
})
|
||||
}
|
||||
|
||||
// 更新挂机状态
|
||||
if (role.hangUp) {
|
||||
const now = Date.now() / 1000
|
||||
hangUp.value.lastTime = role.hangUp.lastTime
|
||||
hangUp.value.hangUpTime = role.hangUp.hangUpTime
|
||||
|
||||
const elapsed = now - hangUp.value.lastTime
|
||||
if (elapsed <= hangUp.value.hangUpTime) {
|
||||
// 确保剩余时间为整数秒
|
||||
hangUp.value.remainingTime = Math.floor(hangUp.value.hangUpTime - elapsed)
|
||||
hangUp.value.isActive = true
|
||||
} else {
|
||||
hangUp.value.remainingTime = 0
|
||||
hangUp.value.isActive = false
|
||||
}
|
||||
// 确保已挂机时间为整数秒
|
||||
hangUp.value.elapsedTime = Math.floor(hangUp.value.hangUpTime - hangUp.value.remainingTime)
|
||||
|
||||
console.log('⏰ 挂机状态更新:', {
|
||||
lastTime: hangUp.value.lastTime,
|
||||
hangUpTime: hangUp.value.hangUpTime,
|
||||
elapsed: elapsed,
|
||||
remainingTime: hangUp.value.remainingTime,
|
||||
elapsedTime: hangUp.value.elapsedTime,
|
||||
isActive: hangUp.value.isActive
|
||||
})
|
||||
}
|
||||
|
||||
// 更新俱乐部排位状态
|
||||
if (role.statistics) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const todayTimestamp = today.getTime() / 1000
|
||||
|
||||
legionMatch.value.isRegistered =
|
||||
Number(role.statistics['last:legion:match:sign:up:time']) > todayTimestamp
|
||||
}
|
||||
|
||||
// 更新俱乐部签到状态
|
||||
if (role.statisticsTime) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const todayTimestamp = today.getTime() / 1000
|
||||
|
||||
legionSignin.value.isSignedIn =
|
||||
role.statisticsTime['legion:sign:in'] > todayTimestamp
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 定时器更新
|
||||
let timer = null
|
||||
const startTimer = () => {
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
// 更新盐罐机器人剩余时间
|
||||
if (bottleHelper.value.isRunning && bottleHelper.value.remainingTime > 0) {
|
||||
bottleHelper.value.remainingTime = Math.max(0, bottleHelper.value.remainingTime - 1)
|
||||
if (bottleHelper.value.remainingTime <= 0) {
|
||||
bottleHelper.value.isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新挂机剩余时间
|
||||
if (hangUp.value.isActive && hangUp.value.remainingTime > 0) {
|
||||
hangUp.value.remainingTime = Math.max(0, hangUp.value.remainingTime - 1)
|
||||
hangUp.value.elapsedTime = hangUp.value.elapsedTime + 1
|
||||
if (hangUp.value.remainingTime <= 0) {
|
||||
hangUp.value.isActive = false
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 盐罐机器人操作
|
||||
const handleBottleHelper = () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 停止后重启
|
||||
tokenStore.sendMessage(tokenId, 'bottlehelper_stop')
|
||||
setTimeout(() => {
|
||||
tokenStore.sendMessage(tokenId, 'bottlehelper_start')
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
}, 500)
|
||||
|
||||
message.info(bottleHelper.value.isRunning ? '重启盐罐机器人' : '启动盐罐机器人')
|
||||
}
|
||||
|
||||
// 挂机操作
|
||||
const extendHangUp = () => {
|
||||
if (!tokenStore.selectedToken) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 发送4次分享回调请求来加钟
|
||||
for (let i = 0; i < 4; i++) {
|
||||
setTimeout(() => {
|
||||
tokenStore.sendMessage(tokenId, 'system_mysharecallback', {
|
||||
isSkipShareCard: true,
|
||||
type: 2
|
||||
})
|
||||
}, i * 200)
|
||||
}
|
||||
|
||||
message.info('正在加钟...')
|
||||
}
|
||||
|
||||
const claimHangUpReward = () => {
|
||||
if (!tokenStore.selectedToken) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 领取挂机奖励
|
||||
tokenStore.sendMessage(tokenId, 'system_mysharecallback')
|
||||
tokenStore.sendMessage(tokenId, 'system_claimhangupreward')
|
||||
tokenStore.sendMessage(tokenId, 'system_mysharecallback', {
|
||||
isSkipShareCard: true,
|
||||
type: 2
|
||||
})
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
|
||||
message.info('领取挂机奖励')
|
||||
}
|
||||
|
||||
// 俱乐部排位报名
|
||||
const registerLegionMatch = () => {
|
||||
if (!tokenStore.selectedToken || legionMatch.value.isRegistered) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'legionmatch_rolesignup')
|
||||
|
||||
message.info('报名俱乐部排位')
|
||||
}
|
||||
|
||||
// 俱乐部签到
|
||||
const signInLegion = () => {
|
||||
if (!tokenStore.selectedToken || legionSignin.value.isSignedIn) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'legion_signin')
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
|
||||
message.info('俱乐部签到')
|
||||
}
|
||||
|
||||
// 学习答题
|
||||
const startStudy = () => {
|
||||
if (!tokenStore.selectedToken || study.value.isAnswering) return
|
||||
|
||||
study.value.isAnswering = true
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'study_startgame')
|
||||
|
||||
setTimeout(() => {
|
||||
study.value.isAnswering = false
|
||||
}, 3000)
|
||||
|
||||
message.info('开始答题')
|
||||
}
|
||||
|
||||
|
||||
// 监听角色信息变化
|
||||
watch(roleInfo, (newValue) => {
|
||||
if (newValue) {
|
||||
updateGameStatus()
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
updateGameStatus()
|
||||
startTimer()
|
||||
|
||||
// 获取俱乐部信息
|
||||
if (tokenStore.selectedToken) {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'legion_getinfo')
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.game-status-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
min-height: 200px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-full);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: rgba(156, 163, 175, 0.1);
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.active {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.weekly {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
&.energy {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.energy-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
.time-display {
|
||||
font-size: 1.5rem; /* text-2xl */
|
||||
font-weight: 700; /* font-bold */
|
||||
color: #111827; /* text-gray-900 */
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', monospace;
|
||||
letter-spacing: 0.1em;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.club-name {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.tower-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
.label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.tower-level {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-medium);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--primary-color-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: var(--secondary-color);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--secondary-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--info-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--info-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.game-status-container {
|
||||
grid-template-columns: 1fr;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-card {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
319
src/components/MessageTester.vue
Normal file
319
src/components/MessageTester.vue
Normal file
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div class="message-tester">
|
||||
<n-card
|
||||
title="消息加解密测试"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<!-- 选择Token -->
|
||||
<div>
|
||||
<n-select
|
||||
v-model:value="selectedTokenId"
|
||||
:options="tokenOptions"
|
||||
placeholder="选择要测试的Token"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket连接状态 -->
|
||||
<div v-if="selectedTokenId">
|
||||
<n-tag :type="wsStatusType">
|
||||
{{ wsStatusText }}
|
||||
</n-tag>
|
||||
<n-button
|
||||
v-if="wsStatus !== 'connected'"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
@click="connectWebSocket"
|
||||
>
|
||||
连接WebSocket
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 预设消息测试 -->
|
||||
<n-divider title-placement="left">
|
||||
预设消息测试
|
||||
</n-divider>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendHeartbeat"
|
||||
>
|
||||
💗 发送心跳
|
||||
</n-button>
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendGetRoleInfo"
|
||||
>
|
||||
👤 获取角色信息
|
||||
</n-button>
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendGetDataVersion"
|
||||
>
|
||||
📦 获取数据版本
|
||||
</n-button>
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendSignIn"
|
||||
>
|
||||
📅 签到
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 自定义消息发送 -->
|
||||
<n-divider title-placement="left">
|
||||
自定义消息
|
||||
</n-divider>
|
||||
<div class="space-y-2">
|
||||
<n-input
|
||||
v-model:value="customCmd"
|
||||
placeholder="命令 (例如: role_getroleinfo)"
|
||||
class="w-full"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="customBody"
|
||||
type="textarea"
|
||||
placeholder="消息体 JSON (例如: {"clientVersion": "1.65.3-wx"})"
|
||||
:rows="3"
|
||||
class="w-full"
|
||||
/>
|
||||
<n-button
|
||||
:disabled="!canSendMessage || !customCmd"
|
||||
type="primary"
|
||||
@click="sendCustomMessage"
|
||||
>
|
||||
🚀 发送自定义消息
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 消息历史 -->
|
||||
<n-divider title-placement="left">
|
||||
消息历史
|
||||
</n-divider>
|
||||
<div class="message-history max-h-96 overflow-y-auto">
|
||||
<div
|
||||
v-for="(message, index) in messageHistory"
|
||||
:key="index"
|
||||
class="message-item p-3 mb-2 rounded border"
|
||||
:class="message.type === 'sent' ? 'bg-blue-50 border-blue-200' : 'bg-green-50 border-green-200'"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-semibold">
|
||||
{{ message.type === 'sent' ? '📤 发送' : '📨 接收' }}
|
||||
<span class="text-sm text-gray-500 ml-2">{{ formatTime(message.timestamp) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.cmd"
|
||||
class="text-sm"
|
||||
>
|
||||
<strong>命令:</strong> {{ message.cmd }}
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<n-collapse>
|
||||
<n-collapse-item
|
||||
title="查看详细数据"
|
||||
name="detail"
|
||||
>
|
||||
<pre class="text-xs bg-gray-100 p-2 rounded overflow-x-auto">{{ formatJSON(message.data) }}</pre>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="messageHistory.length === 0"
|
||||
class="text-center text-gray-500"
|
||||
>
|
||||
暂无消息历史
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useTokenStore } from '../stores/tokenStore'
|
||||
import { useGameRolesStore } from '../stores/gameRoles'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const gameRolesStore = useGameRolesStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const selectedTokenId = ref(null)
|
||||
const customCmd = ref('')
|
||||
const customBody = ref('{}')
|
||||
const messageHistory = ref([])
|
||||
|
||||
// 计算属性
|
||||
const tokenOptions = computed(() => {
|
||||
return gameRolesStore.gameRoles.map(role => ({
|
||||
label: role.name,
|
||||
value: role.id
|
||||
}))
|
||||
})
|
||||
|
||||
const wsStatus = computed(() => {
|
||||
return selectedTokenId.value ? tokenStore.getWebSocketStatus(selectedTokenId.value) : 'disconnected'
|
||||
})
|
||||
|
||||
const wsStatusType = computed(() => {
|
||||
switch (wsStatus.value) {
|
||||
case 'connected': return 'success'
|
||||
case 'connecting': return 'warning'
|
||||
case 'error': return 'error'
|
||||
default: return 'default'
|
||||
}
|
||||
})
|
||||
|
||||
const wsStatusText = computed(() => {
|
||||
switch (wsStatus.value) {
|
||||
case 'connected': return '🟢 已连接'
|
||||
case 'connecting': return '🟡 连接中'
|
||||
case 'error': return '🔴 连接错误'
|
||||
default: return '⚪ 未连接'
|
||||
}
|
||||
})
|
||||
|
||||
const canSendMessage = computed(() => {
|
||||
return selectedTokenId.value && wsStatus.value === 'connected'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const connectWebSocket = () => {
|
||||
if (!selectedTokenId.value) return
|
||||
|
||||
const role = gameRolesStore.gameRoles.find(r => r.id === selectedTokenId.value)
|
||||
if (role) {
|
||||
gameRolesStore.selectRole(role)
|
||||
message.success('正在建立WebSocket连接...')
|
||||
}
|
||||
}
|
||||
|
||||
const addToHistory = (type, data, cmd = null) => {
|
||||
messageHistory.value.unshift({
|
||||
type,
|
||||
timestamp: new Date().toISOString(),
|
||||
cmd,
|
||||
data
|
||||
})
|
||||
|
||||
// 保持历史记录在合理范围内
|
||||
if (messageHistory.value.length > 50) {
|
||||
messageHistory.value = messageHistory.value.slice(0, 50)
|
||||
}
|
||||
}
|
||||
|
||||
const sendHeartbeat = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendHeartbeat(selectedTokenId.value)
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: '_sys/ack' }, '_sys/ack')
|
||||
message.success('心跳消息已发送')
|
||||
} else {
|
||||
message.error('心跳消息发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendGetRoleInfo = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendGetRoleInfo(selectedTokenId.value)
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: 'role_getroleinfo' }, 'role_getroleinfo')
|
||||
message.success('角色信息请求已发送')
|
||||
} else {
|
||||
message.error('角色信息请求发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendGetDataVersion = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendGameMessage(selectedTokenId.value, 'system_getdatabundlever', { isAudit: false })
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: 'system_getdatabundlever' }, 'system_getdatabundlever')
|
||||
message.success('数据版本请求已发送')
|
||||
} else {
|
||||
message.error('数据版本请求发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendSignIn = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendGameMessage(selectedTokenId.value, 'system_signinreward', {})
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: 'system_signinreward' }, 'system_signinreward')
|
||||
message.success('签到请求已发送')
|
||||
} else {
|
||||
message.error('签到请求发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendCustomMessage = () => {
|
||||
if (!canSendMessage.value || !customCmd.value) return
|
||||
|
||||
try {
|
||||
const body = JSON.parse(customBody.value || '{}')
|
||||
const success = tokenStore.sendGameMessage(selectedTokenId.value, customCmd.value, body)
|
||||
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: customCmd.value, body }, customCmd.value)
|
||||
message.success(`自定义消息 ${customCmd.value} 已发送`)
|
||||
|
||||
// 清空输入
|
||||
customCmd.value = ''
|
||||
customBody.value = '{}'
|
||||
} else {
|
||||
message.error('自定义消息发送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('消息体JSON格式错误: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleTimeString()
|
||||
}
|
||||
|
||||
const formatJSON = (data) => {
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
|
||||
// 监听WebSocket消息(模拟,实际需要在tokenStore中触发事件)
|
||||
watch(() => tokenStore.wsConnections, (connections) => {
|
||||
if (!selectedTokenId.value || !connections[selectedTokenId.value]) return
|
||||
|
||||
const connection = connections[selectedTokenId.value]
|
||||
if (connection.lastMessage) {
|
||||
const lastMessage = connection.lastMessage
|
||||
addToHistory('received', lastMessage.parsed, lastMessage.parsed?.cmd)
|
||||
}
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-tester {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.message-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
454
src/components/TeamStatus.vue
Normal file
454
src/components/TeamStatus.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div class="team-status-card">
|
||||
<div class="card-header">
|
||||
<img src="/icons/Ob7pyorzmHiJcbab2c25af264d0758b527bc1b61cc3b.png" alt="队伍图标" class="team-icon">
|
||||
<div class="team-info">
|
||||
<h3>队伍阵容</h3>
|
||||
<p>当前使用的战斗阵容</p>
|
||||
</div>
|
||||
<div class="team-selector">
|
||||
<button
|
||||
v-for="teamId in availableTeams"
|
||||
:key="teamId"
|
||||
@click="selectTeam(teamId)"
|
||||
:class="[
|
||||
'team-button',
|
||||
{ active: currentTeam === teamId }
|
||||
]"
|
||||
>
|
||||
{{ teamId }}
|
||||
</button>
|
||||
<button
|
||||
@click="refreshTeamData"
|
||||
class="team-button refresh-button"
|
||||
title="刷新队伍数据"
|
||||
>
|
||||
🔄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="team-display">
|
||||
<div class="current-team-info">
|
||||
<span class="label">当前阵容</span>
|
||||
<span class="team-number">阵容 {{ currentTeam }}</span>
|
||||
</div>
|
||||
|
||||
<div class="heroes-container">
|
||||
<div class="heroes-grid">
|
||||
<div
|
||||
v-for="hero in currentTeamHeroes"
|
||||
:key="hero.id || hero.name"
|
||||
class="hero-card"
|
||||
>
|
||||
<img
|
||||
v-if="hero.avatar"
|
||||
:src="hero.avatar"
|
||||
:alt="hero.name"
|
||||
class="hero-avatar"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="hero-placeholder"
|
||||
>
|
||||
{{ hero.name?.substring(0, 2) || '?' }}
|
||||
</div>
|
||||
<span class="hero-name">{{ hero.name || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!currentTeamHeroes.length" class="empty-team">
|
||||
<p>暂无队伍信息</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const currentTeam = ref(1)
|
||||
const availableTeams = ref([1, 2, 3, 4])
|
||||
|
||||
// 计算属性
|
||||
const presetTeamInfo = computed(() => {
|
||||
return tokenStore.gameData?.presetTeam || null
|
||||
})
|
||||
|
||||
const currentTeamHeroes = computed(() => {
|
||||
if (!presetTeamInfo.value) {
|
||||
console.log('👥 TeamStatus: presetTeamInfo 为空')
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('👥 TeamStatus: 当前队伍信息结构:', {
|
||||
presetTeamInfo: presetTeamInfo.value,
|
||||
currentTeam: currentTeam.value,
|
||||
hasPresetTeamInfo: !!presetTeamInfo.value.presetTeamInfo,
|
||||
presetTeamInfoKeys: presetTeamInfo.value.presetTeamInfo ? Object.keys(presetTeamInfo.value.presetTeamInfo) : []
|
||||
})
|
||||
|
||||
// 尝试多种可能的数据结构
|
||||
let teamData = null
|
||||
|
||||
// 方式1: 标准结构 presetTeamInfo[teamId].teamInfo
|
||||
if (presetTeamInfo.value.presetTeamInfo?.[currentTeam.value]?.teamInfo) {
|
||||
teamData = presetTeamInfo.value.presetTeamInfo[currentTeam.value].teamInfo
|
||||
console.log('👥 TeamStatus: 使用标准结构获取队伍数据')
|
||||
}
|
||||
// 方式2: 直接在presetTeamInfo[teamId]下
|
||||
else if (presetTeamInfo.value.presetTeamInfo?.[currentTeam.value]) {
|
||||
const teamInfo = presetTeamInfo.value.presetTeamInfo[currentTeam.value]
|
||||
if (typeof teamInfo === 'object' && !Array.isArray(teamInfo)) {
|
||||
teamData = teamInfo
|
||||
console.log('👥 TeamStatus: 使用直接结构获取队伍数据')
|
||||
}
|
||||
}
|
||||
// 方式3: 查找任何包含英雄数据的结构
|
||||
else if (presetTeamInfo.value.presetTeamInfo) {
|
||||
for (const [key, value] of Object.entries(presetTeamInfo.value.presetTeamInfo)) {
|
||||
if (value && typeof value === 'object') {
|
||||
// 查找包含heroId或类似字段的数据
|
||||
if (value.teamInfo || value.heroes || value.formation ||
|
||||
Object.values(value).some(v => v && v.heroId)) {
|
||||
teamData = value.teamInfo || value.heroes || value.formation || value
|
||||
console.log(`👥 TeamStatus: 在 ${key} 中找到队伍数据`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!teamData) {
|
||||
console.log('👥 TeamStatus: 未找到队伍数据')
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('👥 TeamStatus: 解析队伍数据:', teamData)
|
||||
|
||||
// 转换队伍信息为英雄数组
|
||||
const heroes = []
|
||||
|
||||
// 处理不同的数据格式
|
||||
if (Array.isArray(teamData)) {
|
||||
// 数组格式
|
||||
teamData.forEach((hero, index) => {
|
||||
if (hero && (hero.heroId || hero.id)) {
|
||||
heroes.push({
|
||||
id: hero.heroId || hero.id,
|
||||
name: getHeroName(hero.heroId || hero.id),
|
||||
position: index + 1,
|
||||
level: hero.level || 1
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (typeof teamData === 'object') {
|
||||
// 对象格式(position => hero)
|
||||
for (const [position, hero] of Object.entries(teamData)) {
|
||||
if (hero && (hero.heroId || hero.id)) {
|
||||
heroes.push({
|
||||
id: hero.heroId || hero.id,
|
||||
name: getHeroName(hero.heroId || hero.id),
|
||||
position: position,
|
||||
level: hero.level || 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('👥 TeamStatus: 解析出的英雄列表:', heroes)
|
||||
return heroes
|
||||
})
|
||||
|
||||
// 从presetTeamInfo获取可用队伍数量
|
||||
const updateAvailableTeams = () => {
|
||||
if (!presetTeamInfo.value?.presetTeamInfo) return
|
||||
|
||||
const teams = Object.keys(presetTeamInfo.value.presetTeamInfo)
|
||||
.map(Number)
|
||||
.filter(num => !isNaN(num))
|
||||
.sort((a, b) => a - b)
|
||||
|
||||
if (teams.length > 0) {
|
||||
availableTeams.value = teams
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前队伍
|
||||
const updateCurrentTeam = () => {
|
||||
if (presetTeamInfo.value?.presetTeamInfo?.useTeamId) {
|
||||
currentTeam.value = presetTeamInfo.value.presetTeamInfo.useTeamId
|
||||
}
|
||||
}
|
||||
|
||||
// 获取英雄名称(这里需要英雄数据字典)
|
||||
const getHeroName = (heroId) => {
|
||||
// 暂时返回英雄ID,后续可以添加英雄名称映射
|
||||
const heroNames = {
|
||||
1: '剑士',
|
||||
2: '法师',
|
||||
3: '弓手',
|
||||
4: '盗贼',
|
||||
5: '牧师'
|
||||
// 更多英雄映射...
|
||||
}
|
||||
|
||||
return heroNames[heroId] || `英雄${heroId}`
|
||||
}
|
||||
|
||||
// 选择队伍
|
||||
const selectTeam = (teamId) => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
currentTeam.value = teamId
|
||||
|
||||
// 发送切换队伍的消息
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'presetteam_saveteam', { teamId })
|
||||
|
||||
message.info(`切换到阵容 ${teamId}`)
|
||||
}
|
||||
|
||||
// 监听预设队伍信息变化
|
||||
watch(presetTeamInfo, (newValue) => {
|
||||
if (newValue) {
|
||||
updateAvailableTeams()
|
||||
updateCurrentTeam()
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 刷新队伍数据
|
||||
const refreshTeamData = () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
console.log('👥 手动刷新队伍数据')
|
||||
|
||||
// 发送多个可能的队伍相关命令
|
||||
const commands = [
|
||||
'presetteam_getteam',
|
||||
'role_gettargetteam',
|
||||
'role_getroleinfo' // 角色信息中可能包含队伍数据
|
||||
]
|
||||
|
||||
commands.forEach(cmd => {
|
||||
tokenStore.sendMessage(tokenId, cmd, {})
|
||||
console.log(`👥 发送命令: ${cmd}`)
|
||||
})
|
||||
|
||||
message.info('正在刷新队伍数据...')
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 获取队伍信息
|
||||
refreshTeamData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.team-status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.team-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-info {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.team-selector {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.team-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.refresh-button {
|
||||
background: var(--success-color, #10b981);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: var(--success-color-dark, #059669);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
.current-team-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.team-number {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.heroes-container {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.heroes-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--border-radius-medium);
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-fast);
|
||||
min-width: 80px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hero-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.empty-team {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.team-selector {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.heroes-grid {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
507
src/components/TokenManager.vue
Normal file
507
src/components/TokenManager.vue
Normal file
@@ -0,0 +1,507 @@
|
||||
<template>
|
||||
<div class="token-manager">
|
||||
<div class="header">
|
||||
<h3>Token管理器</h3>
|
||||
<div class="header-actions">
|
||||
<n-button
|
||||
size="small"
|
||||
@click="refreshTokens"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><Refresh /></n-icon>
|
||||
</template>
|
||||
刷新
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="warning"
|
||||
@click="exportTokens"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><Download /></n-icon>
|
||||
</template>
|
||||
导出
|
||||
</n-button>
|
||||
<n-upload
|
||||
:show-file-list="false"
|
||||
accept=".json"
|
||||
@change="importTokens"
|
||||
>
|
||||
<n-button
|
||||
size="small"
|
||||
type="info"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><CloudUpload /></n-icon>
|
||||
</template>
|
||||
导入
|
||||
</n-button>
|
||||
</n-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户Token -->
|
||||
<div class="token-section">
|
||||
<h4>用户认证Token</h4>
|
||||
<div
|
||||
v-if="localTokenStore.userToken"
|
||||
class="token-item"
|
||||
>
|
||||
<div class="token-info">
|
||||
<span class="token-label">Token:</span>
|
||||
<span class="token-value">{{ maskToken(localTokenStore.userToken) }}</span>
|
||||
</div>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="error"
|
||||
@click="clearUserToken"
|
||||
>
|
||||
清除
|
||||
</n-button>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="empty-token"
|
||||
>
|
||||
<span>未设置用户Token</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏Token列表 -->
|
||||
<div class="token-section">
|
||||
<h4>游戏角色Token ({{ Object.keys(localTokenStore.gameTokens).length }}个)</h4>
|
||||
<div class="game-tokens-list">
|
||||
<div
|
||||
v-for="(tokenData, roleId) in localTokenStore.gameTokens"
|
||||
:key="roleId"
|
||||
class="game-token-item"
|
||||
>
|
||||
<div class="token-header">
|
||||
<div class="role-info">
|
||||
<span class="role-name">{{ tokenData.roleName }}</span>
|
||||
<span class="role-server">{{ tokenData.server }}</span>
|
||||
</div>
|
||||
<div class="token-actions">
|
||||
<n-button
|
||||
size="tiny"
|
||||
:type="getWSStatus(roleId) === 'connected' ? 'success' : 'default'"
|
||||
@click="toggleWebSocket(roleId, tokenData)"
|
||||
>
|
||||
{{ getWSStatus(roleId) === 'connected' ? '断开WS' : '连接WS' }}
|
||||
</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="warning"
|
||||
@click="regenerateToken(roleId)"
|
||||
>
|
||||
重生成
|
||||
</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="error"
|
||||
@click="removeToken(roleId)"
|
||||
>
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="token-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Token:</span>
|
||||
<span class="detail-value">{{ maskToken(tokenData.token) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">WebSocket URL:</span>
|
||||
<span class="detail-value">{{ tokenData.wsUrl }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">创建时间:</span>
|
||||
<span class="detail-value">{{ formatTime(tokenData.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">最后使用:</span>
|
||||
<span class="detail-value">{{ formatTime(tokenData.lastUsed) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">连接状态:</span>
|
||||
<n-tag
|
||||
size="small"
|
||||
:type="getWSStatusType(getWSStatus(roleId))"
|
||||
>
|
||||
{{ getWSStatusText(getWSStatus(roleId)) }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作 -->
|
||||
<div class="bulk-actions">
|
||||
<n-button
|
||||
type="warning"
|
||||
@click="cleanExpiredTokens"
|
||||
>
|
||||
清理过期Token
|
||||
</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="clearAllTokens"
|
||||
>
|
||||
清除所有Token
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useMessage, useDialog } from 'naive-ui'
|
||||
import { useLocalTokenStore } from '@/stores/localTokenManager'
|
||||
import { useGameRolesStore } from '@/stores/gameRoles'
|
||||
import {
|
||||
Refresh,
|
||||
Download,
|
||||
CloudUpload
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const localTokenStore = useLocalTokenStore()
|
||||
const gameRolesStore = useGameRolesStore()
|
||||
|
||||
// 方法
|
||||
const maskToken = (token) => {
|
||||
if (!token) return ''
|
||||
const len = token.length
|
||||
if (len <= 8) return token
|
||||
return token.substring(0, 4) + '***' + token.substring(len - 4)
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const getWSStatus = (roleId) => {
|
||||
return localTokenStore.getWebSocketStatus(roleId)
|
||||
}
|
||||
|
||||
const getWSStatusType = (status) => {
|
||||
switch (status) {
|
||||
case 'connected': return 'success'
|
||||
case 'error': return 'error'
|
||||
case 'connecting': return 'warning'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const getWSStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'connected': return '已连接'
|
||||
case 'error': return '连接错误'
|
||||
case 'connecting': return '连接中'
|
||||
default: return '未连接'
|
||||
}
|
||||
}
|
||||
|
||||
const refreshTokens = () => {
|
||||
localTokenStore.initTokenManager()
|
||||
message.success('Token数据已刷新')
|
||||
}
|
||||
|
||||
const clearUserToken = () => {
|
||||
dialog.warning({
|
||||
title: '清除用户Token',
|
||||
content: '确定要清除用户认证Token吗?这将会退出登录。',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
localTokenStore.clearUserToken()
|
||||
message.success('用户Token已清除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toggleWebSocket = (roleId, tokenData) => {
|
||||
const status = getWSStatus(roleId)
|
||||
|
||||
if (status === 'connected') {
|
||||
localTokenStore.closeWebSocketConnection(roleId)
|
||||
message.info('WebSocket连接已断开')
|
||||
} else {
|
||||
try {
|
||||
localTokenStore.createWebSocketConnection(roleId, tokenData.token, tokenData.wsUrl)
|
||||
message.success('正在建立WebSocket连接...')
|
||||
} catch (error) {
|
||||
message.error('建立WebSocket连接失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const regenerateToken = (roleId) => {
|
||||
dialog.info({
|
||||
title: '重新生成Token',
|
||||
content: '确定要为此角色重新生成游戏Token吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
const oldTokenData = localTokenStore.getGameToken(roleId)
|
||||
if (oldTokenData) {
|
||||
const newToken = 'game_token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 16)
|
||||
localTokenStore.updateGameToken(roleId, {
|
||||
token: newToken,
|
||||
regeneratedAt: new Date().toISOString()
|
||||
})
|
||||
message.success('Token已重新生成')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const removeToken = (roleId) => {
|
||||
dialog.warning({
|
||||
title: '删除Token',
|
||||
content: '确定要删除此角色的游戏Token吗?这将断开相关的WebSocket连接。',
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
localTokenStore.removeGameToken(roleId)
|
||||
message.success('Token已删除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const exportTokens = () => {
|
||||
try {
|
||||
const tokenData = localTokenStore.exportTokens()
|
||||
const dataStr = JSON.stringify(tokenData, null, 2)
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(dataBlob)
|
||||
link.download = `tokens_backup_${new Date().toISOString().split('T')[0]}.json`
|
||||
link.click()
|
||||
|
||||
message.success('Token数据已导出')
|
||||
} catch (error) {
|
||||
message.error('导出失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const importTokens = ({ file }) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const tokenData = JSON.parse(e.target.result)
|
||||
const result = localTokenStore.importTokens(tokenData)
|
||||
|
||||
if (result.success) {
|
||||
message.success(result.message)
|
||||
// 刷新游戏角色数据
|
||||
gameRolesStore.fetchGameRoles()
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('导入失败:文件格式错误')
|
||||
}
|
||||
}
|
||||
reader.readAsText(file.file)
|
||||
}
|
||||
|
||||
const cleanExpiredTokens = () => {
|
||||
dialog.info({
|
||||
title: '清理过期Token',
|
||||
content: '确定要清理超过24小时未使用的Token吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
const cleanedCount = localTokenStore.cleanExpiredTokens()
|
||||
message.success(`已清理 ${cleanedCount} 个过期Token`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clearAllTokens = () => {
|
||||
dialog.error({
|
||||
title: '清除所有Token',
|
||||
content: '确定要清除所有游戏Token吗?这将断开所有WebSocket连接。此操作不可恢复!',
|
||||
positiveText: '确定清除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
localTokenStore.clearAllGameTokens()
|
||||
message.success('所有游戏Token已清除')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.token-manager {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
margin: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.token-section {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
h4 {
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
}
|
||||
|
||||
.token-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.token-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.token-label {
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.token-value {
|
||||
font-family: monospace;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.empty-token {
|
||||
padding: var(--spacing-md);
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.game-tokens-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.game-token-item {
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.token-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.role-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.role-server {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.token-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.token-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-primary);
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.token-item {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.token-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.token-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
404
src/components/TowerStatus.vue
Normal file
404
src/components/TowerStatus.vue
Normal file
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<div class="tower-status-card">
|
||||
<div class="card-header">
|
||||
<div class="header-info">
|
||||
<img src="/icons/1733492491706148.png" alt="爬塔图标" class="tower-icon">
|
||||
<div class="tower-info">
|
||||
<h3>咸将塔</h3>
|
||||
<p>一个不小心就过了</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="energy-display">
|
||||
<img src="/icons/xiaoyugan.png" alt="小鱼干" class="energy-icon">
|
||||
<span class="energy-count">{{ towerEnergy }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="tower-floor">
|
||||
<span class="label">当前层数</span>
|
||||
<span class="floor-number">{{ currentFloor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button
|
||||
:class="[
|
||||
'climb-button',
|
||||
{
|
||||
'active': canClimb,
|
||||
'disabled': !canClimb
|
||||
}
|
||||
]"
|
||||
:disabled="!canClimb"
|
||||
@click="startTowerClimb"
|
||||
>
|
||||
开始爬塔
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const isClimbing = ref(false)
|
||||
|
||||
// 计算属性 - 从gameData中获取塔相关信息
|
||||
const roleInfo = computed(() => {
|
||||
const data = tokenStore.gameData?.roleInfo || null
|
||||
console.log('🗼 TowerStatus roleInfo 计算属性更新:', data)
|
||||
if (data?.role?.tower) {
|
||||
console.log('🗼 TowerStatus 发现tower数据:', data.role.tower)
|
||||
} else {
|
||||
console.log('🗼 TowerStatus 没有找到tower数据, gameData:', tokenStore.gameData)
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const currentFloor = computed(() => {
|
||||
const tower = roleInfo.value?.role?.tower
|
||||
console.log('🗼 TowerStatus currentFloor 计算属性更新')
|
||||
console.log('🗼 TowerStatus 输入的tower数据:', tower)
|
||||
console.log('🗼 TowerStatus 完整的roleInfo:', roleInfo.value)
|
||||
|
||||
if (!tower) {
|
||||
console.log('🗼 没有tower对象,显示默认值')
|
||||
return "0 - 0"
|
||||
}
|
||||
|
||||
if (!tower.id && tower.id !== 0) {
|
||||
console.log('🗼 没有塔ID或ID无效,显示默认值, tower.id:', tower.id)
|
||||
return "0 - 0"
|
||||
}
|
||||
|
||||
const towerId = tower.id
|
||||
const floor = Math.floor(towerId / 10) + 1
|
||||
const layer = towerId % 10 + 1
|
||||
const result = `${floor} - ${layer}`
|
||||
console.log(`🗼 计算层数: towerId=${towerId} -> floor=${floor}, layer=${layer} -> ${result}`)
|
||||
return result
|
||||
})
|
||||
|
||||
const towerEnergy = computed(() => {
|
||||
const tower = roleInfo.value?.role?.tower
|
||||
console.log('🗼 TowerStatus towerEnergy 计算属性更新')
|
||||
console.log('🗼 TowerStatus tower对象:', tower)
|
||||
|
||||
const energy = tower?.energy || 0
|
||||
console.log('🗼 TowerStatus 计算出的energy:', energy)
|
||||
return energy
|
||||
})
|
||||
|
||||
const canClimb = computed(() => {
|
||||
return towerEnergy.value > 0 && !isClimbing.value
|
||||
})
|
||||
|
||||
// 方法
|
||||
const startTowerClimb = async () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
if (!canClimb.value) {
|
||||
message.warning('体力不足或正在爬塔中')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isClimbing.value = true
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
message.info('开始爬塔挑战...')
|
||||
|
||||
// 发送爬塔命令
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'fight_starttower', {}, 10000)
|
||||
|
||||
message.success('爬塔命令已发送')
|
||||
|
||||
// 立即查询塔信息以获取最新状态
|
||||
console.log('🗼 爬塔完成,立即查询塔信息')
|
||||
await getTowerInfo()
|
||||
|
||||
// 再延迟查询一次确保数据同步
|
||||
setTimeout(async () => {
|
||||
console.log('🗼 延迟查询塔信息')
|
||||
await getTowerInfo()
|
||||
}, 3000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('爬塔失败:', error)
|
||||
message.error('爬塔失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
isClimbing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getTowerInfo = async () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
console.warn('🗼 getTowerInfo: 没有选中的Token')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
console.log('🗼 getTowerInfo: 开始获取塔信息, tokenId:', tokenId)
|
||||
|
||||
// 检查WebSocket连接状态
|
||||
const wsStatus = tokenStore.getWebSocketStatus(tokenId)
|
||||
console.log('🗼 getTowerInfo: WebSocket状态:', wsStatus)
|
||||
|
||||
if (wsStatus !== 'connected') {
|
||||
console.warn('🗼 getTowerInfo: WebSocket未连接,无法获取数据')
|
||||
return
|
||||
}
|
||||
|
||||
// 首先获取角色信息,这包含了塔的数据
|
||||
console.log('🗼 getTowerInfo: 正在请求角色信息...')
|
||||
const roleResult = tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
console.log('🗼 getTowerInfo: 角色信息请求结果:', roleResult)
|
||||
|
||||
// 直接请求塔信息
|
||||
console.log('🗼 getTowerInfo: 正在请求塔信息...')
|
||||
const towerResult = tokenStore.sendMessage(tokenId, 'tower_getinfo')
|
||||
console.log('🗼 getTowerInfo: 塔信息请求结果:', towerResult)
|
||||
|
||||
// 检查当前gameData状态
|
||||
console.log('🗼 getTowerInfo: 当前gameData:', tokenStore.gameData)
|
||||
console.log('🗼 getTowerInfo: 当前roleInfo:', tokenStore.gameData?.roleInfo)
|
||||
console.log('🗼 getTowerInfo: 当前tower数据:', tokenStore.gameData?.roleInfo?.role?.tower)
|
||||
|
||||
if (!roleResult && !towerResult) {
|
||||
console.error('🗼 getTowerInfo: 所有请求都失败了')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('🗼 getTowerInfo: 获取塔信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 监听WebSocket连接状态变化
|
||||
const wsStatus = computed(() => {
|
||||
if (!tokenStore.selectedToken) return 'disconnected'
|
||||
return tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
})
|
||||
|
||||
// 监听WebSocket连接状态,连接成功后自动获取塔信息
|
||||
watch(wsStatus, (newStatus, oldStatus) => {
|
||||
console.log(`🗼 WebSocket状态变化: ${oldStatus} -> ${newStatus}`)
|
||||
|
||||
if (newStatus === 'connected' && oldStatus !== 'connected') {
|
||||
console.log('🗼 WebSocket已连接,自动获取塔信息')
|
||||
// 延迟一点时间让WebSocket完全就绪
|
||||
setTimeout(() => {
|
||||
getTowerInfo()
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听选中Token变化
|
||||
watch(() => tokenStore.selectedToken, (newToken, oldToken) => {
|
||||
if (newToken && newToken.id !== oldToken?.id) {
|
||||
console.log('🗼 Token已切换,获取新的塔信息')
|
||||
// 检查WebSocket是否已连接
|
||||
const status = tokenStore.getWebSocketStatus(newToken.id)
|
||||
if (status === 'connected') {
|
||||
getTowerInfo()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
console.log('🗼 TowerStatus 组件已挂载')
|
||||
console.log('🗼 当前选中Token:', tokenStore.selectedToken?.name)
|
||||
console.log('🗼 当前选中Token ID:', tokenStore.selectedToken?.id)
|
||||
console.log('🗼 当前WebSocket状态:', wsStatus.value)
|
||||
console.log('🗼 当前游戏数据:', tokenStore.gameData)
|
||||
console.log('🗼 当前roleInfo:', tokenStore.gameData?.roleInfo)
|
||||
console.log('🗼 当前tower数据:', tokenStore.gameData?.roleInfo?.role?.tower)
|
||||
|
||||
// 检查WebSocket客户端
|
||||
if (tokenStore.selectedToken) {
|
||||
const client = tokenStore.getWebSocketClient(tokenStore.selectedToken.id)
|
||||
console.log('🗼 WebSocket客户端:', client)
|
||||
console.log('🗼 WebSocket客户端状态:', client ? 'exists' : 'null')
|
||||
}
|
||||
|
||||
// 组件挂载时获取塔信息
|
||||
if (tokenStore.selectedToken && wsStatus.value === 'connected') {
|
||||
console.log('🗼 条件满足,开始获取塔信息')
|
||||
getTowerInfo()
|
||||
} else if (!tokenStore.selectedToken) {
|
||||
console.log('🗼 没有选中的Token,无法获取塔信息')
|
||||
} else {
|
||||
console.log('🗼 WebSocket未连接,等待连接后自动获取塔信息')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.tower-status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
border-left: 4px solid #6366f1;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.tower-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tower-info {
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.energy-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
background: var(--bg-tertiary);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.energy-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.energy-count {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.tower-floor {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.floor-number {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
|
||||
.climb-button {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&.active {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #5855eb;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
margin-top: var(--spacing-sm);
|
||||
padding: var(--spacing-xs);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-small);
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
|
||||
small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.energy-display {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
496
src/components/WebSocketTester.vue
Normal file
496
src/components/WebSocketTester.vue
Normal file
@@ -0,0 +1,496 @@
|
||||
<template>
|
||||
<div class="websocket-tester">
|
||||
<n-card
|
||||
title="WebSocket连接测试"
|
||||
class="mb-4"
|
||||
>
|
||||
<n-space
|
||||
direction="vertical"
|
||||
size="large"
|
||||
>
|
||||
<!-- 连接状态 -->
|
||||
<n-card
|
||||
title="连接状态"
|
||||
size="small"
|
||||
>
|
||||
<n-space align="center">
|
||||
<n-tag
|
||||
:type="getStatusType(status)"
|
||||
size="large"
|
||||
>
|
||||
{{ getStatusText(status) }}
|
||||
</n-tag>
|
||||
<n-button
|
||||
v-if="selectedRoleId && status !== 'connected'"
|
||||
type="primary"
|
||||
:loading="status === 'connecting'"
|
||||
@click="connectWebSocket"
|
||||
>
|
||||
连接WebSocket
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="status === 'connected'"
|
||||
type="error"
|
||||
@click="disconnectWebSocket"
|
||||
>
|
||||
断开连接
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<!-- 角色选择 -->
|
||||
<n-form-item label="选择角色">
|
||||
<n-select
|
||||
v-model:value="selectedRoleId"
|
||||
placeholder="请选择要测试的角色"
|
||||
:options="roleOptions"
|
||||
@update:value="onRoleChange"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<!-- 连接详情 -->
|
||||
<n-card
|
||||
v-if="connectionDetails"
|
||||
title="连接详情"
|
||||
size="small"
|
||||
>
|
||||
<n-descriptions
|
||||
:column="2"
|
||||
bordered
|
||||
size="small"
|
||||
>
|
||||
<n-descriptions-item label="角色ID">
|
||||
{{ connectionDetails.roleId }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="状态">
|
||||
<n-tag :type="getStatusType(connectionDetails.status)">
|
||||
{{ getStatusText(connectionDetails.status) }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="WebSocket URL">
|
||||
<n-text
|
||||
code
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
{{ connectionDetails.wsUrl }}
|
||||
</n-text>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="连接时间">
|
||||
{{ formatTime(connectionDetails.connectedAt) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item
|
||||
v-if="connectionDetails.lastError"
|
||||
label="最后错误"
|
||||
>
|
||||
<n-text type="error">
|
||||
{{ connectionDetails.lastError }}
|
||||
</n-text>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item
|
||||
v-if="connectionDetails.reconnectAttempt > 0"
|
||||
label="重连次数"
|
||||
>
|
||||
{{ connectionDetails.reconnectAttempt }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
|
||||
<!-- 游戏命令测试 -->
|
||||
<n-card
|
||||
v-if="status === 'connected'"
|
||||
title="游戏命令测试"
|
||||
size="small"
|
||||
>
|
||||
<n-space direction="vertical">
|
||||
<n-form-item label="选择命令">
|
||||
<n-select
|
||||
v-model:value="selectedCommand"
|
||||
placeholder="请选择要测试的命令"
|
||||
:options="commandOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item
|
||||
v-if="selectedCommand"
|
||||
label="命令参数 (JSON)"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="commandParams"
|
||||
type="textarea"
|
||||
placeholder="例如: {"roleId": 123456}"
|
||||
:rows="3"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-space>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!selectedCommand"
|
||||
:loading="sendingCommand"
|
||||
@click="sendCommand"
|
||||
>
|
||||
发送命令
|
||||
</n-button>
|
||||
<n-button
|
||||
type="success"
|
||||
:disabled="!selectedCommand"
|
||||
:loading="waitingResponse"
|
||||
@click="sendCommandWithPromise"
|
||||
>
|
||||
发送并等待响应
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<!-- 消息日志 -->
|
||||
<n-card
|
||||
title="消息日志"
|
||||
size="small"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="clearLog"
|
||||
>
|
||||
清空日志
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<div class="message-log">
|
||||
<div
|
||||
v-for="(msg, index) in messageLog"
|
||||
:key="index"
|
||||
class="message-item"
|
||||
:class="`message-${msg.type}`"
|
||||
>
|
||||
<div class="message-header">
|
||||
<n-tag
|
||||
:type="msg.type === 'sent' ? 'info' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
{{ msg.type === 'sent' ? '发送' : '接收' }}
|
||||
</n-tag>
|
||||
<n-text
|
||||
depth="3"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
{{ formatTime(msg.timestamp) }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<pre>{{ JSON.stringify(msg.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="messageLog.length === 0"
|
||||
class="no-messages"
|
||||
>
|
||||
暂无消息日志
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useTokenStore } from '../stores/tokenStore.js'
|
||||
|
||||
const message = useMessage()
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 响应式数据
|
||||
const selectedRoleId = ref(null)
|
||||
const status = ref('disconnected')
|
||||
const connectionDetails = ref(null)
|
||||
const selectedCommand = ref(null)
|
||||
const commandParams = ref('{}')
|
||||
const sendingCommand = ref(false)
|
||||
const waitingResponse = ref(false)
|
||||
const messageLog = ref([])
|
||||
|
||||
// Token选项
|
||||
const roleOptions = computed(() => {
|
||||
return tokenStore.gameTokens.map(token => ({
|
||||
label: `${token.name} (${token.server})`,
|
||||
value: token.id
|
||||
}))
|
||||
})
|
||||
|
||||
// 命令选项
|
||||
const commandOptions = [
|
||||
{ label: '获取角色信息', value: 'role_getroleinfo' },
|
||||
{ label: '获取数据包版本', value: 'system_getdatabundlever' },
|
||||
{ label: '签到奖励', value: 'system_signinreward' },
|
||||
{ label: '领取每日任务奖励', value: 'task_claimdailyreward' },
|
||||
{ label: '获取邮件列表', value: 'mail_getlist' },
|
||||
{ label: '领取所有邮件附件', value: 'mail_claimallattachment' },
|
||||
{ label: '获取军团信息', value: 'legion_getinfo' },
|
||||
{ label: '英雄招募', value: 'hero_recruit' },
|
||||
{ label: '领取挂机奖励', value: 'system_claimhangupreward' }
|
||||
]
|
||||
|
||||
// 方法
|
||||
const getStatusType = (statusValue) => {
|
||||
const typeMap = {
|
||||
connected: 'success',
|
||||
connecting: 'warning',
|
||||
disconnected: 'default',
|
||||
reconnecting: 'info',
|
||||
error: 'error'
|
||||
}
|
||||
return typeMap[statusValue] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (statusValue) => {
|
||||
const textMap = {
|
||||
connected: '已连接',
|
||||
connecting: '连接中',
|
||||
disconnected: '已断开',
|
||||
reconnecting: '重连中',
|
||||
error: '连接错误'
|
||||
}
|
||||
return textMap[statusValue] || '未知状态'
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return '-'
|
||||
return new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const onRoleChange = () => {
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
const updateStatus = () => {
|
||||
if (!selectedRoleId.value) {
|
||||
status.value = 'disconnected'
|
||||
connectionDetails.value = null
|
||||
return
|
||||
}
|
||||
|
||||
status.value = tokenStore.getWebSocketStatus(selectedRoleId.value)
|
||||
const connection = tokenStore.wsConnections[selectedRoleId.value]
|
||||
if (connection) {
|
||||
connectionDetails.value = {
|
||||
roleId: selectedRoleId.value,
|
||||
status: connection.status,
|
||||
connectedAt: connection.connectedAt,
|
||||
wsUrl: connection.wsUrl
|
||||
}
|
||||
} else {
|
||||
connectionDetails.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const connectWebSocket = async () => {
|
||||
if (!selectedRoleId.value) {
|
||||
message.error('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
status.value = 'connecting'
|
||||
const token = tokenStore.gameTokens.find(t => t.id === selectedRoleId.value)
|
||||
|
||||
if (!token) {
|
||||
message.error('未找到Token数据')
|
||||
return
|
||||
}
|
||||
|
||||
tokenStore.createWebSocketConnection(
|
||||
selectedRoleId.value,
|
||||
token.token,
|
||||
token.wsUrl
|
||||
)
|
||||
|
||||
// 监听消息
|
||||
startMessageListener()
|
||||
|
||||
message.success('WebSocket连接已启动')
|
||||
} catch (error) {
|
||||
console.error('WebSocket连接失败:', error)
|
||||
message.error('WebSocket连接失败: ' + error.message)
|
||||
} finally {
|
||||
setTimeout(updateStatus, 1000) // 延迟更新状态以等待连接建立
|
||||
}
|
||||
}
|
||||
|
||||
const disconnectWebSocket = () => {
|
||||
if (!selectedRoleId.value) return
|
||||
|
||||
tokenStore.closeWebSocketConnection(selectedRoleId.value)
|
||||
status.value = 'disconnected'
|
||||
connectionDetails.value = null
|
||||
message.info('WebSocket连接已断开')
|
||||
}
|
||||
|
||||
const sendCommand = async () => {
|
||||
if (!selectedCommand.value) {
|
||||
message.error('请选择要发送的命令')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
sendingCommand.value = true
|
||||
|
||||
let params = {}
|
||||
if (commandParams.value.trim()) {
|
||||
params = JSON.parse(commandParams.value)
|
||||
}
|
||||
|
||||
const success = tokenStore.sendMessage(
|
||||
selectedRoleId.value,
|
||||
selectedCommand.value,
|
||||
params
|
||||
)
|
||||
|
||||
if (success) {
|
||||
addToLog('sent', {
|
||||
command: selectedCommand.value,
|
||||
params
|
||||
})
|
||||
message.success('命令发送成功')
|
||||
} else {
|
||||
message.error('命令发送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送命令失败:', error)
|
||||
message.error('发送命令失败: ' + error.message)
|
||||
} finally {
|
||||
sendingCommand.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const sendCommandWithPromise = async () => {
|
||||
if (!selectedCommand.value) {
|
||||
message.error('请选择要发送的命令')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
waitingResponse.value = true
|
||||
|
||||
let params = {}
|
||||
if (commandParams.value.trim()) {
|
||||
params = JSON.parse(commandParams.value)
|
||||
}
|
||||
|
||||
const response = await tokenStore.sendMessageWithPromise(
|
||||
selectedRoleId.value,
|
||||
selectedCommand.value,
|
||||
params
|
||||
)
|
||||
|
||||
addToLog('sent', {
|
||||
command: selectedCommand.value,
|
||||
params
|
||||
})
|
||||
|
||||
addToLog('received', response)
|
||||
|
||||
message.success('命令执行成功,已收到响应')
|
||||
} catch (error) {
|
||||
console.error('发送命令失败:', error)
|
||||
message.error('发送命令失败: ' + error.message)
|
||||
} finally {
|
||||
waitingResponse.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startMessageListener = () => {
|
||||
// 这里简化处理,实际应该通过WebSocket客户端的onMessage事件来接收消息
|
||||
// 由于消息处理在store中,这里只是演示
|
||||
}
|
||||
|
||||
const addToLog = (type, data) => {
|
||||
messageLog.value.unshift({
|
||||
type,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
// 限制日志条数
|
||||
if (messageLog.value.length > 100) {
|
||||
messageLog.value = messageLog.value.slice(0, 100)
|
||||
}
|
||||
}
|
||||
|
||||
const clearLog = () => {
|
||||
messageLog.value = []
|
||||
}
|
||||
|
||||
// 定时更新状态
|
||||
let statusTimer = null
|
||||
|
||||
onMounted(() => {
|
||||
// 定时更新连接状态
|
||||
statusTimer = setInterval(() => {
|
||||
if (selectedRoleId.value) {
|
||||
updateStatus()
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (statusTimer) {
|
||||
clearInterval(statusTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.websocket-tester {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.message-log {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e6;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.message-sent {
|
||||
border-left: 3px solid #2080f0;
|
||||
}
|
||||
|
||||
.message-received {
|
||||
border-left: 3px solid #18a058;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.message-content pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.no-messages {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
17
src/main.js
Normal file
17
src/main.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import naive from 'naive-ui'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import './assets/styles/global.scss'
|
||||
|
||||
// 创建应用实例
|
||||
const app = createApp(App)
|
||||
|
||||
// 使用插件
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(naive)
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
137
src/router/index.js
Normal file
137
src/router/index.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/Home.vue'),
|
||||
meta: {
|
||||
title: '首页',
|
||||
requiresToken: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tokens',
|
||||
name: 'TokenImport',
|
||||
component: () => import('@/views/TokenImport.vue'),
|
||||
meta: {
|
||||
title: 'Token管理',
|
||||
requiresToken: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
meta: {
|
||||
title: '控制台',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'Profile',
|
||||
component: () => import('@/views/Profile.vue'),
|
||||
meta: {
|
||||
title: '个人设置',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/daily-tasks',
|
||||
name: 'DailyTasks',
|
||||
component: () => import('@/views/DailyTasks.vue'),
|
||||
meta: {
|
||||
title: '日常任务',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/game-features',
|
||||
name: 'GameFeatures',
|
||||
component: () => import('@/views/GameFeatures.vue'),
|
||||
meta: {
|
||||
title: '游戏功能',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/message-test',
|
||||
name: 'MessageTest',
|
||||
component: () => import('@/components/MessageTester.vue'),
|
||||
meta: {
|
||||
title: '消息测试',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/websocket-test',
|
||||
name: 'WebSocketTest',
|
||||
component: () => import('@/components/WebSocketTester.vue'),
|
||||
meta: {
|
||||
title: 'WebSocket测试',
|
||||
requiresToken: true
|
||||
}
|
||||
},
|
||||
// 兼容旧路由,重定向到新的token管理页面
|
||||
{
|
||||
path: '/login',
|
||||
redirect: '/tokens'
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
redirect: '/tokens'
|
||||
},
|
||||
{
|
||||
path: '/game-roles',
|
||||
redirect: '/tokens'
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/NotFound.vue'),
|
||||
meta: {
|
||||
title: '页面不存在'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else {
|
||||
return { top: 0 }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 导航守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 设置页面标题
|
||||
document.title = to.meta.title ? `${to.meta.title} - XYZW 游戏管理系统` : 'XYZW 游戏管理系统'
|
||||
|
||||
// 检查是否需要Token
|
||||
if (to.meta.requiresToken && !tokenStore.hasTokens) {
|
||||
next('/tokens')
|
||||
} else if (to.name === 'TokenImport' && tokenStore.hasTokens && tokenStore.selectedToken) {
|
||||
// 如果已有token且已选择,重定向到控制台
|
||||
next('/dashboard')
|
||||
} else if (to.path === '/' && tokenStore.hasTokens) {
|
||||
// 首页重定向逻辑
|
||||
if (tokenStore.selectedToken) {
|
||||
next('/dashboard')
|
||||
} else {
|
||||
next('/tokens')
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
158
src/stores/auth.js
Normal file
158
src/stores/auth.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useLocalTokenStore } from './localTokenManager'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 状态
|
||||
const user = ref(null)
|
||||
const token = ref(localStorage.getItem('token') || null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const localTokenStore = useLocalTokenStore()
|
||||
|
||||
// 计算属性
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
const userInfo = computed(() => user.value)
|
||||
|
||||
// 登录 - 移除API调用,使用本地认证
|
||||
const login = async (credentials) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
// 模拟本地认证逻辑
|
||||
const mockUser = {
|
||||
id: 'local_user_' + Date.now(),
|
||||
username: credentials.username,
|
||||
email: credentials.email || `${credentials.username}@local.game`,
|
||||
avatar: '/icons/xiaoyugan.png',
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
const mockToken = 'local_token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
||||
|
||||
token.value = mockToken
|
||||
user.value = mockUser
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('token', token.value)
|
||||
localStorage.setItem('user', JSON.stringify(user.value))
|
||||
|
||||
// 同时保存到token管理器
|
||||
localTokenStore.setUserToken(mockToken)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
return { success: false, message: '本地认证失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 - 移除API调用,使用本地注册
|
||||
const register = async (userInfo) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
// 检查用户名是否已存在(简单的本地检查)
|
||||
const existingUsers = JSON.parse(localStorage.getItem('registeredUsers') || '[]')
|
||||
const userExists = existingUsers.some(u => u.username === userInfo.username)
|
||||
|
||||
if (userExists) {
|
||||
return { success: false, message: '用户名已存在' }
|
||||
}
|
||||
|
||||
// 保存新用户信息到本地
|
||||
const newUser = {
|
||||
...userInfo,
|
||||
id: 'user_' + Date.now(),
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
existingUsers.push(newUser)
|
||||
localStorage.setItem('registeredUsers', JSON.stringify(existingUsers))
|
||||
|
||||
return { success: true, message: '注册成功,请登录' }
|
||||
} catch (error) {
|
||||
console.error('注册错误:', error)
|
||||
return { success: false, message: '本地注册失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 登出
|
||||
const logout = () => {
|
||||
user.value = null
|
||||
token.value = null
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('gameRoles')
|
||||
|
||||
// 清除token管理器中的数据
|
||||
localTokenStore.clearUserToken()
|
||||
localTokenStore.clearAllGameTokens()
|
||||
}
|
||||
|
||||
// 获取用户信息 - 移除API调用,使用本地数据
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
if (!token.value) return false
|
||||
|
||||
// 从本地存储获取用户信息
|
||||
const savedUser = localStorage.getItem('user')
|
||||
if (savedUser) {
|
||||
try {
|
||||
user.value = JSON.parse(savedUser)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
logout()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化认证状态 - 移除API验证,使用本地验证
|
||||
const initAuth = async () => {
|
||||
const savedUser = localStorage.getItem('user')
|
||||
if (token.value && savedUser) {
|
||||
try {
|
||||
user.value = JSON.parse(savedUser)
|
||||
// 初始化token管理器
|
||||
localTokenStore.initTokenManager()
|
||||
} catch (error) {
|
||||
console.error('初始化认证失败:', error)
|
||||
logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
user,
|
||||
token,
|
||||
isLoading,
|
||||
|
||||
// 计算属性
|
||||
isAuthenticated,
|
||||
userInfo,
|
||||
|
||||
// 方法
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
fetchUserInfo,
|
||||
initAuth
|
||||
}
|
||||
})
|
||||
204
src/stores/gameRoles.js
Normal file
204
src/stores/gameRoles.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useLocalTokenStore } from './localTokenManager'
|
||||
|
||||
export const useGameRolesStore = defineStore('gameRoles', () => {
|
||||
// 状态
|
||||
const gameRoles = ref([])
|
||||
const isLoading = ref(false)
|
||||
const selectedRole = ref(null)
|
||||
|
||||
const localTokenStore = useLocalTokenStore()
|
||||
|
||||
// 获取游戏角色列表 - 移除API调用,使用本地数据
|
||||
const fetchGameRoles = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
// 从本地存储获取角色数据
|
||||
const savedRoles = localStorage.getItem('gameRoles')
|
||||
if (savedRoles) {
|
||||
try {
|
||||
gameRoles.value = JSON.parse(savedRoles)
|
||||
} catch (error) {
|
||||
console.error('解析游戏角色数据失败:', error)
|
||||
gameRoles.value = []
|
||||
}
|
||||
} else {
|
||||
gameRoles.value = []
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('获取游戏角色失败:', error)
|
||||
return { success: false, message: '本地数据读取失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加游戏角色 - 移除API调用,本地生成角色和token
|
||||
const addGameRole = async (roleData) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
// 生成角色ID和游戏token
|
||||
const roleId = 'role_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
||||
const gameToken = 'game_token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 16)
|
||||
|
||||
const newRole = {
|
||||
...roleData,
|
||||
id: roleId,
|
||||
createdAt: new Date().toISOString(),
|
||||
isActive: false,
|
||||
exp: 0,
|
||||
gold: 1000, // 默认金币
|
||||
vip: false,
|
||||
avatar: roleData.avatar || '/icons/xiaoyugan.png'
|
||||
}
|
||||
|
||||
// 添加到角色列表
|
||||
gameRoles.value.push(newRole)
|
||||
localStorage.setItem('gameRoles', JSON.stringify(gameRoles.value))
|
||||
|
||||
// 生成并保存游戏token
|
||||
const tokenData = {
|
||||
token: gameToken,
|
||||
roleId: roleId,
|
||||
roleName: newRole.name,
|
||||
server: newRole.server,
|
||||
wsUrl: null, // 使用默认的游戏WebSocket地址
|
||||
createdAt: new Date().toISOString(),
|
||||
isActive: true
|
||||
}
|
||||
|
||||
localTokenStore.addGameToken(roleId, tokenData)
|
||||
|
||||
return { success: true, message: '添加角色成功,已生成游戏token' }
|
||||
} catch (error) {
|
||||
console.error('添加游戏角色失败:', error)
|
||||
return { success: false, message: '添加角色失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新游戏角色 - 移除API调用,使用本地更新
|
||||
const updateGameRole = async (roleId, roleData) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const index = gameRoles.value.findIndex(role => role.id === roleId)
|
||||
if (index !== -1) {
|
||||
gameRoles.value[index] = {
|
||||
...gameRoles.value[index],
|
||||
...roleData,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
localStorage.setItem('gameRoles', JSON.stringify(gameRoles.value))
|
||||
|
||||
// 更新对应的token信息
|
||||
const existingToken = localTokenStore.getGameToken(roleId)
|
||||
if (existingToken) {
|
||||
localTokenStore.updateGameToken(roleId, {
|
||||
roleName: roleData.name || existingToken.roleName,
|
||||
server: roleData.server || existingToken.server
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true, message: '更新角色成功' }
|
||||
} else {
|
||||
return { success: false, message: '角色不存在' }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新游戏角色失败:', error)
|
||||
return { success: false, message: '更新角色失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除游戏角色 - 移除API调用,同时删除对应token
|
||||
const deleteGameRole = async (roleId) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
gameRoles.value = gameRoles.value.filter(role => role.id !== roleId)
|
||||
localStorage.setItem('gameRoles', JSON.stringify(gameRoles.value))
|
||||
|
||||
// 删除对应的token和WebSocket连接
|
||||
localTokenStore.removeGameToken(roleId)
|
||||
|
||||
// 如果删除的是当前选中角色,清除选中状态
|
||||
if (selectedRole.value && selectedRole.value.id === roleId) {
|
||||
selectedRole.value = null
|
||||
localStorage.removeItem('selectedRole')
|
||||
}
|
||||
|
||||
return { success: true, message: '删除角色成功,已清理相关token' }
|
||||
} catch (error) {
|
||||
console.error('删除游戏角色失败:', error)
|
||||
return { success: false, message: '删除角色失败' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择角色 - 添加WebSocket连接功能
|
||||
const selectRole = (role) => {
|
||||
selectedRole.value = role
|
||||
localStorage.setItem('selectedRole', JSON.stringify(role))
|
||||
|
||||
// 自动建立WebSocket连接
|
||||
const tokenData = localTokenStore.getGameToken(role.id)
|
||||
if (tokenData && tokenData.token) {
|
||||
try {
|
||||
localTokenStore.createWebSocketConnection(
|
||||
role.id,
|
||||
tokenData.token,
|
||||
tokenData.wsUrl
|
||||
)
|
||||
console.log(`已为角色 ${role.name} 建立WebSocket连接`)
|
||||
} catch (error) {
|
||||
console.error(`建立WebSocket连接失败 [${role.name}]:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
const initGameRoles = () => {
|
||||
const cachedRoles = localStorage.getItem('gameRoles')
|
||||
const cachedSelectedRole = localStorage.getItem('selectedRole')
|
||||
|
||||
if (cachedRoles) {
|
||||
try {
|
||||
gameRoles.value = JSON.parse(cachedRoles)
|
||||
} catch (error) {
|
||||
console.error('解析缓存的游戏角色数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (cachedSelectedRole) {
|
||||
try {
|
||||
selectedRole.value = JSON.parse(cachedSelectedRole)
|
||||
} catch (error) {
|
||||
console.error('解析缓存的选中角色数据失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
gameRoles,
|
||||
isLoading,
|
||||
selectedRole,
|
||||
|
||||
// 方法
|
||||
fetchGameRoles,
|
||||
addGameRole,
|
||||
updateGameRole,
|
||||
deleteGameRole,
|
||||
selectRole,
|
||||
initGameRoles
|
||||
}
|
||||
})
|
||||
454
src/stores/localTokenManager.js
Normal file
454
src/stores/localTokenManager.js
Normal file
@@ -0,0 +1,454 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
/**
|
||||
* 本地Token管理器
|
||||
* 用于管理用户认证token和游戏角色token的本地存储
|
||||
*/
|
||||
export const useLocalTokenStore = defineStore('localToken', () => {
|
||||
// 状态
|
||||
const userToken = ref(localStorage.getItem('userToken') || null)
|
||||
const gameTokens = ref(JSON.parse(localStorage.getItem('gameTokens') || '{}'))
|
||||
const wsConnections = ref({}) // WebSocket连接状态
|
||||
|
||||
// 计算属性
|
||||
const isUserAuthenticated = computed(() => !!userToken.value)
|
||||
const hasGameTokens = computed(() => Object.keys(gameTokens.value).length > 0)
|
||||
|
||||
// 用户认证token管理
|
||||
const setUserToken = (token) => {
|
||||
userToken.value = token
|
||||
localStorage.setItem('userToken', token)
|
||||
}
|
||||
|
||||
const clearUserToken = () => {
|
||||
userToken.value = null
|
||||
localStorage.removeItem('userToken')
|
||||
}
|
||||
|
||||
// 游戏token管理
|
||||
const addGameToken = (roleId, tokenData) => {
|
||||
const newTokenData = {
|
||||
...tokenData,
|
||||
roleId,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsed: new Date().toISOString()
|
||||
}
|
||||
|
||||
gameTokens.value[roleId] = newTokenData
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
|
||||
return newTokenData
|
||||
}
|
||||
|
||||
const getGameToken = (roleId) => {
|
||||
const token = gameTokens.value[roleId]
|
||||
if (token) {
|
||||
// 更新最后使用时间
|
||||
token.lastUsed = new Date().toISOString()
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
const updateGameToken = (roleId, updates) => {
|
||||
if (gameTokens.value[roleId]) {
|
||||
gameTokens.value[roleId] = {
|
||||
...gameTokens.value[roleId],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
}
|
||||
}
|
||||
|
||||
const removeGameToken = (roleId) => {
|
||||
delete gameTokens.value[roleId]
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
|
||||
// 同时断开对应的WebSocket连接
|
||||
if (wsConnections.value[roleId]) {
|
||||
closeWebSocketConnection(roleId)
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllGameTokens = () => {
|
||||
// 关闭所有WebSocket连接
|
||||
Object.keys(wsConnections.value).forEach(roleId => {
|
||||
closeWebSocketConnection(roleId)
|
||||
})
|
||||
|
||||
gameTokens.value = {}
|
||||
localStorage.removeItem('gameTokens')
|
||||
}
|
||||
|
||||
// WebSocket连接管理 - 使用新的WsAgent
|
||||
const createWebSocketConnection = async (roleId, base64Token, customWsUrl = null) => {
|
||||
if (wsConnections.value[roleId]) {
|
||||
closeWebSocketConnection(roleId)
|
||||
}
|
||||
|
||||
try {
|
||||
// 动态导入WebSocket客户端
|
||||
const { WsAgent } = await import('../utils/wsAgent.js')
|
||||
const { gameCommands } = await import('../utils/gameCommands.js')
|
||||
|
||||
// 解析Base64获取实际Token
|
||||
let actualToken = base64Token
|
||||
|
||||
// 尝试解析Base64获取实际token
|
||||
try {
|
||||
const cleanBase64 = base64Token.replace(/^data:.*base64,/, '').trim()
|
||||
const decoded = atob(cleanBase64)
|
||||
|
||||
// 尝试解析为JSON获取token字段
|
||||
try {
|
||||
const tokenData = JSON.parse(decoded)
|
||||
actualToken = tokenData.token || tokenData.gameToken || decoded
|
||||
} catch {
|
||||
// 如果不是JSON,直接使用解码后的字符串
|
||||
actualToken = decoded
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Base64解析失败,使用原始token:', error.message)
|
||||
actualToken = base64Token
|
||||
}
|
||||
|
||||
// 创建WebSocket客户端实例
|
||||
const wsAgent = new WsAgent({
|
||||
heartbeatInterval: 2000,
|
||||
queueInterval: 50,
|
||||
channel: 'x', // 使用x通道
|
||||
autoReconnect: true,
|
||||
maxReconnectAttempts: 5
|
||||
})
|
||||
|
||||
// 设置事件监听器
|
||||
wsAgent.onOpen = () => {
|
||||
console.log(`✅ WebSocket连接已建立: ${roleId}`)
|
||||
|
||||
// 更新连接状态
|
||||
wsConnections.value[roleId].status = 'connected'
|
||||
wsConnections.value[roleId].connectedAt = new Date().toISOString()
|
||||
|
||||
// 发送初始化命令
|
||||
setTimeout(() => {
|
||||
// 获取角色信息
|
||||
wsAgent.send(gameCommands.role_getroleinfo(0, 0, { roleId }))
|
||||
|
||||
// 获取数据包版本
|
||||
wsAgent.send(gameCommands.system_getdatabundlever())
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
wsAgent.onMessage = (message) => {
|
||||
console.log(`📨 收到消息 [${roleId}]:`, message)
|
||||
|
||||
// 处理不同类型的消息
|
||||
if (message.cmd) {
|
||||
handleGameMessage(roleId, message)
|
||||
}
|
||||
}
|
||||
|
||||
wsAgent.onError = (error) => {
|
||||
console.error(`❌ WebSocket错误 [${roleId}]:`, error)
|
||||
if (wsConnections.value[roleId]) {
|
||||
wsConnections.value[roleId].status = 'error'
|
||||
wsConnections.value[roleId].lastError = error.message
|
||||
}
|
||||
}
|
||||
|
||||
wsAgent.onClose = (event) => {
|
||||
console.log(`🔌 WebSocket连接已关闭 [${roleId}]:`, event.code, event.reason)
|
||||
if (wsConnections.value[roleId]) {
|
||||
wsConnections.value[roleId].status = 'disconnected'
|
||||
}
|
||||
}
|
||||
|
||||
wsAgent.onReconnect = (attempt) => {
|
||||
console.log(`🔄 WebSocket重连中 [${roleId}] 第${attempt}次`)
|
||||
if (wsConnections.value[roleId]) {
|
||||
wsConnections.value[roleId].status = 'reconnecting'
|
||||
wsConnections.value[roleId].reconnectAttempt = attempt
|
||||
}
|
||||
}
|
||||
|
||||
// 构建WebSocket URL
|
||||
const baseWsUrl = 'wss://xxz-xyzw.hortorgames.com/agent'
|
||||
const wsUrl = customWsUrl || WsAgent.buildUrl(baseWsUrl, {
|
||||
p: actualToken,
|
||||
e: 'x',
|
||||
lang: 'chinese'
|
||||
})
|
||||
|
||||
// 保存连接信息
|
||||
wsConnections.value[roleId] = {
|
||||
agent: wsAgent,
|
||||
gameCommands,
|
||||
status: 'connecting',
|
||||
roleId,
|
||||
wsUrl,
|
||||
actualToken,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastError: null,
|
||||
reconnectAttempt: 0
|
||||
}
|
||||
|
||||
// 建立连接
|
||||
await wsAgent.connect(wsUrl)
|
||||
|
||||
return wsAgent
|
||||
} catch (error) {
|
||||
console.error(`创建WebSocket连接失败 [${roleId}]:`, error)
|
||||
if (wsConnections.value[roleId]) {
|
||||
wsConnections.value[roleId].status = 'error'
|
||||
wsConnections.value[roleId].lastError = error.message
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理游戏消息
|
||||
const handleGameMessage = (roleId, message) => {
|
||||
const { cmd, body } = message
|
||||
|
||||
switch (cmd) {
|
||||
case 'role_getroleinfo':
|
||||
console.log(`角色信息 [${roleId}]:`, body)
|
||||
break
|
||||
|
||||
case 'system_getdatabundlever':
|
||||
console.log(`数据包版本 [${roleId}]:`, body)
|
||||
break
|
||||
|
||||
case 'task_claimdailyreward':
|
||||
console.log(`每日任务奖励 [${roleId}]:`, body)
|
||||
break
|
||||
|
||||
case 'system_signinreward':
|
||||
console.log(`签到奖励 [${roleId}]:`, body)
|
||||
break
|
||||
|
||||
default:
|
||||
console.log(`未处理的消息 [${roleId}] ${cmd}:`, body)
|
||||
}
|
||||
}
|
||||
|
||||
const closeWebSocketConnection = (roleId) => {
|
||||
const connection = wsConnections.value[roleId]
|
||||
if (connection) {
|
||||
// 如果是新的WsAgent实例
|
||||
if (connection.agent && typeof connection.agent.close === 'function') {
|
||||
connection.agent.close()
|
||||
}
|
||||
// 如果是旧的WebSocket实例
|
||||
else if (connection.connection && typeof connection.connection.close === 'function') {
|
||||
connection.connection.close()
|
||||
}
|
||||
|
||||
delete wsConnections.value[roleId]
|
||||
}
|
||||
}
|
||||
|
||||
const getWebSocketStatus = (roleId) => {
|
||||
return wsConnections.value[roleId]?.status || 'disconnected'
|
||||
}
|
||||
|
||||
// 发送游戏命令
|
||||
const sendGameCommand = (roleId, commandName, params = {}) => {
|
||||
const connection = wsConnections.value[roleId]
|
||||
if (!connection || !connection.agent) {
|
||||
console.warn(`角色 ${roleId} 的WebSocket连接不存在`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (connection.status !== 'connected') {
|
||||
console.warn(`角色 ${roleId} 的WebSocket未连接`)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const { gameCommands } = connection
|
||||
|
||||
if (typeof gameCommands[commandName] === 'function') {
|
||||
const command = gameCommands[commandName](0, 0, params)
|
||||
connection.agent.send(command)
|
||||
console.log(`发送游戏命令 [${roleId}] ${commandName}:`, params)
|
||||
return true
|
||||
} else {
|
||||
console.error(`未知的游戏命令: ${commandName}`)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`发送游戏命令失败 [${roleId}] ${commandName}:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 发送游戏命令并等待响应
|
||||
const sendGameCommandWithPromise = async (roleId, commandName, params = {}, timeout = 8000) => {
|
||||
const connection = wsConnections.value[roleId]
|
||||
if (!connection || !connection.agent) {
|
||||
throw new Error(`角色 ${roleId} 的WebSocket连接不存在`)
|
||||
}
|
||||
|
||||
if (connection.status !== 'connected') {
|
||||
throw new Error(`角色 ${roleId} 的WebSocket未连接`)
|
||||
}
|
||||
|
||||
try {
|
||||
const { gameCommands } = connection
|
||||
|
||||
if (typeof gameCommands[commandName] === 'function') {
|
||||
const response = await connection.agent.sendWithPromise({
|
||||
cmd: commandName,
|
||||
body: params,
|
||||
timeout
|
||||
})
|
||||
console.log(`游戏命令响应 [${roleId}] ${commandName}:`, response)
|
||||
return response
|
||||
} else {
|
||||
throw new Error(`未知的游戏命令: ${commandName}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`发送游戏命令失败 [${roleId}] ${commandName}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取连接详细状态
|
||||
const getWebSocketDetails = (roleId) => {
|
||||
const connection = wsConnections.value[roleId]
|
||||
if (!connection) {
|
||||
return {
|
||||
status: 'disconnected',
|
||||
roleId,
|
||||
error: '连接不存在'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: connection.status,
|
||||
roleId: connection.roleId,
|
||||
wsUrl: connection.wsUrl,
|
||||
connectedAt: connection.connectedAt,
|
||||
createdAt: connection.createdAt,
|
||||
lastError: connection.lastError,
|
||||
reconnectAttempt: connection.reconnectAttempt,
|
||||
agentStatus: connection.agent ? connection.agent.getStatus() : null
|
||||
}
|
||||
}
|
||||
|
||||
// 批量导入/导出功能
|
||||
const exportTokens = () => {
|
||||
return {
|
||||
userToken: userToken.value,
|
||||
gameTokens: gameTokens.value,
|
||||
exportedAt: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const importTokens = (tokenData) => {
|
||||
try {
|
||||
if (tokenData.userToken) {
|
||||
setUserToken(tokenData.userToken)
|
||||
}
|
||||
|
||||
if (tokenData.gameTokens) {
|
||||
gameTokens.value = tokenData.gameTokens
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
}
|
||||
|
||||
return { success: true, message: 'Token导入成功' }
|
||||
} catch (error) {
|
||||
console.error('Token导入失败:', error)
|
||||
return { success: false, message: '导入失败:数据格式错误' }
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期token
|
||||
const cleanExpiredTokens = () => {
|
||||
const now = new Date()
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
const cleanedTokens = {}
|
||||
let cleanedCount = 0
|
||||
|
||||
Object.entries(gameTokens.value).forEach(([roleId, tokenData]) => {
|
||||
const lastUsed = new Date(tokenData.lastUsed || tokenData.createdAt)
|
||||
if (lastUsed > oneDayAgo) {
|
||||
cleanedTokens[roleId] = tokenData
|
||||
} else {
|
||||
cleanedCount++
|
||||
// 关闭对应的WebSocket连接
|
||||
if (wsConnections.value[roleId]) {
|
||||
closeWebSocketConnection(roleId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
gameTokens.value = cleanedTokens
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
|
||||
return cleanedCount
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const initTokenManager = () => {
|
||||
// 从localStorage恢复数据
|
||||
const savedUserToken = localStorage.getItem('userToken')
|
||||
const savedGameTokens = localStorage.getItem('gameTokens')
|
||||
|
||||
if (savedUserToken) {
|
||||
userToken.value = savedUserToken
|
||||
}
|
||||
|
||||
if (savedGameTokens) {
|
||||
try {
|
||||
gameTokens.value = JSON.parse(savedGameTokens)
|
||||
} catch (error) {
|
||||
console.error('解析游戏token数据失败:', error)
|
||||
gameTokens.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期token
|
||||
cleanExpiredTokens()
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
userToken,
|
||||
gameTokens,
|
||||
wsConnections,
|
||||
|
||||
// 计算属性
|
||||
isUserAuthenticated,
|
||||
hasGameTokens,
|
||||
|
||||
// 用户token方法
|
||||
setUserToken,
|
||||
clearUserToken,
|
||||
|
||||
// 游戏token方法
|
||||
addGameToken,
|
||||
getGameToken,
|
||||
updateGameToken,
|
||||
removeGameToken,
|
||||
clearAllGameTokens,
|
||||
|
||||
// WebSocket方法
|
||||
createWebSocketConnection,
|
||||
closeWebSocketConnection,
|
||||
getWebSocketStatus,
|
||||
getWebSocketDetails,
|
||||
sendGameCommand,
|
||||
sendGameCommandWithPromise,
|
||||
|
||||
// 工具方法
|
||||
exportTokens,
|
||||
importTokens,
|
||||
cleanExpiredTokens,
|
||||
initTokenManager
|
||||
}
|
||||
})
|
||||
792
src/stores/tokenStore.js
Normal file
792
src/stores/tokenStore.js
Normal file
@@ -0,0 +1,792 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { bonProtocol, GameMessages, g_utils } from '../utils/bonProtocol.js'
|
||||
import { XyzwWebSocketClient } from '../utils/xyzwWebSocket.js'
|
||||
|
||||
/**
|
||||
* 重构后的Token管理存储
|
||||
* 以名称-token列表形式管理多个游戏角色
|
||||
*/
|
||||
export const useTokenStore = defineStore('tokens', () => {
|
||||
// 状态
|
||||
const gameTokens = ref(JSON.parse(localStorage.getItem('gameTokens') || '[]'))
|
||||
const selectedTokenId = ref(localStorage.getItem('selectedTokenId') || null)
|
||||
const wsConnections = ref({}) // WebSocket连接状态
|
||||
|
||||
// 游戏数据存储
|
||||
const gameData = ref({
|
||||
roleInfo: null,
|
||||
legionInfo: null,
|
||||
presetTeam: null,
|
||||
lastUpdated: null
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const hasTokens = computed(() => gameTokens.value.length > 0)
|
||||
const selectedToken = computed(() =>
|
||||
gameTokens.value.find(token => token.id === selectedTokenId.value)
|
||||
)
|
||||
|
||||
// Token管理
|
||||
const addToken = (tokenData) => {
|
||||
const newToken = {
|
||||
id: 'token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
||||
name: tokenData.name,
|
||||
token: tokenData.token, // 保存原始Base64 token
|
||||
wsUrl: tokenData.wsUrl || null, // 可选的自定义WebSocket URL
|
||||
server: tokenData.server || '',
|
||||
level: tokenData.level || 1,
|
||||
profession: tokenData.profession || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsed: new Date().toISOString(),
|
||||
isActive: true
|
||||
}
|
||||
|
||||
gameTokens.value.push(newToken)
|
||||
saveTokensToStorage()
|
||||
|
||||
return newToken
|
||||
}
|
||||
|
||||
const updateToken = (tokenId, updates) => {
|
||||
const index = gameTokens.value.findIndex(token => token.id === tokenId)
|
||||
if (index !== -1) {
|
||||
gameTokens.value[index] = {
|
||||
...gameTokens.value[index],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
saveTokensToStorage()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const removeToken = (tokenId) => {
|
||||
gameTokens.value = gameTokens.value.filter(token => token.id !== tokenId)
|
||||
saveTokensToStorage()
|
||||
|
||||
// 关闭对应的WebSocket连接
|
||||
if (wsConnections.value[tokenId]) {
|
||||
closeWebSocketConnection(tokenId)
|
||||
}
|
||||
|
||||
// 如果删除的是当前选中token,清除选中状态
|
||||
if (selectedTokenId.value === tokenId) {
|
||||
selectedTokenId.value = null
|
||||
localStorage.removeItem('selectedTokenId')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const selectToken = (tokenId) => {
|
||||
const token = gameTokens.value.find(t => t.id === tokenId)
|
||||
if (token) {
|
||||
selectedTokenId.value = tokenId
|
||||
localStorage.setItem('selectedTokenId', tokenId)
|
||||
|
||||
// 更新最后使用时间
|
||||
updateToken(tokenId, { lastUsed: new Date().toISOString() })
|
||||
|
||||
// 自动建立WebSocket连接
|
||||
createWebSocketConnection(tokenId, token.token, token.wsUrl)
|
||||
|
||||
return token
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 辅助函数:分析数据结构
|
||||
const analyzeDataStructure = (obj, depth = 0, maxDepth = 3) => {
|
||||
if (depth > maxDepth || !obj || typeof obj !== 'object') {
|
||||
return typeof obj
|
||||
}
|
||||
|
||||
const structure = {}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (Array.isArray(value)) {
|
||||
structure[key] = `Array[${value.length}]${value.length > 0 ? `: ${analyzeDataStructure(value[0], depth + 1, maxDepth)}` : ''}`
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
structure[key] = analyzeDataStructure(value, depth + 1, maxDepth)
|
||||
} else {
|
||||
structure[key] = typeof value
|
||||
}
|
||||
}
|
||||
return structure
|
||||
}
|
||||
|
||||
// 辅助函数:尝试解析队伍数据
|
||||
const tryParseTeamData = (data, cmd) => {
|
||||
console.log(`👥 尝试解析队伍数据 [${cmd}]:`, data)
|
||||
|
||||
// 查找队伍相关字段
|
||||
const teamFields = []
|
||||
const scanForTeamData = (obj, path = '') => {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const currentPath = path ? `${path}.${key}` : key
|
||||
|
||||
if (key.toLowerCase().includes('team') ||
|
||||
key.toLowerCase().includes('preset') ||
|
||||
key.toLowerCase().includes('formation') ||
|
||||
key.toLowerCase().includes('lineup')) {
|
||||
teamFields.push({
|
||||
path: currentPath,
|
||||
key: key,
|
||||
value: value,
|
||||
type: typeof value,
|
||||
isArray: Array.isArray(value)
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
scanForTeamData(value, currentPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanForTeamData(data)
|
||||
|
||||
if (teamFields.length > 0) {
|
||||
console.log(`👥 找到 ${teamFields.length} 个队伍相关字段:`, teamFields)
|
||||
|
||||
// 尝试更新游戏数据
|
||||
teamFields.forEach(field => {
|
||||
if (field.key === 'presetTeamInfo' || field.path.includes('presetTeamInfo')) {
|
||||
console.log(`👥 发现预设队伍信息,准备更新:`, field.value)
|
||||
if (!gameData.value.presetTeam) {
|
||||
gameData.value.presetTeam = {}
|
||||
}
|
||||
gameData.value.presetTeam.presetTeamInfo = field.value
|
||||
gameData.value.lastUpdated = new Date().toISOString()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log(`👥 未找到明显的队伍字段,完整数据结构:`, analyzeDataStructure(data))
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏消息处理
|
||||
const handleGameMessage = (tokenId, message) => {
|
||||
try {
|
||||
if (!message || message.error) {
|
||||
console.warn(`⚠️ 消息处理跳过 [${tokenId}]:`, message?.error || '无效消息')
|
||||
return
|
||||
}
|
||||
|
||||
const cmd = message.cmd?.toLowerCase()
|
||||
// 优先使用rawData(ProtoMsg自动解码),然后decodedBody(手动解码),最后body(原始数据)
|
||||
const body = message.rawData !== undefined ? message.rawData :
|
||||
message.decodedBody !== undefined ? message.decodedBody :
|
||||
message.body
|
||||
|
||||
console.log(`📋 处理消息 [${tokenId}] ${cmd}:`, {
|
||||
hasRawData: message.rawData !== undefined,
|
||||
hasDecodedBody: message.decodedBody !== undefined,
|
||||
hasBody: message.body !== undefined,
|
||||
bodyType: body ? typeof body : 'undefined',
|
||||
bodyContent: body,
|
||||
originalCmd: message.cmd,
|
||||
fullMessage: message
|
||||
})
|
||||
|
||||
// 记录所有消息的原始命令名
|
||||
console.log(`📨 收到消息 [${tokenId}] 原始cmd: "${message.cmd}", 处理cmd: "${cmd}"`)
|
||||
|
||||
// 特别记录所有包含tower的消息
|
||||
if (cmd && cmd.includes('tower')) {
|
||||
console.log(`🗼 发现塔相关消息 [${tokenId}] ${cmd}:`, message)
|
||||
}
|
||||
|
||||
// 处理角色信息 - 支持多种可能的响应命令
|
||||
if (cmd === 'role_getroleinfo' || cmd === 'role_getroleinforesp' || cmd.includes('role') && cmd.includes('info')) {
|
||||
console.log(`📊 匹配到角色信息命令: ${cmd}`)
|
||||
|
||||
if (body) {
|
||||
gameData.value.roleInfo = body
|
||||
gameData.value.lastUpdated = new Date().toISOString()
|
||||
console.log('📊 角色信息已更新:', body)
|
||||
console.log('📊 角色信息类型:', typeof body)
|
||||
console.log('📊 角色信息内容概览:', Object.keys(body || {}))
|
||||
|
||||
// 特别检查塔信息
|
||||
if (body.role?.tower) {
|
||||
console.log('🗼 在角色信息中找到塔信息:', body.role.tower)
|
||||
} else if (body.tower) {
|
||||
console.log('🗼 在响应根级别找到塔信息:', body.tower)
|
||||
} else {
|
||||
console.log('🗼 未找到塔信息在角色数据中')
|
||||
console.log('📊 角色数据结构:', body.role ? Object.keys(body.role) : '没有role对象')
|
||||
}
|
||||
} else {
|
||||
console.log('📊 角色信息响应body为空')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理军团信息
|
||||
else if (cmd === 'legion_getinfo') {
|
||||
if (body) {
|
||||
gameData.value.legionInfo = body
|
||||
console.log('🏛️ 军团信息已更新:', body)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理队伍信息 - 支持多种队伍相关响应
|
||||
else if (cmd === 'presetteam_getteam' || cmd === 'presetteam_getteamresp' ||
|
||||
cmd === 'presetteam_setteam' || cmd === 'presetteam_setteamresp' ||
|
||||
cmd === 'presetteam_saveteam' || cmd === 'presetteam_saveteamresp' ||
|
||||
cmd === 'role_gettargetteam' || cmd === 'role_gettargetteamresp' ||
|
||||
(cmd && cmd.includes('presetteam')) || (cmd && cmd.includes('team'))) {
|
||||
console.log(`👥 匹配到队伍信息命令: ${cmd}`)
|
||||
|
||||
if (body) {
|
||||
// 更新队伍数据
|
||||
if (!gameData.value.presetTeam) {
|
||||
gameData.value.presetTeam = {}
|
||||
}
|
||||
|
||||
// 根据不同的响应类型处理数据
|
||||
if (cmd.includes('getteam')) {
|
||||
// 获取队伍信息响应
|
||||
gameData.value.presetTeam = { ...gameData.value.presetTeam, ...body }
|
||||
} else if (cmd.includes('setteam') || cmd.includes('saveteam')) {
|
||||
// 设置/保存队伍响应 - 可能只返回确认信息
|
||||
if (body.presetTeamInfo) {
|
||||
gameData.value.presetTeam.presetTeamInfo = body.presetTeamInfo
|
||||
}
|
||||
// 合并其他队伍相关数据
|
||||
Object.keys(body).forEach(key => {
|
||||
if (key.includes('team') || key.includes('Team')) {
|
||||
gameData.value.presetTeam[key] = body[key]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 其他队伍相关响应
|
||||
gameData.value.presetTeam = { ...gameData.value.presetTeam, ...body }
|
||||
}
|
||||
|
||||
gameData.value.lastUpdated = new Date().toISOString()
|
||||
console.log('👥 队伍信息已更新:', {
|
||||
cmd: cmd,
|
||||
updatedData: gameData.value.presetTeam,
|
||||
bodyKeys: Object.keys(body),
|
||||
bodyContent: body
|
||||
})
|
||||
|
||||
// 详细日志队伍数据结构
|
||||
if (gameData.value.presetTeam.presetTeamInfo) {
|
||||
console.log('👥 队伍详细结构:', {
|
||||
teamCount: Object.keys(gameData.value.presetTeam.presetTeamInfo).length,
|
||||
teamIds: Object.keys(gameData.value.presetTeam.presetTeamInfo),
|
||||
useTeamId: gameData.value.presetTeam.presetTeamInfo.useTeamId,
|
||||
sampleTeam: gameData.value.presetTeam.presetTeamInfo[1] || gameData.value.presetTeam.presetTeamInfo[Object.keys(gameData.value.presetTeam.presetTeamInfo)[0]]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.log('👥 队伍信息响应body为空')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理爬塔响应
|
||||
else if (cmd === 'fight_starttower' || cmd === 'fight_starttowerresp') {
|
||||
if (body) {
|
||||
console.log('🗼 爬塔响应:', body)
|
||||
// 爬塔后立即更新角色信息和塔信息
|
||||
setTimeout(() => {
|
||||
console.log('🗼 爬塔后自动更新数据')
|
||||
try {
|
||||
const connection = wsConnections.value[tokenId]
|
||||
if (connection && connection.status === 'connected' && connection.client) {
|
||||
// 获取最新角色信息
|
||||
console.log('🗼 正在请求角色信息...')
|
||||
connection.client.send('role_getroleinfo', {})
|
||||
} else {
|
||||
console.warn('🗼 WebSocket未连接,无法更新数据')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('爬塔后更新数据失败:', error)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理心跳响应
|
||||
else if (cmd === '_sys/ack') {
|
||||
console.log(`💗 心跳响应 [${tokenId}]`)
|
||||
}
|
||||
|
||||
// 处理其他消息
|
||||
else {
|
||||
console.log(`📋 收到游戏消息 [${tokenId}] ${cmd}:`, body)
|
||||
|
||||
// 特别关注队伍相关的未处理消息
|
||||
if (cmd && (cmd.includes('team') || cmd.includes('preset') || cmd.includes('formation'))) {
|
||||
console.log(`👥 未处理的队伍相关消息 [${tokenId}] ${cmd}:`, {
|
||||
originalMessage: message,
|
||||
parsedBody: body,
|
||||
messageKeys: Object.keys(message || {}),
|
||||
bodyStructure: body ? analyzeDataStructure(body) : null
|
||||
})
|
||||
|
||||
// 尝试自动解析队伍数据
|
||||
if (body && typeof body === 'object') {
|
||||
tryParseTeamData(body, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// 特别关注塔相关的未处理消息
|
||||
if (cmd && cmd.includes('tower')) {
|
||||
console.log(`🗼 未处理的塔相关消息 [${tokenId}] ${cmd}:`, {
|
||||
originalMessage: message,
|
||||
parsedBody: body,
|
||||
messageKeys: Object.keys(message || {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理游戏消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Base64解析功能
|
||||
const parseBase64Token = (base64String) => {
|
||||
try {
|
||||
// 移除可能的前缀和空格
|
||||
const cleanBase64 = base64String.replace(/^data:.*base64,/, '').trim()
|
||||
|
||||
// 解码base64
|
||||
const decoded = atob(cleanBase64)
|
||||
|
||||
// 尝试解析为JSON
|
||||
let tokenData
|
||||
try {
|
||||
tokenData = JSON.parse(decoded)
|
||||
} catch {
|
||||
// 如果不是JSON,当作纯token字符串处理
|
||||
tokenData = { token: decoded }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: tokenData
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: '解析失败:' + error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const importBase64Token = (name, base64String, additionalInfo = {}) => {
|
||||
const parseResult = parseBase64Token(base64String)
|
||||
|
||||
if (!parseResult.success) {
|
||||
return parseResult
|
||||
}
|
||||
|
||||
const tokenData = {
|
||||
name,
|
||||
token: parseResult.data.token || parseResult.data.gameToken || base64String,
|
||||
...additionalInfo,
|
||||
...parseResult.data // 解析出的数据覆盖手动输入
|
||||
}
|
||||
|
||||
const newToken = addToken(tokenData)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: newToken,
|
||||
message: `Token "${name}" 导入成功`
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket连接管理
|
||||
const createWebSocketConnection = (tokenId, base64Token, customWsUrl = null) => {
|
||||
if (wsConnections.value[tokenId]) {
|
||||
closeWebSocketConnection(tokenId)
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析Base64获取实际Token
|
||||
let actualToken = base64Token
|
||||
|
||||
// 尝试解析Base64获取实际token
|
||||
try {
|
||||
const cleanBase64 = base64Token.replace(/^data:.*base64,/, '').trim()
|
||||
const decoded = atob(cleanBase64)
|
||||
|
||||
// 尝试解析为JSON获取token字段
|
||||
try {
|
||||
const tokenData = JSON.parse(decoded)
|
||||
actualToken = tokenData.token || tokenData.gameToken || decoded
|
||||
} catch {
|
||||
// 如果不是JSON,直接使用解码后的字符串
|
||||
actualToken = decoded
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Base64解析失败,使用原始token:', error.message)
|
||||
actualToken = base64Token
|
||||
}
|
||||
|
||||
// 使用固定的WebSocket基础地址,将token带入占位符
|
||||
const baseWsUrl = 'wss://xxz-xyzw.hortorgames.com/agent?p=%s&e=x&lang=chinese'
|
||||
const wsUrl = customWsUrl || baseWsUrl.replace('%s', encodeURIComponent(actualToken))
|
||||
|
||||
console.log(`🔗 创建WebSocket连接:`, wsUrl)
|
||||
console.log(`🎯 Token ID: ${tokenId}`)
|
||||
console.log(`🔑 使用Token: ${actualToken.substring(0, 20)}...`)
|
||||
|
||||
// 检查g_utils结构
|
||||
console.log('🔍 g_utils结构检查:', {
|
||||
hasGetEnc: !!g_utils.getEnc,
|
||||
hasEncode: !!g_utils.encode,
|
||||
hasParse: !!g_utils.parse,
|
||||
hasBon: !!g_utils.bon,
|
||||
bonHasDecode: !!(g_utils.bon && g_utils.bon.decode)
|
||||
})
|
||||
|
||||
// 创建新的WebSocket客户端
|
||||
const wsClient = new XyzwWebSocketClient({
|
||||
url: wsUrl,
|
||||
utils: g_utils,
|
||||
heartbeatMs: 5000 // 5秒心跳间隔
|
||||
})
|
||||
|
||||
// 设置连接状态
|
||||
wsConnections.value[tokenId] = {
|
||||
client: wsClient,
|
||||
status: 'connecting',
|
||||
tokenId,
|
||||
wsUrl,
|
||||
actualToken,
|
||||
connectedAt: null,
|
||||
lastMessage: null,
|
||||
lastError: null
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
wsClient.onConnect = () => {
|
||||
console.log(`✅ WebSocket连接已建立: ${tokenId}`)
|
||||
if (wsConnections.value[tokenId]) {
|
||||
wsConnections.value[tokenId].status = 'connected'
|
||||
wsConnections.value[tokenId].connectedAt = new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
wsClient.onDisconnect = (event) => {
|
||||
console.log(`🔌 WebSocket连接已断开: ${tokenId}`, event)
|
||||
if (wsConnections.value[tokenId]) {
|
||||
wsConnections.value[tokenId].status = 'disconnected'
|
||||
}
|
||||
}
|
||||
|
||||
wsClient.onError = (error) => {
|
||||
console.error(`❌ WebSocket错误 [${tokenId}]:`, error)
|
||||
if (wsConnections.value[tokenId]) {
|
||||
wsConnections.value[tokenId].status = 'error'
|
||||
wsConnections.value[tokenId].lastError = {
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error.toString(),
|
||||
url: wsUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置消息监听
|
||||
wsClient.setMessageListener((message) => {
|
||||
console.log(`📨 收到消息 [${tokenId}]:`, message)
|
||||
|
||||
// 更新连接状态中的最后接收消息
|
||||
if (wsConnections.value[tokenId]) {
|
||||
wsConnections.value[tokenId].lastMessage = {
|
||||
timestamp: new Date().toISOString(),
|
||||
data: message
|
||||
}
|
||||
}
|
||||
|
||||
// 处理游戏消息
|
||||
handleGameMessage(tokenId, message)
|
||||
})
|
||||
|
||||
// 开启调试模式
|
||||
wsClient.setShowMsg(true)
|
||||
|
||||
// 初始化连接
|
||||
wsClient.init()
|
||||
|
||||
return wsClient
|
||||
} catch (error) {
|
||||
console.error(`创建WebSocket连接失败 [${tokenId}]:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const closeWebSocketConnection = (tokenId) => {
|
||||
const connection = wsConnections.value[tokenId]
|
||||
if (connection && connection.client) {
|
||||
connection.client.disconnect()
|
||||
delete wsConnections.value[tokenId]
|
||||
}
|
||||
}
|
||||
|
||||
const getWebSocketStatus = (tokenId) => {
|
||||
return wsConnections.value[tokenId]?.status || 'disconnected'
|
||||
}
|
||||
|
||||
// 获取WebSocket客户端
|
||||
const getWebSocketClient = (tokenId) => {
|
||||
return wsConnections.value[tokenId]?.client || null
|
||||
}
|
||||
|
||||
|
||||
// 发送消息到WebSocket
|
||||
const sendMessage = (tokenId, cmd, params = {}, options = {}) => {
|
||||
const connection = wsConnections.value[tokenId]
|
||||
if (!connection || connection.status !== 'connected') {
|
||||
console.error(`❌ WebSocket未连接,无法发送消息 [${tokenId}]`)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const client = connection.client
|
||||
if (!client) {
|
||||
console.error(`❌ WebSocket客户端不存在 [${tokenId}]`)
|
||||
return false
|
||||
}
|
||||
|
||||
client.send(cmd, params, options)
|
||||
console.log(`📤 发送消息 [${tokenId}]: ${cmd}`, params)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`❌ 发送消息失败 [${tokenId}]:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Promise版发送消息
|
||||
const sendMessageWithPromise = async (tokenId, cmd, params = {}, timeout = 5000) => {
|
||||
const connection = wsConnections.value[tokenId]
|
||||
if (!connection || connection.status !== 'connected') {
|
||||
throw new Error(`WebSocket未连接 [${tokenId}]`)
|
||||
}
|
||||
|
||||
const client = connection.client
|
||||
if (!client) {
|
||||
throw new Error(`WebSocket客户端不存在 [${tokenId}]`)
|
||||
}
|
||||
|
||||
return await client.sendWithPromise(cmd, params, timeout)
|
||||
}
|
||||
|
||||
// 发送心跳消息
|
||||
const sendHeartbeat = (tokenId) => {
|
||||
return sendMessage(tokenId, 'heart_beat')
|
||||
}
|
||||
|
||||
// 发送获取角色信息请求
|
||||
const sendGetRoleInfo = (tokenId, params = {}) => {
|
||||
return sendMessageWithPromise(tokenId, 'role_getroleinfo', params)
|
||||
}
|
||||
|
||||
// 发送获取数据版本请求
|
||||
const sendGetDataBundleVersion = (tokenId, params = {}) => {
|
||||
return sendMessageWithPromise(tokenId, 'system_getdatabundlever', params)
|
||||
}
|
||||
|
||||
// 发送签到请求
|
||||
const sendSignIn = (tokenId) => {
|
||||
return sendMessageWithPromise(tokenId, 'system_signinreward')
|
||||
}
|
||||
|
||||
// 发送领取日常任务奖励
|
||||
const sendClaimDailyReward = (tokenId, rewardId = 0) => {
|
||||
return sendMessageWithPromise(tokenId, 'task_claimdailyreward', { rewardId })
|
||||
}
|
||||
|
||||
// 发送获取队伍信息
|
||||
const sendGetTeamInfo = (tokenId, params = {}) => {
|
||||
return sendMessageWithPromise(tokenId, 'presetteam_getteam', params)
|
||||
}
|
||||
|
||||
// 发送自定义游戏消息
|
||||
const sendGameMessage = (tokenId, cmd, params = {}, options = {}) => {
|
||||
if (options.usePromise) {
|
||||
return sendMessageWithPromise(tokenId, cmd, params, options.timeout)
|
||||
} else {
|
||||
return sendMessage(tokenId, cmd, params, options)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前塔层数
|
||||
const getCurrentTowerLevel = () => {
|
||||
try {
|
||||
// 从游戏数据中获取塔信息
|
||||
const roleInfo = gameData.value.roleInfo
|
||||
if (!roleInfo || !roleInfo.role) {
|
||||
console.warn('⚠️ 角色信息不存在')
|
||||
return null
|
||||
}
|
||||
|
||||
const tower = roleInfo.role.tower
|
||||
if (!tower) {
|
||||
console.warn('⚠️ 塔信息不存在')
|
||||
return null
|
||||
}
|
||||
|
||||
// 可能的塔层数字段(根据实际数据结构调整)
|
||||
const level = tower.level || tower.currentLevel || tower.floor || tower.stage
|
||||
|
||||
console.log('🗼 当前塔层数:', level, '塔信息:', tower)
|
||||
return level
|
||||
} catch (error) {
|
||||
console.error('❌ 获取塔层数失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取详细塔信息
|
||||
const getTowerInfo = () => {
|
||||
try {
|
||||
const roleInfo = gameData.value.roleInfo
|
||||
if (!roleInfo || !roleInfo.role) {
|
||||
return null
|
||||
}
|
||||
|
||||
return roleInfo.role.tower || null
|
||||
} catch (error) {
|
||||
console.error('❌ 获取塔信息失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
const exportTokens = () => {
|
||||
return {
|
||||
tokens: gameTokens.value,
|
||||
exportedAt: new Date().toISOString(),
|
||||
version: '2.0'
|
||||
}
|
||||
}
|
||||
|
||||
const importTokens = (data) => {
|
||||
try {
|
||||
if (data.tokens && Array.isArray(data.tokens)) {
|
||||
gameTokens.value = data.tokens
|
||||
saveTokensToStorage()
|
||||
return { success: true, message: `成功导入 ${data.tokens.length} 个Token` }
|
||||
} else {
|
||||
return { success: false, message: '导入数据格式错误' }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, message: '导入失败:' + error.message }
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllTokens = () => {
|
||||
// 关闭所有WebSocket连接
|
||||
Object.keys(wsConnections.value).forEach(tokenId => {
|
||||
closeWebSocketConnection(tokenId)
|
||||
})
|
||||
|
||||
gameTokens.value = []
|
||||
selectedTokenId.value = null
|
||||
localStorage.removeItem('gameTokens')
|
||||
localStorage.removeItem('selectedTokenId')
|
||||
}
|
||||
|
||||
const cleanExpiredTokens = () => {
|
||||
const now = new Date()
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
const cleanedTokens = gameTokens.value.filter(token => {
|
||||
const lastUsed = new Date(token.lastUsed || token.createdAt)
|
||||
return lastUsed > oneDayAgo
|
||||
})
|
||||
|
||||
const cleanedCount = gameTokens.value.length - cleanedTokens.length
|
||||
gameTokens.value = cleanedTokens
|
||||
saveTokensToStorage()
|
||||
|
||||
return cleanedCount
|
||||
}
|
||||
|
||||
const saveTokensToStorage = () => {
|
||||
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const initTokenStore = () => {
|
||||
// 恢复数据
|
||||
const savedTokens = localStorage.getItem('gameTokens')
|
||||
const savedSelectedId = localStorage.getItem('selectedTokenId')
|
||||
|
||||
if (savedTokens) {
|
||||
try {
|
||||
gameTokens.value = JSON.parse(savedTokens)
|
||||
} catch (error) {
|
||||
console.error('解析Token数据失败:', error)
|
||||
gameTokens.value = []
|
||||
}
|
||||
}
|
||||
|
||||
if (savedSelectedId) {
|
||||
selectedTokenId.value = savedSelectedId
|
||||
}
|
||||
|
||||
// 清理过期token
|
||||
cleanExpiredTokens()
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
gameTokens,
|
||||
selectedTokenId,
|
||||
wsConnections,
|
||||
gameData,
|
||||
|
||||
// 计算属性
|
||||
hasTokens,
|
||||
selectedToken,
|
||||
|
||||
// Token管理方法
|
||||
addToken,
|
||||
updateToken,
|
||||
removeToken,
|
||||
selectToken,
|
||||
|
||||
// Base64解析方法
|
||||
parseBase64Token,
|
||||
importBase64Token,
|
||||
|
||||
// WebSocket方法
|
||||
createWebSocketConnection,
|
||||
closeWebSocketConnection,
|
||||
getWebSocketStatus,
|
||||
getWebSocketClient,
|
||||
sendMessage,
|
||||
sendMessageWithPromise,
|
||||
sendHeartbeat,
|
||||
sendGetRoleInfo,
|
||||
sendGetDataBundleVersion,
|
||||
sendSignIn,
|
||||
sendClaimDailyReward,
|
||||
sendGetTeamInfo,
|
||||
sendGameMessage,
|
||||
|
||||
// 工具方法
|
||||
exportTokens,
|
||||
importTokens,
|
||||
clearAllTokens,
|
||||
cleanExpiredTokens,
|
||||
initTokenStore,
|
||||
|
||||
// 塔信息方法
|
||||
getCurrentTowerLevel,
|
||||
getTowerInfo
|
||||
}
|
||||
})
|
||||
779
src/utils/bonProtocol.js
Normal file
779
src/utils/bonProtocol.js
Normal file
@@ -0,0 +1,779 @@
|
||||
/**
|
||||
* BON (Binary Object Notation) 协议实现
|
||||
* 基于提供的真实 BON 源码重新实现
|
||||
*/
|
||||
import lz4 from 'lz4js';
|
||||
|
||||
// -----------------------------
|
||||
// BON 编解码器核心实现
|
||||
// -----------------------------
|
||||
|
||||
export class Int64 {
|
||||
constructor(high, low) {
|
||||
this.high = high;
|
||||
this.low = low;
|
||||
}
|
||||
}
|
||||
|
||||
export class DataReader {
|
||||
constructor(bytes) {
|
||||
this._data = bytes || new Uint8Array(0);
|
||||
this._view = null;
|
||||
this.position = 0;
|
||||
}
|
||||
|
||||
get data() { return this._data; }
|
||||
get dataView() {
|
||||
return this._view || (this._view = new DataView(this._data.buffer, this._data.byteOffset, this._data.byteLength));
|
||||
}
|
||||
|
||||
reset(bytes) {
|
||||
this._data = bytes;
|
||||
this.position = 0;
|
||||
this._view = null;
|
||||
}
|
||||
|
||||
validate(n) {
|
||||
if (this.position + n > this._data.length) {
|
||||
console.error('read eof');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
readUInt8() {
|
||||
if (!this.validate(1)) return;
|
||||
return this._data[this.position++];
|
||||
}
|
||||
|
||||
readInt16() {
|
||||
if (!this.validate(2)) return;
|
||||
const v = this._data[this.position++] | (this._data[this.position++] << 8);
|
||||
return (v << 16) >> 16;
|
||||
}
|
||||
|
||||
readInt32() {
|
||||
if (!this.validate(4)) return;
|
||||
const v = this._data[this.position++] | (this._data[this.position++] << 8) | (this._data[this.position++] << 16) | (this._data[this.position++] << 24);
|
||||
return v | 0;
|
||||
}
|
||||
|
||||
readInt64() {
|
||||
const lo = this.readInt32();
|
||||
if (lo === undefined) return;
|
||||
let _lo = lo;
|
||||
if (_lo < 0) _lo += 0x100000000;
|
||||
const hi = this.readInt32();
|
||||
if (hi === undefined) return;
|
||||
return _lo + 0x100000000 * hi;
|
||||
}
|
||||
|
||||
readFloat32() {
|
||||
if (!this.validate(4)) return;
|
||||
const v = this.dataView.getFloat32(this.position, true);
|
||||
this.position += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
readFloat64() {
|
||||
if (!this.validate(8)) return;
|
||||
const v = this.dataView.getFloat64(this.position, true);
|
||||
this.position += 8;
|
||||
return v;
|
||||
}
|
||||
|
||||
read7BitInt() {
|
||||
let value = 0;
|
||||
let shift = 0;
|
||||
let b = 0;
|
||||
let count = 0;
|
||||
do {
|
||||
if (count++ === 35) throw new Error('Format_Bad7BitInt32');
|
||||
b = this.readUInt8();
|
||||
value |= (b & 0x7F) << shift;
|
||||
shift += 7;
|
||||
} while ((b & 0x80) !== 0);
|
||||
return value >>> 0;
|
||||
}
|
||||
|
||||
readUTF() {
|
||||
const len = this.read7BitInt();
|
||||
return this.readUTFBytes(len);
|
||||
}
|
||||
|
||||
readUint8Array(length, copy = false) {
|
||||
const start = this.position;
|
||||
const end = start + length;
|
||||
const out = copy ? this._data.slice(start, end) : this._data.subarray(start, end);
|
||||
this.position = end;
|
||||
return out;
|
||||
}
|
||||
|
||||
readUTFBytes(length) {
|
||||
if (length === 0) return '';
|
||||
if (!this.validate(length)) return;
|
||||
const str = new TextDecoder('utf8').decode(this._data.subarray(this.position, this.position + length));
|
||||
this.position += length;
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
let _shared = new Uint8Array(524288); // 512 KB initial buffer
|
||||
|
||||
export class DataWriter {
|
||||
constructor() {
|
||||
this.position = 0;
|
||||
this._view = null;
|
||||
this.data = _shared;
|
||||
}
|
||||
|
||||
get dataView() {
|
||||
return this._view || (this._view = new DataView(this.data.buffer, 0, this.data.byteLength));
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.data = _shared;
|
||||
this._view = null;
|
||||
this.position = 0;
|
||||
}
|
||||
|
||||
ensureBuffer(size) {
|
||||
if (this.position + size <= _shared.byteLength) return;
|
||||
const prev = _shared;
|
||||
const need = this.position + size;
|
||||
const nextLen = Math.max(Math.floor((_shared.byteLength * 12) / 10), need);
|
||||
_shared = new Uint8Array(nextLen);
|
||||
_shared.set(prev, 0);
|
||||
this.data = _shared;
|
||||
this._view = null;
|
||||
}
|
||||
|
||||
writeInt8(v) {
|
||||
this.ensureBuffer(1);
|
||||
this.data[this.position++] = v | 0;
|
||||
}
|
||||
|
||||
writeInt16(v) {
|
||||
this.ensureBuffer(2);
|
||||
this.data[this.position++] = v | 0;
|
||||
this.data[this.position++] = (v >> 8) & 0xFF;
|
||||
}
|
||||
|
||||
writeInt32(v) {
|
||||
this.ensureBuffer(4);
|
||||
this.data[this.position++] = v | 0;
|
||||
this.data[this.position++] = (v >> 8) & 0xFF;
|
||||
this.data[this.position++] = (v >> 16) & 0xFF;
|
||||
this.data[this.position++] = (v >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
writeInt64(v) {
|
||||
this.writeInt32(v);
|
||||
if (v < 0) {
|
||||
this.writeInt32(~Math.floor((-v) / 0x100000000));
|
||||
} else {
|
||||
this.writeInt32(Math.floor(v / 0x100000000) | 0);
|
||||
}
|
||||
}
|
||||
|
||||
writeFloat32(v) {
|
||||
this.ensureBuffer(4);
|
||||
this.dataView.setFloat32(this.position, v, true);
|
||||
this.position += 4;
|
||||
}
|
||||
|
||||
writeFloat64(v) {
|
||||
this.ensureBuffer(8);
|
||||
this.dataView.setFloat64(this.position, v, true);
|
||||
this.position += 8;
|
||||
}
|
||||
|
||||
_write7BitInt(v) {
|
||||
let n = v >>> 0;
|
||||
while (n >= 0x80) {
|
||||
this.data[this.position++] = (n & 0xFF) | 0x80;
|
||||
n >>>= 7;
|
||||
}
|
||||
this.data[this.position++] = n & 0x7F;
|
||||
}
|
||||
|
||||
write7BitInt(v) {
|
||||
this.ensureBuffer(5);
|
||||
this._write7BitInt(v);
|
||||
}
|
||||
|
||||
_7BitIntLen(v) {
|
||||
return v < 0 ? 5
|
||||
: v < 0x80 ? 1
|
||||
: v < 0x4000 ? 2
|
||||
: v < 0x200000 ? 3
|
||||
: v < 0x10000000 ? 4
|
||||
: 5;
|
||||
}
|
||||
|
||||
writeUTF(str) {
|
||||
const t = str.length;
|
||||
if (t === 0) {
|
||||
this.write7BitInt(0);
|
||||
return;
|
||||
}
|
||||
const max = 6 * t;
|
||||
this.ensureBuffer(5 + max);
|
||||
const start = this.position;
|
||||
this.position += this._7BitIntLen(max);
|
||||
const from = this.position;
|
||||
const reserved = from - start;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const { written } = encoder.encodeInto(str, this.data.subarray(this.position));
|
||||
this.position += written;
|
||||
const after = this.position;
|
||||
const size = after - from;
|
||||
|
||||
this.position = start;
|
||||
this._write7BitInt(size);
|
||||
const used = this.position - start;
|
||||
if (used !== reserved) {
|
||||
this.data.copyWithin(from + (used - reserved), from, after);
|
||||
}
|
||||
this.position = from + size + (used - reserved);
|
||||
}
|
||||
|
||||
writeUint8Array(src, offset = 0, length) {
|
||||
const start = offset | 0;
|
||||
const end = Math.min(src.byteLength, start + (length ?? src.byteLength));
|
||||
const n = end - start;
|
||||
if (n <= 0) return;
|
||||
this.ensureBuffer(n);
|
||||
this.data.set(src.subarray(start, end), this.position);
|
||||
this.position += n;
|
||||
}
|
||||
|
||||
writeUTFBytes(str) {
|
||||
this.ensureBuffer(6 * str.length);
|
||||
const encoder = new TextEncoder();
|
||||
const { written } = encoder.encodeInto(str, this.data.subarray(this.position));
|
||||
this.position += written;
|
||||
}
|
||||
|
||||
getBytes(clone = false) {
|
||||
return clone ? this.data.slice(0, this.position) : this.data.subarray(0, this.position);
|
||||
}
|
||||
}
|
||||
|
||||
export class BonEncoder {
|
||||
constructor() {
|
||||
this.dw = new DataWriter();
|
||||
this.strMap = new Map();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.dw.reset();
|
||||
this.strMap.clear();
|
||||
}
|
||||
|
||||
encodeInt(v) {
|
||||
this.dw.writeInt8(1);
|
||||
this.dw.writeInt32(v | 0);
|
||||
}
|
||||
|
||||
encodeLong(v) {
|
||||
this.dw.writeInt8(2);
|
||||
if (typeof v === 'number') {
|
||||
this.dw.writeInt64(v);
|
||||
} else {
|
||||
this.dw.writeInt32(v.low | 0);
|
||||
this.dw.writeInt32(v.high | 0);
|
||||
}
|
||||
}
|
||||
|
||||
encodeFloat(v) {
|
||||
this.dw.writeInt8(3);
|
||||
this.dw.writeFloat32(v);
|
||||
}
|
||||
|
||||
encodeDouble(v) {
|
||||
this.dw.writeInt8(4);
|
||||
this.dw.writeFloat64(v);
|
||||
}
|
||||
|
||||
encodeNumber(v) {
|
||||
if ((v | 0) === v) this.encodeInt(v);
|
||||
else if (Math.floor(v) === v) this.encodeLong(v);
|
||||
else this.encodeDouble(v);
|
||||
}
|
||||
|
||||
encodeString(s) {
|
||||
const hit = this.strMap.get(s);
|
||||
if (hit !== undefined) {
|
||||
this.dw.writeInt8(99); // StringRef
|
||||
this.dw.write7BitInt(hit);
|
||||
return;
|
||||
}
|
||||
this.dw.writeInt8(5); // String
|
||||
this.dw.writeUTF(s);
|
||||
this.strMap.set(s, this.strMap.size);
|
||||
}
|
||||
|
||||
encodeBoolean(b) {
|
||||
this.dw.writeInt8(6);
|
||||
this.dw.writeInt8(b ? 1 : 0);
|
||||
}
|
||||
|
||||
encodeNull() {
|
||||
this.dw.writeInt8(0);
|
||||
}
|
||||
|
||||
encodeDateTime(d) {
|
||||
this.dw.writeInt8(10);
|
||||
this.dw.writeInt64(d.getTime());
|
||||
}
|
||||
|
||||
encodeBinary(u8) {
|
||||
this.dw.writeInt8(7);
|
||||
this.dw.write7BitInt(u8.byteLength);
|
||||
this.dw.writeUint8Array(u8);
|
||||
}
|
||||
|
||||
encodeArray(arr) {
|
||||
this.dw.writeInt8(9);
|
||||
this.dw.write7BitInt(arr.length);
|
||||
for (let i = 0; i < arr.length; i++) this.encode(arr[i]);
|
||||
}
|
||||
|
||||
encodeMap(mp) {
|
||||
this.dw.writeInt8(8);
|
||||
this.dw.write7BitInt(mp.size);
|
||||
mp.forEach((v, k) => {
|
||||
this.encode(k);
|
||||
this.encode(v);
|
||||
});
|
||||
}
|
||||
|
||||
encodeObject(obj) {
|
||||
this.dw.writeInt8(8);
|
||||
const keys = [];
|
||||
for (const k in obj) {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
|
||||
if (k.startsWith('_')) continue;
|
||||
const type = typeof obj[k];
|
||||
if (type === 'function' || type === 'undefined') continue;
|
||||
keys.push(k);
|
||||
}
|
||||
this.dw.write7BitInt(keys.length);
|
||||
for (const k of keys) {
|
||||
this.encode(k);
|
||||
this.encode(obj[k]);
|
||||
}
|
||||
}
|
||||
|
||||
encode(v) {
|
||||
if (v == null) {
|
||||
this.encodeNull();
|
||||
return;
|
||||
}
|
||||
switch (v.constructor) {
|
||||
case Number:
|
||||
this.encodeNumber(v);
|
||||
return;
|
||||
case Boolean:
|
||||
this.encodeBoolean(v);
|
||||
return;
|
||||
case String:
|
||||
this.encodeString(v);
|
||||
return;
|
||||
case Int64:
|
||||
this.encodeLong(v);
|
||||
return;
|
||||
case Array:
|
||||
this.encodeArray(v);
|
||||
return;
|
||||
case Map:
|
||||
this.encodeMap(v);
|
||||
return;
|
||||
case Date:
|
||||
this.encodeDateTime(v);
|
||||
return;
|
||||
case Uint8Array:
|
||||
this.encodeBinary(v);
|
||||
return;
|
||||
default:
|
||||
if (typeof v !== 'object') {
|
||||
this.encodeNull();
|
||||
return;
|
||||
}
|
||||
this.encodeObject(v);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getBytes(clone = false) {
|
||||
return this.dw.getBytes(clone);
|
||||
}
|
||||
}
|
||||
|
||||
export class BonDecoder {
|
||||
constructor() {
|
||||
this.dr = new DataReader(new Uint8Array(0));
|
||||
this.strArr = [];
|
||||
}
|
||||
|
||||
reset(bytes) {
|
||||
this.dr.reset(bytes);
|
||||
this.strArr.length = 0;
|
||||
}
|
||||
|
||||
decode() {
|
||||
const tag = this.dr.readUInt8();
|
||||
switch (tag) {
|
||||
default:
|
||||
return null;
|
||||
case 1:
|
||||
return this.dr.readInt32();
|
||||
case 2:
|
||||
return this.dr.readInt64();
|
||||
case 3:
|
||||
return this.dr.readFloat32();
|
||||
case 4:
|
||||
return this.dr.readFloat64();
|
||||
case 5: {
|
||||
const s = this.dr.readUTF();
|
||||
this.strArr.push(s);
|
||||
return s;
|
||||
}
|
||||
case 6:
|
||||
return this.dr.readUInt8() === 1;
|
||||
case 7: {
|
||||
const len = this.dr.read7BitInt();
|
||||
return this.dr.readUint8Array(len, false);
|
||||
}
|
||||
case 8: {
|
||||
const count = this.dr.read7BitInt();
|
||||
const obj = {};
|
||||
for (let i = 0; i < count; i++) {
|
||||
const k = this.decode();
|
||||
const v = this.decode();
|
||||
obj[k] = v;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
case 9: {
|
||||
const len = this.dr.read7BitInt();
|
||||
const arr = new Array(len);
|
||||
for (let i = 0; i < len; i++) arr[i] = this.decode();
|
||||
return arr;
|
||||
}
|
||||
case 10:
|
||||
return new Date(this.dr.readInt64());
|
||||
case 99:
|
||||
return this.strArr[this.dr.read7BitInt()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单例实例
|
||||
const _enc = new BonEncoder();
|
||||
const _dec = new BonDecoder();
|
||||
|
||||
// BON 编解码函数
|
||||
export const bon = {
|
||||
encode: (value, clone = true) => {
|
||||
_enc.reset();
|
||||
_enc.encode(value);
|
||||
return _enc.getBytes(clone);
|
||||
},
|
||||
decode: (bytes) => {
|
||||
_dec.reset(bytes);
|
||||
return _dec.decode();
|
||||
}
|
||||
};
|
||||
|
||||
/** —— 协议消息包装,与原 ProtoMsg 类等价 —— */
|
||||
export class ProtoMsg {
|
||||
constructor(raw) {
|
||||
if (raw?.cmd) {
|
||||
raw.cmd = raw.cmd.toLowerCase();
|
||||
}
|
||||
this._raw = raw;
|
||||
this._rawData = undefined;
|
||||
this._data = undefined;
|
||||
this._t = undefined;
|
||||
this._sendMsg = undefined;
|
||||
this.rtt = 0;
|
||||
}
|
||||
|
||||
get sendMsg() { return this._sendMsg; }
|
||||
get seq() { return this._raw.seq; }
|
||||
get resp() { return this._raw.resp; }
|
||||
get ack() { return this._raw.ack; }
|
||||
get cmd() { return this._raw?.cmd && this._raw?.cmd.toLowerCase(); }
|
||||
get code() { return ~~this._raw.code; }
|
||||
get error() { return this._raw.error; }
|
||||
get time() { return this._raw.time; }
|
||||
get body() { return this._raw.body; }
|
||||
|
||||
/** 惰性 decode body → rawData(bon.decode) */
|
||||
get rawData() {
|
||||
if (this._rawData !== undefined || this.body === undefined) return this._rawData;
|
||||
this._rawData = bon.decode(this.body);
|
||||
return this._rawData;
|
||||
}
|
||||
|
||||
/** 指定数据类型 */
|
||||
setDataType(t) {
|
||||
if (t) this._t = { name: t.name ?? 'Anonymous', ctor: t };
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 配置"请求"对象,让 respType 自动对齐 */
|
||||
setSendMsg(msg) {
|
||||
this._sendMsg = msg;
|
||||
return this.setDataType(msg.respType);
|
||||
}
|
||||
|
||||
/** 将 rawData 反序列化为业务对象 */
|
||||
getData(clazz) {
|
||||
if (this._data !== undefined || this.rawData === undefined) return this._data;
|
||||
|
||||
let t = this._t;
|
||||
if (clazz && t && clazz !== t.ctor) {
|
||||
console.warn(`getData type not match, ${clazz.name} != ${t.name}`);
|
||||
t = { name: clazz.name, ctor: clazz };
|
||||
}
|
||||
|
||||
this._data = this.rawData;
|
||||
return this._data;
|
||||
}
|
||||
|
||||
toLogString() {
|
||||
const e = { ...this._raw };
|
||||
delete e.body;
|
||||
e.data = this.rawData;
|
||||
e.rtt = this.rtt;
|
||||
return JSON.stringify(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** —— 加解密器注册表 —— */
|
||||
const registry = new Map();
|
||||
|
||||
/** lz4 + 头部掩码的 "lx" 方案 */
|
||||
const lx = {
|
||||
encrypt: (buf) => {
|
||||
let e = lz4.compress(buf);
|
||||
const t = 2 + ~~(Math.random() * 248);
|
||||
for (let n = Math.min(e.length, 100); --n >= 0; ) e[n] ^= t;
|
||||
|
||||
// 写入标识与混淆位
|
||||
e[0] = 112; e[1] = 108;
|
||||
e[2] = (e[2] & 0b10101010) | ((t >> 7 & 1) << 6) | ((t >> 6 & 1) << 4) | ((t >> 5 & 1) << 2) | (t >> 4 & 1);
|
||||
e[3] = (e[3] & 0b10101010) | ((t >> 3 & 1) << 6) | ((t >> 2 & 1) << 4) | ((t >> 1 & 1) << 2) | (t & 1);
|
||||
return e;
|
||||
},
|
||||
decrypt: (e) => {
|
||||
const t =
|
||||
((e[2] >> 6 & 1) << 7) | ((e[2] >> 4 & 1) << 6) | ((e[2] >> 2 & 1) << 5) | ((e[2] & 1) << 4) |
|
||||
((e[3] >> 6 & 1) << 3) | ((e[3] >> 4 & 1) << 2) | ((e[3] >> 2 & 1) << 1) | (e[3] & 1);
|
||||
for (let n = Math.min(100, e.length); --n >= 2; ) e[n] ^= t;
|
||||
e[0] = 4; e[1] = 34; e[2] = 77; e[3] = 24; // 还原头以便 lz4 解
|
||||
return lz4.decompress(e);
|
||||
}
|
||||
};
|
||||
|
||||
/** 随机首 4 字节 + XOR 的 "x" 方案 */
|
||||
const x = {
|
||||
encrypt: (e) => {
|
||||
const rnd = ~~(Math.random() * 0xFFFFFFFF) >>> 0;
|
||||
const n = new Uint8Array(e.length + 4);
|
||||
n[0] = rnd & 0xFF; n[1] = (rnd >>> 8) & 0xFF; n[2] = (rnd >>> 16) & 0xFF; n[3] = (rnd >>> 24) & 0xFF;
|
||||
n.set(e, 4);
|
||||
const r = 2 + ~~(Math.random() * 248);
|
||||
for (let i = n.length; --i >= 0; ) n[i] ^= r;
|
||||
n[0] = 112; n[1] = 120;
|
||||
n[2] = (n[2] & 0b10101010) | ((r >> 7 & 1) << 6) | ((r >> 6 & 1) << 4) | ((r >> 5 & 1) << 2) | (r >> 4 & 1);
|
||||
n[3] = (n[3] & 0b10101010) | ((r >> 3 & 1) << 6) | ((r >> 2 & 1) << 4) | ((r >> 1 & 1) << 2) | (r & 1);
|
||||
return n;
|
||||
},
|
||||
decrypt: (e) => {
|
||||
const t =
|
||||
((e[2] >> 6 & 1) << 7) | ((e[2] >> 4 & 1) << 6) | ((e[2] >> 2 & 1) << 5) | ((e[2] & 1) << 4) |
|
||||
((e[3] >> 6 & 1) << 3) | ((e[3] >> 4 & 1) << 2) | ((e[3] >> 2 & 1) << 1) | (e[3] & 1);
|
||||
for (let n = e.length; --n >= 4; ) e[n] ^= t;
|
||||
return e.subarray(4);
|
||||
}
|
||||
};
|
||||
|
||||
/** 依赖 globalThis.XXTEA 的 "xtm" 方案 */
|
||||
const xtm = {
|
||||
encrypt: (e) => globalThis.XXTEA ? globalThis.XXTEA.encryptMod({ data: e.buffer, length: e.length }) : e,
|
||||
decrypt: (e) => globalThis.XXTEA ? globalThis.XXTEA.decryptMod({ data: e.buffer, length: e.length }) : e,
|
||||
};
|
||||
|
||||
/** 注册器 */
|
||||
function register(name, impl) {
|
||||
registry.set(name, impl);
|
||||
}
|
||||
|
||||
register('lx', lx);
|
||||
register('x', x);
|
||||
register('xtm', xtm);
|
||||
|
||||
/** 默认使用 x 加密(自动检测解密) */
|
||||
const passthrough = {
|
||||
encrypt: (e) => getEnc('x').encrypt(e),
|
||||
decrypt: (e) => {
|
||||
if (e.length > 4 && e[0] === 112 && e[1] === 108) e = getEnc('lx').decrypt(e);
|
||||
else if (e.length > 4 && e[0] === 112 && e[1] === 120) e = getEnc('x').decrypt(e);
|
||||
else if (e.length > 3 && e[0] === 112 && e[1] === 116) e = getEnc('xtm').decrypt(e);
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
/** 对外:按名称取加解密器;找不到则用默认 */
|
||||
export function getEnc(name) {
|
||||
return registry.get(name) ?? passthrough;
|
||||
}
|
||||
|
||||
/** 对外:encode(bon.encode → 加密) */
|
||||
export function encode(obj, enc) {
|
||||
let bytes = bon.encode(obj, false);
|
||||
const out = enc.encrypt(bytes);
|
||||
return out.buffer.byteLength === out.length ? out.buffer : out.buffer.slice(0, out.length);
|
||||
}
|
||||
|
||||
/** 对外:parse(解密 → bon.decode → ProtoMsg) */
|
||||
export function parse(buf, enc) {
|
||||
const u8 = new Uint8Array(buf);
|
||||
const plain = enc.decrypt(u8);
|
||||
const raw = bon.decode(plain);
|
||||
return new ProtoMsg(raw);
|
||||
}
|
||||
|
||||
// 游戏消息模板
|
||||
export const GameMessages = {
|
||||
// 心跳消息
|
||||
heartBeat: (ack = 0, seq = 0) => ({
|
||||
ack,
|
||||
body: undefined,
|
||||
c: undefined,
|
||||
cmd: "_sys/ack",
|
||||
hint: undefined,
|
||||
seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 获取角色信息
|
||||
getRoleInfo: (ack = 0, seq = 0, params = {}) => ({
|
||||
cmd: "role_getroleinfo",
|
||||
body: encode({
|
||||
clientVersion: "1.65.3-wx",
|
||||
inviteUid: 0,
|
||||
platform: "hortor",
|
||||
platformExt: "mix",
|
||||
scene: "",
|
||||
...params
|
||||
}, getEnc('x')),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 获取数据包版本
|
||||
getDataBundleVer: (ack = 0, seq = 0, params = {}) => ({
|
||||
cmd: "system_getdatabundlever",
|
||||
body: encode({
|
||||
isAudit: false,
|
||||
...params
|
||||
}, getEnc('x')),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 购买金币
|
||||
buyGold: (ack = 0, seq = 0, params = {}) => ({
|
||||
ack,
|
||||
body: encode({
|
||||
buyNum: 1,
|
||||
...params
|
||||
}, getEnc('x')),
|
||||
cmd: "system_buygold",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 签到奖励
|
||||
signInReward: (ack = 0, seq = 0, params = {}) => ({
|
||||
ack,
|
||||
body: encode({
|
||||
...params
|
||||
}, getEnc('x')),
|
||||
cmd: "system_signinreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 领取每日任务奖励
|
||||
claimDailyReward: (ack = 0, seq = 0, params = {}) => ({
|
||||
ack,
|
||||
body: encode({
|
||||
rewardId: 0,
|
||||
...params
|
||||
}, getEnc('x')),
|
||||
cmd: "task_claimdailyreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
})
|
||||
};
|
||||
|
||||
// 创建全局实例
|
||||
export const g_utils = {
|
||||
getEnc,
|
||||
encode: (obj, encName = 'x') => encode(obj, getEnc(encName)),
|
||||
parse: (data, encName = 'auto') => parse(data, getEnc(encName)),
|
||||
bon // 添加BON编解码器
|
||||
};
|
||||
|
||||
// 兼容性导出(保持旧的接口)
|
||||
export const bonProtocol = {
|
||||
encode: bon.encode,
|
||||
decode: bon.decode,
|
||||
createMessage: (cmd, body = {}, ack = 0, seq = 0, options = {}) => ({
|
||||
cmd,
|
||||
body: bon.encode(body),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
time: Date.now(),
|
||||
...options
|
||||
}),
|
||||
parseMessage: (messageData) => {
|
||||
try {
|
||||
let message;
|
||||
if (typeof messageData === 'string') {
|
||||
message = JSON.parse(messageData);
|
||||
} else {
|
||||
message = messageData;
|
||||
}
|
||||
if (message.body && (message.body instanceof ArrayBuffer || message.body instanceof Uint8Array)) {
|
||||
message.body = bon.decode(message.body);
|
||||
}
|
||||
return message;
|
||||
} catch (error) {
|
||||
console.error('消息解析失败:', error);
|
||||
return {
|
||||
error: true,
|
||||
message: '消息解析失败',
|
||||
originalData: messageData
|
||||
};
|
||||
}
|
||||
},
|
||||
generateSeq: () => Math.floor(Math.random() * 1000000),
|
||||
generateMessageId: () => 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
||||
};
|
||||
|
||||
// 导出单独的加密器类以兼容测试文件
|
||||
export const LXCrypto = lx;
|
||||
export const XCrypto = x;
|
||||
export const XTMCrypto = xtm;
|
||||
|
||||
export default { ProtoMsg, getEnc, encode, parse, GameMessages, g_utils, bon, bonProtocol };
|
||||
692
src/utils/gameCommands.js
Normal file
692
src/utils/gameCommands.js
Normal file
@@ -0,0 +1,692 @@
|
||||
/**
|
||||
* 游戏命令构造器
|
||||
* 基于mirror代码中的游戏指令实现完整的游戏功能
|
||||
*/
|
||||
|
||||
import { g_utils } from './bonProtocol.js'
|
||||
|
||||
// 生成随机数工具函数
|
||||
function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏命令构造器类
|
||||
* 每个命令方法返回标准的WebSocket消息格式
|
||||
*/
|
||||
export class GameCommands {
|
||||
constructor(g_utils_instance = g_utils) {
|
||||
this.g_utils = g_utils_instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳消息
|
||||
*/
|
||||
heart_beat(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: undefined,
|
||||
c: undefined,
|
||||
cmd: "_sys/ack",
|
||||
hint: undefined,
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色信息
|
||||
*/
|
||||
role_getroleinfo(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
cmd: "role_getroleinfo",
|
||||
body: this.g_utils.bon.encode({
|
||||
clientVersion: "1.65.3-wx",
|
||||
inviteUid: 0,
|
||||
platform: "hortor",
|
||||
platformExt: "mix",
|
||||
scene: "",
|
||||
...params
|
||||
}),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
rtt: randomInt(0, 500),
|
||||
code: 0,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据包版本
|
||||
*/
|
||||
system_getdatabundlever(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
cmd: "system_getdatabundlever",
|
||||
body: this.g_utils.bon.encode({
|
||||
isAudit: false,
|
||||
...params
|
||||
}),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
rtt: randomInt(0, 500),
|
||||
code: 0,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买金币
|
||||
*/
|
||||
system_buygold(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
buyNum: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "system_buygold",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享回调
|
||||
*/
|
||||
system_mysharecallback(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
type: 3,
|
||||
isSkipShareCard: true,
|
||||
...params
|
||||
}),
|
||||
cmd: "system_mysharecallback",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 好友批处理
|
||||
*/
|
||||
friend_batch(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
friendId: 0,
|
||||
...params
|
||||
}),
|
||||
cmd: "friend_batch",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 英雄招募
|
||||
*/
|
||||
hero_recruit(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
byClub: false,
|
||||
recruitNumber: 1,
|
||||
recruitType: 3,
|
||||
...params
|
||||
}),
|
||||
cmd: "hero_recruit",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取挂机奖励
|
||||
*/
|
||||
system_claimhangupreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "system_claimhangupreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开宝箱
|
||||
*/
|
||||
item_openbox(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
itemId: 2001,
|
||||
number: 10,
|
||||
...params
|
||||
}),
|
||||
cmd: "item_openbox",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始竞技场
|
||||
*/
|
||||
arena_startarea(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "arena_startarea",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取竞技场目标
|
||||
*/
|
||||
arena_getareatarget(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
refresh: false,
|
||||
...params
|
||||
}),
|
||||
cmd: "arena_getareatarget",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始竞技场战斗
|
||||
*/
|
||||
fight_startareaarena(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
targetId: 530479307,
|
||||
...params
|
||||
}),
|
||||
cmd: "fight_startareaarena",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取竞技场排名
|
||||
*/
|
||||
arena_getarearank(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
rankType: 0,
|
||||
...params
|
||||
}),
|
||||
cmd: "arena_getarearank",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商店商品列表
|
||||
*/
|
||||
store_goodslist(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
storeId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "store_goodslist",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 商店购买
|
||||
*/
|
||||
store_buy(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
goodsId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "store_buy",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 商店刷新
|
||||
*/
|
||||
store_refresh(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
storeId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "store_refresh",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取机器人助手奖励
|
||||
*/
|
||||
bottlehelper_claim(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "bottlehelper_claim",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动机器人助手
|
||||
*/
|
||||
bottlehelper_start(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
bottleType: -1,
|
||||
...params
|
||||
}),
|
||||
cmd: "bottlehelper_start",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止机器人助手
|
||||
*/
|
||||
bottlehelper_stop(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
bottleType: -1,
|
||||
...params
|
||||
}),
|
||||
cmd: "bottlehelper_stop",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 神器抽奖
|
||||
*/
|
||||
artifact_lottery(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
lotteryNumber: 1,
|
||||
newFree: true,
|
||||
type: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "artifact_lottery",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取每日积分
|
||||
*/
|
||||
task_claimdailypoint(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
taskId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "task_claimdailypoint",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取周奖励
|
||||
*/
|
||||
task_claimweekreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
rewardId: 0,
|
||||
...params
|
||||
}),
|
||||
cmd: "task_claimweekreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始BOSS战
|
||||
*/
|
||||
fight_startboss(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "fight_startboss",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 精灵扫荡
|
||||
*/
|
||||
genie_sweep(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "genie_sweep",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买精灵扫荡
|
||||
*/
|
||||
genie_buysweep(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "genie_buysweep",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 签到奖励
|
||||
*/
|
||||
system_signinreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "system_signinreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取折扣奖励
|
||||
*/
|
||||
discount_claimreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
discountId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "discount_claimreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取卡片奖励
|
||||
*/
|
||||
card_claimreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
cardId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "card_claimreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 军团签到
|
||||
*/
|
||||
legion_signin(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "legion_signin",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始军团BOSS战
|
||||
*/
|
||||
fight_startlegionboss(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "fight_startlegionboss",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取每日任务奖励
|
||||
*/
|
||||
task_claimdailyreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
rewardId: 0,
|
||||
...params
|
||||
}),
|
||||
cmd: "task_claimdailyreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取军团信息
|
||||
*/
|
||||
legion_getinfo(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({}),
|
||||
cmd: "legion_getinfo",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 军团匹配角色报名
|
||||
*/
|
||||
legionmatch_rolesignup(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({}),
|
||||
cmd: "legionmatch_rolesignup",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始爬塔
|
||||
*/
|
||||
fight_starttower(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({}),
|
||||
cmd: "fight_starttower",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取爬塔奖励
|
||||
*/
|
||||
tower_claimreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "tower_claimreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取爬塔信息
|
||||
*/
|
||||
tower_getinfo(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "tower_getinfo",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始答题游戏
|
||||
*/
|
||||
study_startgame(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({}),
|
||||
cmd: "study_startgame",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 答题
|
||||
*/
|
||||
study_answer(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
...params
|
||||
}),
|
||||
cmd: "study_answer",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取答题奖励
|
||||
*/
|
||||
study_claimreward(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
rewardId: 1,
|
||||
...params
|
||||
}),
|
||||
cmd: "study_claimreward",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邮件列表
|
||||
*/
|
||||
mail_getlist(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
category: [0, 4, 5],
|
||||
lastId: 0,
|
||||
size: 60,
|
||||
...params
|
||||
}),
|
||||
cmd: "mail_getlist",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取所有邮件附件
|
||||
*/
|
||||
mail_claimallattachment(ack = 0, seq = 0, params = {}) {
|
||||
return {
|
||||
ack,
|
||||
body: this.g_utils.bon.encode({
|
||||
category: 0,
|
||||
...params
|
||||
}),
|
||||
cmd: "mail_claimallattachment",
|
||||
seq,
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 三国答题题库(基于mirror代码中的题目)
|
||||
export const studyQuestions = [
|
||||
{name: "", value: 2},
|
||||
{name: "《三国演义》中,「大意失街亭」的是马谩?", value: 1},
|
||||
{name: "《三国演义》中,「挥泪斩马谩」的是孙权?", value: 2},
|
||||
{name: "《三国演义》中,「火烧博望坡」的是庞统?", value: 2},
|
||||
{name: "《三国演义》中,「火烧藤甲兵」的是徐庶?", value: 2},
|
||||
{name: "《三国演义》中,「千里走单骑」的是赵云?", value: 2},
|
||||
{name: "《三国演义》中,「温酒斩华雄」的是张飞?", value: 2},
|
||||
{name: "《三国演义》中,关羽在长坂坡「七进七出」?", value: 2},
|
||||
{name: "《三国演义》中,刘备三顾茅庐请诸葛亮出山?", value: 1},
|
||||
{name: "《三国演义》中,孙权与曹操「煮酒论英雄」?", value: 2},
|
||||
{name: "《三国演义》中,提出「隆中对」的是诸葛亮?", value: 1},
|
||||
{name: "《三国演义》中,夏侯杰在当阳桥被张飞吓死?", value: 1},
|
||||
{name: "《三国演义》中,张飞在当阳桥厉吼吓退曹军?", value: 1},
|
||||
{name: "《三国演义》中,赵云参与了「三英战吕布」?", value: 2},
|
||||
{name: "《三国演义》中,赵云参与了「桃园三结义」?", value: 2}
|
||||
// 更多题目可以从原始数据中添加...
|
||||
]
|
||||
|
||||
// 创建命令实例
|
||||
export const gameCommands = new GameCommands()
|
||||
export default GameCommands
|
||||
547
src/utils/readable-xyzw-ws.js
Normal file
547
src/utils/readable-xyzw-ws.js
Normal file
@@ -0,0 +1,547 @@
|
||||
// 解析后的XYZW WebSocket通信库
|
||||
// 原文件: CTx_gHj7.js (混淆版本)
|
||||
|
||||
// 导入依赖模块
|
||||
import { a$ as createRef, G as createApp, $ as defineComponent, n as ref, b0 as computed } from "./DpD38Hq9.js";
|
||||
import { c as useI18n, g as getConfig, u as useState } from "./BUzHT0Ek.js";
|
||||
|
||||
// 字符串相似度计算函数 (Levenshtein Distance 算法)
|
||||
const calculateStringSimilarity = (() => {
|
||||
let cache, isInitialized;
|
||||
|
||||
return createRef(isInitialized ? cache : (isInitialized = 1, cache = function () {
|
||||
// 计算两个字符串之间的编辑距离
|
||||
function calculateDistance(a, b, c, d, e) {
|
||||
return a < b || c < b ? a > c ? c + 1 : a + 1 : d === e ? b : b + 1;
|
||||
}
|
||||
|
||||
return function (str1, str2) {
|
||||
if (str1 === str2) return 0;
|
||||
|
||||
// 确保str1是较短的字符串
|
||||
if (str1.length > str2.length) {
|
||||
[str1, str2] = [str2, str1];
|
||||
}
|
||||
|
||||
let len1 = str1.length;
|
||||
let len2 = str2.length;
|
||||
|
||||
// 去除相同的前缀和后缀
|
||||
while (len1 > 0 && str1.charCodeAt(len1 - 1) === str2.charCodeAt(len2 - 1)) {
|
||||
len1--;
|
||||
len2--;
|
||||
}
|
||||
|
||||
let start = 0;
|
||||
while (start < len1 && str1.charCodeAt(start) === str2.charCodeAt(start)) {
|
||||
start++;
|
||||
}
|
||||
|
||||
len2 -= start;
|
||||
len1 -= start;
|
||||
|
||||
if (len1 === 0 || len2 < 3) return len2;
|
||||
|
||||
// 动态规划计算编辑距离
|
||||
let row = [];
|
||||
for (let i = 0; i < len1; i++) {
|
||||
row.push(i + 1, str1.charCodeAt(start + i));
|
||||
}
|
||||
|
||||
let currentRow = 0;
|
||||
let rowLength = row.length - 1;
|
||||
|
||||
while (currentRow < len2 - 3) {
|
||||
let char1 = str2.charCodeAt(start + currentRow);
|
||||
let char2 = str2.charCodeAt(start + currentRow + 1);
|
||||
let char3 = str2.charCodeAt(start + currentRow + 2);
|
||||
let char4 = str2.charCodeAt(start + currentRow + 3);
|
||||
|
||||
let newValue = currentRow += 4;
|
||||
|
||||
for (let j = 0; j < rowLength; j += 2) {
|
||||
let oldValue = row[j];
|
||||
let charCode = row[j + 1];
|
||||
|
||||
char1 = calculateDistance(oldValue, char1, char2, char1, charCode);
|
||||
char2 = calculateDistance(char1, char2, char3, char2, charCode);
|
||||
char3 = calculateDistance(char2, char3, char4, char3, charCode);
|
||||
newValue = calculateDistance(char3, char4, newValue, char4, charCode);
|
||||
|
||||
row[j] = newValue;
|
||||
char4 = char3;
|
||||
char3 = char2;
|
||||
char2 = char1;
|
||||
char1 = oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理剩余字符
|
||||
while (currentRow < len2) {
|
||||
let char = str2.charCodeAt(start + currentRow);
|
||||
let newValue = ++currentRow;
|
||||
|
||||
for (let j = 0; j < rowLength; j += 2) {
|
||||
let oldValue = row[j];
|
||||
row[j] = newValue = calculateDistance(oldValue, char, newValue, char, row[j + 1]);
|
||||
char = oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
return newValue;
|
||||
};
|
||||
}()));
|
||||
})();
|
||||
|
||||
// 生成随机数
|
||||
function generateRandomNumber(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
// 时间格式化函数
|
||||
function formatTime(seconds) {
|
||||
const totalSeconds = Math.floor(seconds);
|
||||
const hours = Math.floor(totalSeconds / 3600).toString().padStart(2, "0");
|
||||
const remainingSeconds = totalSeconds % 3600;
|
||||
const minutes = Math.floor(remainingSeconds / 60);
|
||||
const secs = Math.floor(remainingSeconds % 60);
|
||||
|
||||
const formattedHours = hours.toString().padStart(2, "0");
|
||||
const formattedMinutes = minutes.toString().padStart(2, "0");
|
||||
const formattedSeconds = (secs < 10 ? "0" : "") + secs.toString();
|
||||
|
||||
let formatTime = "00:00:00";
|
||||
if (seconds > 0) {
|
||||
formatTime = `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
|
||||
}
|
||||
|
||||
return {
|
||||
hours: formattedHours,
|
||||
minutes: formattedMinutes,
|
||||
seconds: formattedSeconds,
|
||||
formatTime: formatTime
|
||||
};
|
||||
}
|
||||
|
||||
// 字符串相似度检查
|
||||
function checkStringSimilarity(str1, str2, threshold) {
|
||||
if (!str1 || !str2) return false;
|
||||
return 1 - calculateStringSimilarity(str1, str2) / Math.max(str1.length, str2.length) >= threshold;
|
||||
}
|
||||
|
||||
// 数值格式化函数 (支持万、亿单位)
|
||||
function formatNumber(num, decimals = 2) {
|
||||
if (num === undefined || isNaN(num) || num <= 0) return "0";
|
||||
|
||||
const billion = 100000000; // 1亿
|
||||
const tenThousand = 10000; // 1万
|
||||
|
||||
const formatDecimal = (value) => {
|
||||
const str = value.toString();
|
||||
const [integer, decimal = ""] = str.split(".");
|
||||
return decimal.length >= decimals
|
||||
? `${integer}.${decimal.slice(0, decimals)}`
|
||||
: `${integer}.${"0".repeat(decimals - decimal.length)}${decimal}`;
|
||||
};
|
||||
|
||||
if (num >= billion) {
|
||||
return `${formatDecimal(num / billion)}亿`;
|
||||
} else if (num >= tenThousand) {
|
||||
return `${formatDecimal(num / tenThousand)}万`;
|
||||
} else if (num < 1) {
|
||||
return `0.${"0".repeat(decimals)}${num.toFixed(decimals + 1).slice(-decimals)}`;
|
||||
} else {
|
||||
return num.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟函数
|
||||
function delay(milliseconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
// 游戏消息模板定义
|
||||
const gameMessageTemplates = {
|
||||
// 心跳包
|
||||
heart_beat: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: undefined,
|
||||
c: undefined,
|
||||
cmd: "_sys/ack",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 获取角色信息
|
||||
role_getroleinfo: (client, ack, seq, params) => ({
|
||||
cmd: "role_getroleinfo",
|
||||
body: client.bon.encode({
|
||||
clientVersion: "1.65.3-wx",
|
||||
inviteUid: 0,
|
||||
platform: "hortor",
|
||||
platformExt: "mix",
|
||||
scene: "",
|
||||
...params
|
||||
}),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
rtt: generateRandomNumber(0, 500),
|
||||
code: 0,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 获取数据包版本
|
||||
system_getdatabundlever: (client, ack, seq, params) => ({
|
||||
cmd: "system_getdatabundlever",
|
||||
body: client.bon.encode({
|
||||
isAudit: false,
|
||||
...params
|
||||
}),
|
||||
ack: ack || 0,
|
||||
seq: seq || 0,
|
||||
rtt: generateRandomNumber(0, 500),
|
||||
code: 0,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 购买金币
|
||||
system_buygold: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
buyNum: 1,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "system_buygold",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 分享回调
|
||||
system_mysharecallback: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
type: 3,
|
||||
isSkipShareCard: true,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "system_mysharecallback",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 好友批处理
|
||||
friend_batch: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
friendId: 0,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "friend_batch",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 英雄招募
|
||||
hero_recruit: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
byClub: false,
|
||||
recruitNumber: 1,
|
||||
recruitType: 3,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "hero_recruit",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 领取挂机奖励
|
||||
system_claimhangupreward: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "system_claimhangupreward",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 开启宝箱
|
||||
item_openbox: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
itemId: 2001,
|
||||
number: 10,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "item_openbox",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 竞技场相关命令
|
||||
arena_startarea: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({...params}),
|
||||
c: undefined,
|
||||
cmd: "arena_startarea",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
arena_getareatarget: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
refresh: false,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "arena_getareatarget",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
fight_startareaarena: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
targetId: 530479307,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "fight_startareaarena",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
arena_getarearank: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
rankType: 0,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "arena_getarearank",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
// 商店相关
|
||||
store_goodslist: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
storeId: 1,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "store_goodslist",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
store_buy: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({
|
||||
goodsId: 1,
|
||||
...params
|
||||
}),
|
||||
c: undefined,
|
||||
cmd: "store_buy",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
}),
|
||||
|
||||
store_refresh: (client, ack, seq, params) => ({
|
||||
ack: ack,
|
||||
body: client.bon.encode({...params}),
|
||||
c: undefined,
|
||||
cmd: "store_refresh",
|
||||
hint: undefined,
|
||||
seq: seq,
|
||||
time: Date.now()
|
||||
})
|
||||
};
|
||||
|
||||
// 游戏逻辑处理函数 (从原始混淆代码中提取的核心逻辑)
|
||||
function processGameLogic(client) {
|
||||
const app = createApp();
|
||||
const state = useState();
|
||||
const { message } = useI18n(["message", "dialog"]);
|
||||
|
||||
// 处理问答逻辑
|
||||
const handleQuestionsLogic = (responseData) => {
|
||||
const questionList = responseData.body.questionList;
|
||||
let hasMatch = false;
|
||||
const config = useState();
|
||||
|
||||
// 遍历问题列表寻找匹配
|
||||
for (let i = 0; i < questionList.length; i++) {
|
||||
const question = questionList[i];
|
||||
//todo
|
||||
// 这里应该有问题匹配逻辑,但在原代码中被混淆了
|
||||
// 原始逻辑涉及某个答案数组 v,可能需要根据实际需求补充
|
||||
}
|
||||
|
||||
return hasMatch;
|
||||
};
|
||||
|
||||
return {
|
||||
handleQuestionsLogic,
|
||||
// 其他游戏逻辑函数可以在这里添加
|
||||
};
|
||||
}
|
||||
|
||||
// Base64 编解码工具 (从原始代码第1部分提取)
|
||||
const base64Utils = {
|
||||
// 字节长度计算
|
||||
byteLength: function (str) {
|
||||
const parsed = this.parseBase64(str);
|
||||
const validLength = parsed[0];
|
||||
const paddingLength = parsed[1];
|
||||
return validLength;
|
||||
},
|
||||
|
||||
// 转换为字节数组
|
||||
toByteArray: function (str) {
|
||||
const parsed = this.parseBase64(str);
|
||||
const validLength = parsed[0];
|
||||
const paddingLength = parsed[1];
|
||||
const result = new Uint8Array(this.calculateLength(validLength, paddingLength, str.length));
|
||||
|
||||
// 解码逻辑
|
||||
// ... 这里应该包含完整的Base64解码实现
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// 从字节数组转换
|
||||
fromByteArray: function (uint8Array) {
|
||||
const length = uint8Array.length;
|
||||
const remainder = length % 3;
|
||||
const chunks = [];
|
||||
const maxChunkLength = 16383;
|
||||
|
||||
// 处理主要部分
|
||||
for (let i = 0; i < length - remainder; i += maxChunkLength) {
|
||||
const end = i + maxChunkLength > length - remainder ? length - remainder : i + maxChunkLength;
|
||||
chunks.push(this.encodeChunk(uint8Array, i, end));
|
||||
}
|
||||
|
||||
// 处理剩余字节
|
||||
if (remainder === 1) {
|
||||
const byte = uint8Array[length - 1];
|
||||
chunks.push(this.chars[byte >> 2] + this.chars[byte << 4 & 63] + '==');
|
||||
} else if (remainder === 2) {
|
||||
const byte1 = uint8Array[length - 2];
|
||||
const byte2 = uint8Array[length - 1];
|
||||
chunks.push(
|
||||
this.chars[byte1 >> 2] +
|
||||
this.chars[byte1 << 4 & 63 | byte2 >> 4] +
|
||||
this.chars[byte2 << 2 & 63] +
|
||||
'='
|
||||
);
|
||||
}
|
||||
|
||||
return chunks.join('');
|
||||
},
|
||||
|
||||
// Base64字符表
|
||||
chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
|
||||
|
||||
// 辅助函数
|
||||
parseBase64: function (str) {
|
||||
const length = str.length;
|
||||
let paddingIndex = str.indexOf('=');
|
||||
if (paddingIndex === -1) paddingIndex = length;
|
||||
|
||||
const validLength = paddingIndex;
|
||||
const paddingLength = paddingIndex === length ? 0 : 4 - (paddingIndex % 4);
|
||||
|
||||
return [validLength, paddingLength];
|
||||
},
|
||||
|
||||
calculateLength: function (validLength, paddingLength, totalLength) {
|
||||
return Math.floor((validLength + paddingLength) * 3 / 4);
|
||||
},
|
||||
|
||||
encodeChunk: function (uint8Array, start, end) {
|
||||
const chars = this.chars;
|
||||
const result = [];
|
||||
|
||||
for (let i = start; i < end; i += 3) {
|
||||
const byte1 = uint8Array[i];
|
||||
const byte2 = i + 1 < end ? uint8Array[i + 1] : 0;
|
||||
const byte3 = i + 2 < end ? uint8Array[i + 2] : 0;
|
||||
|
||||
const triplet = (byte1 << 16) + (byte2 << 8) + byte3;
|
||||
|
||||
result.push(
|
||||
chars[triplet >> 18 & 63] +
|
||||
chars[triplet >> 12 & 63] +
|
||||
chars[triplet >> 6 & 63] +
|
||||
chars[triplet & 63]
|
||||
);
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
}
|
||||
};
|
||||
|
||||
// 数据存储管理 (从文件末尾部分提取)
|
||||
const createDataStore = () => {
|
||||
return {
|
||||
// 响应数据存储
|
||||
resp: {},
|
||||
|
||||
// 更新军团信息
|
||||
updateLegioninfo: function(newData) {
|
||||
const currentLegionData = this.resp.legion_getinforesp;
|
||||
|
||||
if (currentLegionData && currentLegionData.data) {
|
||||
this.resp.legion_getinforesp = {
|
||||
loading: false,
|
||||
data: Object.assign({}, currentLegionData.data, newData),
|
||||
cmd: "legion_getinfor"
|
||||
};
|
||||
} else {
|
||||
this.resp.legion_getinforesp = {
|
||||
loading: false,
|
||||
data: newData,
|
||||
cmd: "legion_getinfor"
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 导出的主要功能模块
|
||||
export {
|
||||
useState as createGameState, // b -> a
|
||||
formatNumber as formatGameNumber, // h -> b
|
||||
gameMessageTemplates as gameCommands, // m -> c
|
||||
processGameLogic as gameLogicHandler, // y -> d
|
||||
createDataStore as dataStoreFactory, // C -> e
|
||||
formatTime, // f
|
||||
base64Utils as encodingUtils, // E -> g
|
||||
createDataStore as storeManager, // S -> h
|
||||
delay as sleep, // g -> s
|
||||
createApp as appFactory // A -> u
|
||||
};
|
||||
438
src/utils/wsAgent.js
Normal file
438
src/utils/wsAgent.js
Normal file
@@ -0,0 +1,438 @@
|
||||
/**
|
||||
* WebSocket客户端 - 基于mirror代码的完整实现
|
||||
* 支持BON协议编解码、加密通道、心跳保活、消息队列等
|
||||
*/
|
||||
|
||||
import { g_utils } from './bonProtocol.js'
|
||||
|
||||
export class WsAgent {
|
||||
/**
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
const {
|
||||
heartbeatInterval = 2000, // 心跳间隔(ms)
|
||||
queueInterval = 50, // 发送队列轮询间隔(ms)
|
||||
heartbeatCmd = 'heart_beat', // 心跳命令
|
||||
channel = 'x', // 加密通道
|
||||
autoReconnect = true, // 自动重连
|
||||
maxReconnectAttempts = 5, // 最大重连次数
|
||||
reconnectDelay = 3000 // 重连延迟(ms)
|
||||
} = options
|
||||
|
||||
// 配置参数
|
||||
this.heartbeatInterval = heartbeatInterval
|
||||
this.queueInterval = queueInterval
|
||||
this.heartbeatCmd = heartbeatCmd
|
||||
this.channel = channel
|
||||
this.autoReconnect = autoReconnect
|
||||
this.maxReconnectAttempts = maxReconnectAttempts
|
||||
this.reconnectDelay = reconnectDelay
|
||||
|
||||
// 连接状态
|
||||
this.ws = null
|
||||
this.connected = false
|
||||
this.connecting = false
|
||||
this.reconnectAttempts = 0
|
||||
|
||||
// 协议状态
|
||||
this.ack = 0
|
||||
this.seq = 1
|
||||
|
||||
// 定时器
|
||||
this._heartbeatTimer = null
|
||||
this._queueTimer = null
|
||||
this._reconnectTimer = null
|
||||
|
||||
// 发送队列
|
||||
this.sendQueue = []
|
||||
|
||||
// Promise等待队列 respKey -> {resolve, reject, timeoutId}
|
||||
this.waitingPromises = new Map()
|
||||
|
||||
// 事件监听器
|
||||
this.onOpen = () => {}
|
||||
this.onClose = () => {}
|
||||
this.onError = () => {}
|
||||
this.onMessage = () => {}
|
||||
this.onReconnect = () => {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接WebSocket
|
||||
* @param {string} url WebSocket URL
|
||||
* @param {Object} connectionParams 连接参数
|
||||
*/
|
||||
connect(url, connectionParams = {}) {
|
||||
if (this.connecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
||||
console.warn('WebSocket已连接或正在连接中')
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.connecting = true
|
||||
console.log(`🔗 连接WebSocket: ${url}`)
|
||||
|
||||
this.ws = new WebSocket(url)
|
||||
this.ws.binaryType = 'arraybuffer'
|
||||
|
||||
// 连接打开
|
||||
this.ws.onopen = () => {
|
||||
this.connecting = false
|
||||
this.connected = true
|
||||
this.reconnectAttempts = 0
|
||||
|
||||
console.log('✅ WebSocket连接已建立')
|
||||
|
||||
// 重置协议状态
|
||||
this.seq = 1
|
||||
|
||||
// 启动心跳和队列处理
|
||||
this._startHeartbeat()
|
||||
this._startQueueProcessor()
|
||||
|
||||
this.onOpen()
|
||||
resolve()
|
||||
}
|
||||
|
||||
// 消息接收
|
||||
this.ws.onmessage = (event) => {
|
||||
this._handleMessage(event.data)
|
||||
}
|
||||
|
||||
// 连接关闭
|
||||
this.ws.onclose = (event) => {
|
||||
this.connecting = false
|
||||
this.connected = false
|
||||
this._cleanup()
|
||||
|
||||
console.log(`🔌 WebSocket连接已关闭: ${event.code} ${event.reason}`)
|
||||
|
||||
this.onClose(event)
|
||||
|
||||
// 自动重连
|
||||
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this._scheduleReconnect(url, connectionParams)
|
||||
}
|
||||
}
|
||||
|
||||
// 连接错误
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('❌ WebSocket错误:', error)
|
||||
this.onError(error)
|
||||
|
||||
if (this.connecting) {
|
||||
this.connecting = false
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.connecting = false
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
* @param {number} code 关闭码
|
||||
* @param {string} reason 关闭原因
|
||||
*/
|
||||
close(code = 1000, reason = 'normal') {
|
||||
this.autoReconnect = false
|
||||
if (this.ws) {
|
||||
this.ws.close(code, reason)
|
||||
}
|
||||
this._cleanup()
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* @param {Object|Array} payload 消息载荷
|
||||
*/
|
||||
send(payload) {
|
||||
if (Array.isArray(payload)) {
|
||||
this.sendQueue.push(...payload)
|
||||
} else {
|
||||
this.sendQueue.push(payload)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息并等待响应
|
||||
* @param {Object} options 请求选项
|
||||
* @returns {Promise} 响应Promise
|
||||
*/
|
||||
sendWithPromise(options) {
|
||||
const { cmd, body = {}, respKey, timeout = 8000 } = options
|
||||
const responseKey = respKey || `${cmd}resp`
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 设置超时
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.waitingPromises.delete(responseKey)
|
||||
reject(new Error(`请求超时: ${cmd}`))
|
||||
}, timeout)
|
||||
|
||||
// 注册Promise
|
||||
this.waitingPromises.set(responseKey, {
|
||||
resolve,
|
||||
reject,
|
||||
timeoutId
|
||||
})
|
||||
|
||||
// 发送消息
|
||||
this.send({ cmd, body, respKey: responseKey })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
* @private
|
||||
*/
|
||||
_handleMessage(data) {
|
||||
try {
|
||||
// 使用g_utils解密和解码消息
|
||||
const message = g_utils.parse(data, this.channel)
|
||||
|
||||
if (!message) {
|
||||
console.warn('消息解析失败')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('📨 收到消息:', message)
|
||||
|
||||
// 更新ack
|
||||
if (message.seq) {
|
||||
this.ack = message.seq
|
||||
}
|
||||
|
||||
// 检查是否有等待的Promise
|
||||
const cmd = message.cmd || message.c
|
||||
const respKey = message.respKey || cmd
|
||||
|
||||
if (respKey && this.waitingPromises.has(respKey)) {
|
||||
const { resolve, timeoutId } = this.waitingPromises.get(respKey)
|
||||
clearTimeout(timeoutId)
|
||||
this.waitingPromises.delete(respKey)
|
||||
resolve(message)
|
||||
return
|
||||
}
|
||||
|
||||
// 派发给普通消息处理器
|
||||
this.onMessage(message)
|
||||
|
||||
} catch (error) {
|
||||
console.error('消息处理失败:', error)
|
||||
this.onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳
|
||||
* @private
|
||||
*/
|
||||
_startHeartbeat() {
|
||||
this._stopHeartbeat()
|
||||
|
||||
if (!this.heartbeatInterval) return
|
||||
|
||||
this._heartbeatTimer = setInterval(() => {
|
||||
if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
|
||||
this._sendHeartbeat()
|
||||
}
|
||||
}, this.heartbeatInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳
|
||||
* @private
|
||||
*/
|
||||
_stopHeartbeat() {
|
||||
if (this._heartbeatTimer) {
|
||||
clearInterval(this._heartbeatTimer)
|
||||
this._heartbeatTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳消息
|
||||
* @private
|
||||
*/
|
||||
_sendHeartbeat() {
|
||||
const heartbeatMsg = {
|
||||
ack: this.ack,
|
||||
body: undefined,
|
||||
c: undefined,
|
||||
cmd: '_sys/ack',
|
||||
hint: undefined,
|
||||
seq: 0, // 心跳消息seq为0
|
||||
time: Date.now()
|
||||
}
|
||||
|
||||
this._rawSend(heartbeatMsg)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动队列处理器
|
||||
* @private
|
||||
*/
|
||||
_startQueueProcessor() {
|
||||
this._stopQueueProcessor()
|
||||
this._queueTimer = setInterval(() => {
|
||||
this._processQueue()
|
||||
}, this.queueInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止队列处理器
|
||||
* @private
|
||||
*/
|
||||
_stopQueueProcessor() {
|
||||
if (this._queueTimer) {
|
||||
clearInterval(this._queueTimer)
|
||||
this._queueTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理发送队列
|
||||
* @private
|
||||
*/
|
||||
_processQueue() {
|
||||
if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.sendQueue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const item = this.sendQueue.shift()
|
||||
const packet = this._buildPacket(item)
|
||||
this._rawSend(packet)
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建数据包
|
||||
* @private
|
||||
*/
|
||||
_buildPacket(payload) {
|
||||
const { cmd, body = {}, respKey } = payload
|
||||
|
||||
// 生成随机RTT (0-500ms)
|
||||
const rtt = Math.floor(Math.random() * 500)
|
||||
|
||||
const packet = {
|
||||
ack: this.ack,
|
||||
seq: cmd === this.heartbeatCmd ? 0 : this.seq++,
|
||||
time: Date.now(),
|
||||
cmd,
|
||||
body,
|
||||
respKey,
|
||||
rtt,
|
||||
code: 0
|
||||
}
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
/**
|
||||
* 原始发送数据
|
||||
* @private
|
||||
*/
|
||||
_rawSend(packet) {
|
||||
try {
|
||||
// 使用g_utils编码和加密
|
||||
const data = g_utils.encode(packet, this.channel)
|
||||
this.ws.send(data)
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
this.onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计划重连
|
||||
* @private
|
||||
*/
|
||||
_scheduleReconnect(url, connectionParams) {
|
||||
if (this._reconnectTimer) {
|
||||
clearTimeout(this._reconnectTimer)
|
||||
}
|
||||
|
||||
this.reconnectAttempts++
|
||||
console.log(`🔄 计划重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts}) 延迟: ${this.reconnectDelay}ms`)
|
||||
|
||||
this._reconnectTimer = setTimeout(() => {
|
||||
console.log(`🔄 开始第${this.reconnectAttempts}次重连...`)
|
||||
this.onReconnect(this.reconnectAttempts)
|
||||
this.connect(url, connectionParams).catch(error => {
|
||||
console.error('重连失败:', error)
|
||||
})
|
||||
}, this.reconnectDelay)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
* @private
|
||||
*/
|
||||
_cleanup() {
|
||||
this._stopHeartbeat()
|
||||
this._stopQueueProcessor()
|
||||
|
||||
if (this._reconnectTimer) {
|
||||
clearTimeout(this._reconnectTimer)
|
||||
this._reconnectTimer = null
|
||||
}
|
||||
|
||||
// 清理等待的Promise
|
||||
for (const [key, { reject, timeoutId }] of this.waitingPromises) {
|
||||
clearTimeout(timeoutId)
|
||||
reject(new Error('连接已关闭'))
|
||||
}
|
||||
this.waitingPromises.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
connected: this.connected,
|
||||
connecting: this.connecting,
|
||||
readyState: this.ws?.readyState,
|
||||
ack: this.ack,
|
||||
seq: this.seq,
|
||||
queueLength: this.sendQueue.length,
|
||||
waitingPromises: this.waitingPromises.size,
|
||||
reconnectAttempts: this.reconnectAttempts
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建WebSocket URL
|
||||
* @static
|
||||
*/
|
||||
static buildUrl(baseUrl, params = {}) {
|
||||
const url = new URL(baseUrl)
|
||||
|
||||
// 添加连接参数到p参数
|
||||
if (params.p && typeof params.p === 'object') {
|
||||
url.searchParams.set('p', JSON.stringify(params.p))
|
||||
}
|
||||
|
||||
// 添加其他参数
|
||||
Object.keys(params).forEach(key => {
|
||||
if (key !== 'p' && params[key] !== undefined) {
|
||||
url.searchParams.set(key, params[key])
|
||||
}
|
||||
})
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export default WsAgent
|
||||
537
src/utils/xyzwWebSocket.js
Normal file
537
src/utils/xyzwWebSocket.js
Normal file
@@ -0,0 +1,537 @@
|
||||
/**
|
||||
* XYZW WebSocket 客户端
|
||||
* 基于 readable-xyzw-ws.js 重构,适配本项目架构
|
||||
*/
|
||||
|
||||
import { bonProtocol, g_utils } from './bonProtocol.js'
|
||||
|
||||
/** 生成 [min,max] 的随机整数 */
|
||||
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
|
||||
|
||||
/** Promise 版 sleep */
|
||||
const sleep = (ms) => new Promise((res) => setTimeout(res, ms))
|
||||
|
||||
/**
|
||||
* 命令注册器:保存每个 cmd 的默认体,发送时与 params 合并
|
||||
*/
|
||||
export class CommandRegistry {
|
||||
constructor(encoder, enc) {
|
||||
this.encoder = encoder
|
||||
this.enc = enc
|
||||
this.commands = new Map()
|
||||
}
|
||||
|
||||
/** 注册命令 */
|
||||
register(cmd, defaultBody = {}) {
|
||||
this.commands.set(cmd, (ack = 0, seq = 0, params = {}) => ({
|
||||
cmd,
|
||||
ack,
|
||||
seq,
|
||||
code: 0,
|
||||
rtt: randInt(0, 500),
|
||||
time: Date.now(),
|
||||
body: this.encoder?.bon?.encode
|
||||
? this.encoder.bon.encode({ ...defaultBody, ...params })
|
||||
: undefined,
|
||||
c: undefined,
|
||||
hint: undefined,
|
||||
}))
|
||||
return this
|
||||
}
|
||||
|
||||
/** 特例:系统心跳的 ack 用的是 "_sys/ack" */
|
||||
registerHeartbeat() {
|
||||
this.commands.set("heart_beat", (ack, seq) => ({
|
||||
cmd: "_sys/ack",
|
||||
ack,
|
||||
seq,
|
||||
time: Date.now(),
|
||||
body: undefined,
|
||||
c: undefined,
|
||||
hint: undefined,
|
||||
}))
|
||||
return this
|
||||
}
|
||||
|
||||
/** 生成最终可发送的二进制 */
|
||||
encodePacket(raw) {
|
||||
if (this.encoder?.encode && this.enc) {
|
||||
// 使用加密编码
|
||||
return this.encoder.encode(raw, this.enc)
|
||||
} else {
|
||||
// 降级到JSON字符串
|
||||
return JSON.stringify(raw)
|
||||
}
|
||||
}
|
||||
|
||||
/** 构造报文 */
|
||||
build(cmd, ack, seq, params) {
|
||||
const fn = this.commands.get(cmd)
|
||||
if (!fn) throw new Error(`Unknown cmd: ${cmd}`)
|
||||
return fn(ack, seq, params)
|
||||
}
|
||||
}
|
||||
|
||||
/** 预注册游戏命令 */
|
||||
export function registerDefaultCommands(reg) {
|
||||
return reg.registerHeartbeat()
|
||||
// 角色/系统
|
||||
.register("role_getroleinfo", {
|
||||
clientVersion: "1.65.3-wx",
|
||||
inviteUid: 0,
|
||||
platform: "hortor",
|
||||
platformExt: "mix",
|
||||
scene: "",
|
||||
})
|
||||
.register("system_getdatabundlever", { isAudit: false })
|
||||
.register("system_buygold", { buyNum: 1 })
|
||||
.register("system_claimhangupreward")
|
||||
.register("system_signinreward")
|
||||
|
||||
// 任务相关
|
||||
.register("task_claimdailypoint", { taskId: 1 })
|
||||
.register("task_claimdailyreward", { rewardId: 0 })
|
||||
.register("task_claimweekreward", { rewardId: 0 })
|
||||
|
||||
// 好友/招募
|
||||
.register("friend_batch", { friendId: 0 })
|
||||
.register("hero_recruit", { byClub: false, recruitNumber: 1, recruitType: 3 })
|
||||
.register("item_openbox", { itemId: 2001, number: 10 })
|
||||
|
||||
// 竞技场
|
||||
.register("arena_startarea")
|
||||
.register("arena_getareatarget", { refresh: false })
|
||||
.register("fight_startareaarena", { targetId: 530479307 })
|
||||
.register("arena_getarearank", { rankType: 0 })
|
||||
|
||||
// 商店
|
||||
.register("store_goodslist", { storeId: 1 })
|
||||
.register("store_buy", { goodsId: 1 })
|
||||
.register("store_refresh", { storeId: 1 })
|
||||
|
||||
// 军团
|
||||
.register("legion_getinfo")
|
||||
.register("legion_signin")
|
||||
.register("legion_getwarrank")
|
||||
|
||||
// 邮件
|
||||
.register("mail_getlist", { category: [0, 4, 5], lastId: 0, size: 60 })
|
||||
.register("mail_claimallattachment", { category: 0 })
|
||||
|
||||
// 学习问答
|
||||
.register("study_startgame")
|
||||
.register("study_answer")
|
||||
.register("study_claimreward", { rewardId: 1 })
|
||||
|
||||
// 战斗相关
|
||||
.register("fight_starttower")
|
||||
.register("fight_startboss")
|
||||
.register("fight_startlegionboss")
|
||||
.register("fight_startdungeon")
|
||||
.register("fight_startpvp")
|
||||
|
||||
// 瓶子机器人
|
||||
.register("bottlehelper_claim")
|
||||
.register("bottlehelper_start", { bottleType: -1 })
|
||||
.register("bottlehelper_stop", { bottleType: -1 })
|
||||
|
||||
// 军团匹配和签到
|
||||
.register("legionmatch_rolesignup")
|
||||
.register("legion_signin")
|
||||
|
||||
// 神器抽奖
|
||||
.register("artifact_lottery", { lotteryNumber: 1, newFree: true, type: 1 })
|
||||
|
||||
// 爬塔相关
|
||||
.register("tower_getinfo")
|
||||
.register("tower_claimreward")
|
||||
|
||||
// 队伍相关
|
||||
.register("presetteam_getteam")
|
||||
.register("presetteam_setteam")
|
||||
.register("presetteam_saveteam")
|
||||
.register("role_gettargetteam")
|
||||
|
||||
// 排名相关
|
||||
.register("rank_getroleinfo")
|
||||
|
||||
// 梦魇相关
|
||||
.register("nightmare_getroleinfo")
|
||||
}
|
||||
|
||||
/**
|
||||
* XYZW WebSocket 客户端
|
||||
*/
|
||||
export class XyzwWebSocketClient {
|
||||
constructor({ url, utils, heartbeatMs = 5000 }) {
|
||||
this.url = url
|
||||
this.utils = utils || g_utils
|
||||
this.enc = this.utils?.getEnc ? this.utils.getEnc("auto") : undefined
|
||||
|
||||
this.socket = null
|
||||
this.ack = 1
|
||||
this.seq = 0
|
||||
this.sendQueue = []
|
||||
this.sendQueueTimer = null
|
||||
this.heartbeatTimer = null
|
||||
this.heartbeatInterval = heartbeatMs
|
||||
|
||||
this.dialogStatus = false
|
||||
this.messageListener = null
|
||||
this.showMsg = false
|
||||
this.connected = false
|
||||
|
||||
this.promises = Object.create(null)
|
||||
this.registry = registerDefaultCommands(new CommandRegistry(this.utils, this.enc))
|
||||
|
||||
console.log('🔧 WebSocket客户端初始化:', {
|
||||
url: this.url,
|
||||
hasUtils: !!this.utils,
|
||||
hasEnc: !!this.enc,
|
||||
hasEncoder: !!this.utils?.encode
|
||||
})
|
||||
|
||||
// 状态回调
|
||||
this.onConnect = null
|
||||
this.onDisconnect = null
|
||||
this.onError = null
|
||||
}
|
||||
|
||||
/** 初始化连接 */
|
||||
init() {
|
||||
console.log(`🔗 连接 WebSocket: ${this.url}`)
|
||||
|
||||
this.socket = new WebSocket(this.url)
|
||||
|
||||
this.socket.onopen = () => {
|
||||
console.log(`✅ WebSocket 连接成功`)
|
||||
this.connected = true
|
||||
console.log(`🔄 启动心跳机制,间隔: ${this.heartbeatInterval}ms`)
|
||||
this._setupHeartbeat()
|
||||
console.log(`🔄 启动消息队列处理`)
|
||||
this._processQueueLoop()
|
||||
if (this.onConnect) this.onConnect()
|
||||
}
|
||||
|
||||
this.socket.onmessage = (evt) => {
|
||||
try {
|
||||
let packet
|
||||
if (typeof evt.data === "string") {
|
||||
packet = JSON.parse(evt.data)
|
||||
} else if (evt.data instanceof ArrayBuffer) {
|
||||
// 二进制数据需要自动检测并解码
|
||||
packet = this.utils?.parse ? this.utils.parse(evt.data, "auto") : evt.data
|
||||
} else if (evt.data instanceof Blob) {
|
||||
// 处理Blob数据
|
||||
console.log('📦 收到Blob数据, 大小:', evt.data.size)
|
||||
evt.data.arrayBuffer().then(buffer => {
|
||||
try {
|
||||
packet = this.utils?.parse ? this.utils.parse(buffer, "auto") : buffer
|
||||
console.log('📦 Blob解析结果:', packet)
|
||||
|
||||
// 处理消息体解码(ProtoMsg会自动解码)
|
||||
if (packet instanceof Object && packet.rawData !== undefined) {
|
||||
console.log('✅ ProtoMsg消息,使用rawData:', packet.rawData)
|
||||
} else if (packet.body && packet.body instanceof Uint8Array) {
|
||||
try {
|
||||
if (this.utils && this.utils.bon && this.utils.bon.decode) {
|
||||
const decodedBody = this.utils.bon.decode(packet.body)
|
||||
console.log('✅ 手动解码消息体成功:', decodedBody)
|
||||
// 不修改packet.body,而是创建一个新的属性存储解码后的数据
|
||||
packet.decodedBody = decodedBody
|
||||
} else {
|
||||
console.warn('⚠️ BON解码器不可用:', this.utils)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('❌ 消息体解码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.showMsg) {
|
||||
console.log(`📨 收到消息(Blob解析后):`, packet)
|
||||
}
|
||||
|
||||
// 回调处理
|
||||
if (this.messageListener) {
|
||||
this.messageListener(packet)
|
||||
}
|
||||
|
||||
// Promise 响应处理
|
||||
this._handlePromiseResponse(packet)
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Blob解析失败:', error)
|
||||
}
|
||||
})
|
||||
return // 异步处理,直接返回
|
||||
} else {
|
||||
console.warn('⚠️ 未知数据类型:', typeof evt.data, evt.data)
|
||||
packet = evt.data
|
||||
}
|
||||
|
||||
if (this.showMsg) {
|
||||
console.log(`📨 收到消息:`, packet)
|
||||
}
|
||||
|
||||
// 处理消息体解码(ProtoMsg会自动解码)
|
||||
if (packet instanceof Object && packet.rawData !== undefined) {
|
||||
console.log('✅ ProtoMsg消息,使用rawData:', packet.rawData)
|
||||
} else if (packet.body && packet.body instanceof Uint8Array) {
|
||||
try {
|
||||
if (this.utils && this.utils.bon && this.utils.bon.decode) {
|
||||
const decodedBody = this.utils.bon.decode(packet.body)
|
||||
console.log('✅ 手动解码消息体成功:', decodedBody)
|
||||
// 不修改packet.body,而是创建一个新的属性存储解码后的数据
|
||||
packet.decodedBody = decodedBody
|
||||
} else {
|
||||
console.warn('⚠️ BON解码器不可用:', this.utils)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('❌ 消息体解码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 回调处理
|
||||
if (this.messageListener) {
|
||||
this.messageListener(packet)
|
||||
}
|
||||
|
||||
// Promise 响应处理
|
||||
this._handlePromiseResponse(packet)
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 消息处理失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
this.socket.onclose = (evt) => {
|
||||
console.log(`🔌 WebSocket 连接关闭:`, evt.code, evt.reason)
|
||||
console.log(`🔍 关闭详情:`, {
|
||||
code: evt.code,
|
||||
reason: evt.reason || '未提供原因',
|
||||
wasClean: evt.wasClean,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
this.connected = false
|
||||
this._clearTimers()
|
||||
if (this.onDisconnect) this.onDisconnect(evt)
|
||||
}
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
console.error(`❌ WebSocket 错误:`, error)
|
||||
this.connected = false
|
||||
this._clearTimers()
|
||||
if (this.onError) this.onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 注册消息回调 */
|
||||
setMessageListener(fn) {
|
||||
this.messageListener = fn
|
||||
}
|
||||
|
||||
/** 控制台消息开关 */
|
||||
setShowMsg(val) {
|
||||
this.showMsg = !!val
|
||||
}
|
||||
|
||||
/** 重连 */
|
||||
reconnect() {
|
||||
this.disconnect()
|
||||
setTimeout(() => this.init(), 1000)
|
||||
}
|
||||
|
||||
/** 断开连接 */
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close()
|
||||
this.socket = null
|
||||
}
|
||||
this.connected = false
|
||||
this._clearTimers()
|
||||
}
|
||||
|
||||
/** 发送消息 */
|
||||
send(cmd, params = {}, options = {}) {
|
||||
if (!this.connected) {
|
||||
console.warn(`⚠️ WebSocket 未连接,消息已入队: ${cmd}`)
|
||||
if (!this.dialogStatus) {
|
||||
this.dialogStatus = true
|
||||
this.reconnect()
|
||||
setTimeout(() => { this.dialogStatus = false }, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const task = {
|
||||
cmd,
|
||||
params,
|
||||
respKey: options.respKey || cmd,
|
||||
sleep: options.sleep || 0,
|
||||
onSent: options.onSent
|
||||
}
|
||||
|
||||
this.sendQueue.push(task)
|
||||
return task
|
||||
}
|
||||
|
||||
/** Promise 版发送 */
|
||||
sendWithPromise(cmd, params = {}, timeoutMs = 5000) {
|
||||
const respKey = `${cmd}_${this.seq + 1}`
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.connected && !this.socket) {
|
||||
return reject(new Error("WebSocket 连接已关闭"))
|
||||
}
|
||||
|
||||
// 设置 Promise 状态
|
||||
this.promises[respKey] = { resolve, reject }
|
||||
|
||||
// 超时处理
|
||||
const timer = setTimeout(() => {
|
||||
delete this.promises[respKey]
|
||||
reject(new Error(`请求超时: ${cmd} (${timeoutMs}ms)`))
|
||||
}, timeoutMs)
|
||||
|
||||
// 发送消息
|
||||
this.send(cmd, params, {
|
||||
respKey,
|
||||
onSent: () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 发送心跳 */
|
||||
sendHeartbeat() {
|
||||
console.log('💓 发送心跳消息')
|
||||
this.send("heart_beat", {}, { respKey: "_sys/ack" })
|
||||
}
|
||||
|
||||
/** 获取角色信息 */
|
||||
getRoleInfo(params = {}) {
|
||||
return this.sendWithPromise("role_getroleinfo", params)
|
||||
}
|
||||
|
||||
/** 获取数据版本 */
|
||||
getDataBundleVersion(params = {}) {
|
||||
return this.sendWithPromise("system_getdatabundlever", params)
|
||||
}
|
||||
|
||||
/** 签到 */
|
||||
signIn() {
|
||||
return this.sendWithPromise("system_signinreward")
|
||||
}
|
||||
|
||||
/** 领取日常任务奖励 */
|
||||
claimDailyReward(rewardId = 0) {
|
||||
return this.sendWithPromise("task_claimdailyreward", { rewardId })
|
||||
}
|
||||
|
||||
/** =============== 内部方法 =============== */
|
||||
|
||||
/** 设置心跳 */
|
||||
_setupHeartbeat() {
|
||||
// 延迟3秒后开始发送第一个心跳,避免连接刚建立就发送
|
||||
setTimeout(() => {
|
||||
if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
|
||||
console.log('💓 开始发送首次心跳')
|
||||
this.sendHeartbeat()
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
// 设置定期心跳
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
|
||||
this.sendHeartbeat()
|
||||
} else {
|
||||
console.log('⚠️ 心跳检查失败: 连接状态异常')
|
||||
}
|
||||
}, this.heartbeatInterval)
|
||||
}
|
||||
|
||||
/** 队列处理循环 */
|
||||
_processQueueLoop() {
|
||||
if (this.sendQueueTimer) clearInterval(this.sendQueueTimer)
|
||||
|
||||
this.sendQueueTimer = setInterval(async () => {
|
||||
if (!this.sendQueue.length) return
|
||||
if (!this.connected || this.socket?.readyState !== WebSocket.OPEN) return
|
||||
|
||||
const task = this.sendQueue.shift()
|
||||
if (!task) return
|
||||
|
||||
try {
|
||||
// 构建报文
|
||||
const raw = this.registry.build(task.cmd, this.ack, this.seq, task.params)
|
||||
if (task.cmd !== "heart_beat") this.seq++
|
||||
|
||||
// 编码并发送
|
||||
const bin = this.registry.encodePacket(raw)
|
||||
this.socket?.send(bin)
|
||||
|
||||
if (this.showMsg || task.cmd === "heart_beat") {
|
||||
console.log(`📤 发送消息: ${task.cmd}`, task.params)
|
||||
if (this.showMsg) {
|
||||
console.log(`🔐 原始数据:`, raw)
|
||||
console.log(`🚀 编码后数据:`, bin)
|
||||
console.log(`🔧 编码类型:`, typeof bin, bin instanceof Uint8Array ? '✅ Uint8Array (加密)' : '❌ String (明文)')
|
||||
if (bin instanceof Uint8Array && bin.length > 0) {
|
||||
console.log(`🎯 加密验证: 前8字节 [${Array.from(bin.slice(0, 8)).join(', ')}]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发发送回调
|
||||
if (task.onSent) {
|
||||
try {
|
||||
task.onSent(task.respKey, task.cmd)
|
||||
} catch (error) {
|
||||
console.warn('发送回调执行失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 可选延时
|
||||
if (task.sleep) await sleep(task.sleep)
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 发送消息失败: ${task.cmd}`, error)
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
/** 处理 Promise 响应 */
|
||||
_handlePromiseResponse(packet) {
|
||||
const cmd = packet.cmd
|
||||
if (!cmd) return
|
||||
|
||||
// 查找对应的 Promise
|
||||
for (const [key, promise] of Object.entries(this.promises)) {
|
||||
if (key.startsWith(cmd) || cmd === key) {
|
||||
delete this.promises[key]
|
||||
|
||||
if (packet.code === 0 || packet.code === undefined) {
|
||||
promise.resolve(packet.body || packet)
|
||||
} else {
|
||||
promise.reject(new Error(`服务器错误: ${packet.code} - ${packet.hint || '未知错误'}`))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 清理定时器 */
|
||||
_clearTimers() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
if (this.sendQueueTimer) {
|
||||
clearInterval(this.sendQueueTimer)
|
||||
this.sendQueueTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 默认导出 */
|
||||
export default XyzwWebSocketClient
|
||||
759
src/views/DailyTasks.vue
Normal file
759
src/views/DailyTasks.vue
Normal file
@@ -0,0 +1,759 @@
|
||||
<template>
|
||||
<div class="daily-tasks-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<h1 class="page-title">
|
||||
日常任务
|
||||
</h1>
|
||||
<p class="page-subtitle">
|
||||
管理和执行您的日常游戏任务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="isRefreshing"
|
||||
@click="refreshTasks"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<Refresh />
|
||||
</n-icon>
|
||||
</template>
|
||||
刷新任务
|
||||
</n-button>
|
||||
|
||||
<n-dropdown
|
||||
:options="bulkActionOptions"
|
||||
@select="handleBulkAction"
|
||||
>
|
||||
<n-button size="large">
|
||||
批量操作
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ChevronDown />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色选择器 -->
|
||||
<div class="role-selector-section">
|
||||
<div class="container">
|
||||
<div class="role-selector">
|
||||
<span class="selector-label">选择角色:</span>
|
||||
<n-select
|
||||
v-model:value="selectedRoleId"
|
||||
:options="roleOptions"
|
||||
placeholder="请选择游戏角色"
|
||||
style="min-width: 200px"
|
||||
@update:value="onRoleChange"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="selectedRole"
|
||||
class="role-stats"
|
||||
>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">总任务:</span>
|
||||
<span class="stat-value">{{ taskStats.total }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">已完成:</span>
|
||||
<span class="stat-value">{{ taskStats.completed }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">进度:</span>
|
||||
<span class="stat-value">{{ taskStats.percentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务筛选 -->
|
||||
<div class="filter-section">
|
||||
<div class="container">
|
||||
<div class="filter-bar">
|
||||
<n-radio-group
|
||||
v-model:value="currentFilter"
|
||||
@update:value="onFilterChange"
|
||||
>
|
||||
<n-radio-button value="all">
|
||||
全部任务
|
||||
</n-radio-button>
|
||||
<n-radio-button value="pending">
|
||||
待完成
|
||||
</n-radio-button>
|
||||
<n-radio-button value="completed">
|
||||
已完成
|
||||
</n-radio-button>
|
||||
<n-radio-button value="auto">
|
||||
自动执行
|
||||
</n-radio-button>
|
||||
</n-radio-group>
|
||||
|
||||
<div class="search-box">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索任务..."
|
||||
clearable
|
||||
@update:value="onSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<Search />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="tasks-section">
|
||||
<div class="container">
|
||||
<div
|
||||
v-if="filteredTasks.length"
|
||||
class="tasks-grid"
|
||||
>
|
||||
<DailyTaskCard
|
||||
v-for="task in filteredTasks"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
@execute="executeTask"
|
||||
@toggle-status="toggleTaskStatus"
|
||||
@update:task="updateTask"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-else-if="!isLoading"
|
||||
class="empty-state"
|
||||
>
|
||||
<n-empty
|
||||
description="暂无任务数据"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<Cube />
|
||||
</n-icon>
|
||||
</template>
|
||||
<template #extra>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="refreshTasks"
|
||||
>
|
||||
刷新任务
|
||||
</n-button>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="loading-state"
|
||||
>
|
||||
<n-spin size="large">
|
||||
<template #description>
|
||||
正在加载任务数据...
|
||||
</template>
|
||||
</n-spin>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage, useDialog } from 'naive-ui'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import DailyTaskCard from '@/components/DailyTaskCard.vue'
|
||||
import {
|
||||
Refresh,
|
||||
ChevronDown,
|
||||
Search,
|
||||
Cube
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 响应式数据
|
||||
const isLoading = ref(false)
|
||||
const isRefreshing = ref(false)
|
||||
const selectedRoleId = ref(null)
|
||||
const currentFilter = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
const tasks = ref([])
|
||||
|
||||
// 计算属性
|
||||
const selectedRole = computed(() => {
|
||||
return gameRolesStore.gameRoles.find(role => role.id === selectedRoleId.value)
|
||||
})
|
||||
|
||||
const roleOptions = computed(() => {
|
||||
return gameRolesStore.gameRoles.map(role => ({
|
||||
label: `${role.name} (${role.server})`,
|
||||
value: role.id
|
||||
}))
|
||||
})
|
||||
|
||||
const taskStats = computed(() => {
|
||||
const total = tasks.value.length
|
||||
const completed = tasks.value.filter(task => task.completed).length
|
||||
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
|
||||
return { total, completed, percentage }
|
||||
})
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
let filtered = tasks.value
|
||||
|
||||
// 状态筛选
|
||||
switch (currentFilter.value) {
|
||||
case 'pending':
|
||||
filtered = filtered.filter(task => !task.completed)
|
||||
break
|
||||
case 'completed':
|
||||
filtered = filtered.filter(task => task.completed)
|
||||
break
|
||||
case 'auto':
|
||||
filtered = filtered.filter(task => task.settings?.autoExecute)
|
||||
break
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
filtered = filtered.filter(task =>
|
||||
task.title.toLowerCase().includes(keyword) ||
|
||||
task.subtitle?.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
const bulkActionOptions = [
|
||||
{
|
||||
label: '执行所有待完成任务',
|
||||
key: 'execute-all-pending'
|
||||
},
|
||||
{
|
||||
label: '标记所有为已完成',
|
||||
key: 'mark-all-completed'
|
||||
},
|
||||
{
|
||||
label: '重置所有任务状态',
|
||||
key: 'reset-all-tasks'
|
||||
}
|
||||
]
|
||||
|
||||
// 方法
|
||||
const refreshTasks = async () => {
|
||||
if (!selectedRoleId.value) {
|
||||
message.warning('请先选择游戏角色')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isRefreshing.value = true
|
||||
isLoading.value = true
|
||||
|
||||
// 使用本地模拟任务数据
|
||||
const mockTasks = generateMockTasks(selectedRoleId.value)
|
||||
tasks.value = mockTasks
|
||||
|
||||
// 缓存到本地存储
|
||||
localStorage.setItem(`dailyTasks_${selectedRoleId.value}`, JSON.stringify(mockTasks))
|
||||
|
||||
message.success('任务数据刷新成功')
|
||||
} catch (error) {
|
||||
console.error('刷新任务失败:', error)
|
||||
message.error('本地数据生成失败')
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成模拟任务数据
|
||||
const generateMockTasks = (roleId) => {
|
||||
const role = gameRolesStore.gameRoles.find(r => r.id === roleId)
|
||||
const roleName = role?.name || '未知角色'
|
||||
|
||||
return [
|
||||
{
|
||||
id: `task_${roleId}_daily_signin`,
|
||||
title: '每日签到',
|
||||
subtitle: '登录游戏获取签到奖励',
|
||||
icon: '/icons/ta.png',
|
||||
completed: false,
|
||||
canExecute: true,
|
||||
progress: { current: 0, total: 1 },
|
||||
reward: '金币 x100, 经验 x50',
|
||||
nextReset: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
settings: { autoExecute: false, delay: 0, notification: true },
|
||||
details: [
|
||||
{ id: 1, name: '打开游戏客户端', completed: false },
|
||||
{ id: 2, name: '点击签到按钮', completed: false }
|
||||
],
|
||||
logs: []
|
||||
},
|
||||
{
|
||||
id: `task_${roleId}_daily_quest`,
|
||||
title: '完成日常任务',
|
||||
subtitle: '完成5个日常任务获得奖励',
|
||||
icon: '/icons/ta.png',
|
||||
completed: false,
|
||||
canExecute: true,
|
||||
progress: { current: 2, total: 5 },
|
||||
reward: '金币 x500, 装备碎片 x10',
|
||||
nextReset: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
settings: { autoExecute: true, delay: 5, notification: true },
|
||||
details: [
|
||||
{ id: 1, name: '击败10只怪物', completed: true },
|
||||
{ id: 2, name: '收集20个材料', completed: true },
|
||||
{ id: 3, name: '完成一次副本', completed: false },
|
||||
{ id: 4, name: '参与公会活动', completed: false },
|
||||
{ id: 5, name: '强化装备', completed: false }
|
||||
],
|
||||
logs: [
|
||||
{ id: 1, timestamp: Date.now() - 30 * 60 * 1000, type: 'success', message: '已完成击败怪物任务' },
|
||||
{ id: 2, timestamp: Date.now() - 60 * 60 * 1000, type: 'success', message: '已完成材料收集任务' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: `task_${roleId}_guild_contribution`,
|
||||
title: '公会贡献',
|
||||
subtitle: '为公会贡献资源获得贡献点',
|
||||
icon: '/icons/ta.png',
|
||||
completed: true,
|
||||
canExecute: false,
|
||||
progress: { current: 1, total: 1 },
|
||||
reward: '公会贡献点 x100',
|
||||
nextReset: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
settings: { autoExecute: true, delay: 0, notification: true },
|
||||
details: [
|
||||
{ id: 1, name: '捐献金币', completed: true }
|
||||
],
|
||||
logs: [
|
||||
{ id: 1, timestamp: Date.now() - 2 * 60 * 60 * 1000, type: 'success', message: '已完成公会贡献' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const onRoleChange = (roleId) => {
|
||||
selectedRoleId.value = roleId
|
||||
gameRolesStore.selectRole(
|
||||
gameRolesStore.gameRoles.find(role => role.id === roleId)
|
||||
)
|
||||
|
||||
if (roleId) {
|
||||
refreshTasks()
|
||||
}
|
||||
}
|
||||
|
||||
const onFilterChange = (filter) => {
|
||||
currentFilter.value = filter
|
||||
}
|
||||
|
||||
const onSearch = (keyword) => {
|
||||
searchKeyword.value = keyword
|
||||
}
|
||||
|
||||
const executeTask = async (taskId) => {
|
||||
if (!selectedRoleId.value) {
|
||||
message.error('请先选择游戏角色')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查WebSocket连接状态
|
||||
const wsStatus = localTokenStore.getWebSocketStatus(selectedRoleId.value)
|
||||
if (wsStatus !== 'connected') {
|
||||
// 尝试建立连接
|
||||
const tokenData = localTokenStore.getGameToken(selectedRoleId.value)
|
||||
if (tokenData) {
|
||||
localTokenStore.createWebSocketConnection(
|
||||
selectedRoleId.value,
|
||||
tokenData.token,
|
||||
tokenData.wsUrl
|
||||
)
|
||||
// 等待一秒让连接建立
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
} else {
|
||||
throw new Error('未找到游戏token,请重新添加角色')
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟通过WebSocket执行任务
|
||||
console.log(`通过WebSocket执行任务: ${taskId}`)
|
||||
|
||||
// 更新本地任务状态
|
||||
const taskIndex = tasks.value.findIndex(task => task.id === taskId)
|
||||
if (taskIndex !== -1) {
|
||||
tasks.value[taskIndex] = {
|
||||
...tasks.value[taskIndex],
|
||||
completed: true,
|
||||
completedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
// 添加执行日志
|
||||
if (!tasks.value[taskIndex].logs) {
|
||||
tasks.value[taskIndex].logs = []
|
||||
}
|
||||
tasks.value[taskIndex].logs.push({
|
||||
id: Date.now(),
|
||||
timestamp: Date.now(),
|
||||
type: 'success',
|
||||
message: `任务 "${tasks.value[taskIndex].title}" 执行成功`
|
||||
})
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem(`dailyTasks_${selectedRoleId.value}`, JSON.stringify(tasks.value))
|
||||
}
|
||||
|
||||
message.success('任务执行成功')
|
||||
} catch (error) {
|
||||
console.error('执行任务失败:', error)
|
||||
|
||||
// 添加错误日志
|
||||
const taskIndex = tasks.value.findIndex(task => task.id === taskId)
|
||||
if (taskIndex !== -1) {
|
||||
if (!tasks.value[taskIndex].logs) {
|
||||
tasks.value[taskIndex].logs = []
|
||||
}
|
||||
tasks.value[taskIndex].logs.push({
|
||||
id: Date.now(),
|
||||
timestamp: Date.now(),
|
||||
type: 'error',
|
||||
message: `任务执行失败: ${error.message}`
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const toggleTaskStatus = (taskId) => {
|
||||
const taskIndex = tasks.value.findIndex(task => task.id === taskId)
|
||||
if (taskIndex !== -1) {
|
||||
tasks.value[taskIndex].completed = !tasks.value[taskIndex].completed
|
||||
message.info('任务状态已更新')
|
||||
}
|
||||
}
|
||||
|
||||
const updateTask = (updatedTask) => {
|
||||
const taskIndex = tasks.value.findIndex(task => task.id === updatedTask.id)
|
||||
if (taskIndex !== -1) {
|
||||
tasks.value[taskIndex] = updatedTask
|
||||
}
|
||||
}
|
||||
|
||||
const handleBulkAction = (key) => {
|
||||
switch (key) {
|
||||
case 'execute-all-pending':
|
||||
executeAllPendingTasks()
|
||||
break
|
||||
case 'mark-all-completed':
|
||||
markAllCompleted()
|
||||
break
|
||||
case 'reset-all-tasks':
|
||||
resetAllTasks()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const executeAllPendingTasks = async () => {
|
||||
const pendingTasks = tasks.value.filter(task => !task.completed && task.canExecute)
|
||||
|
||||
if (pendingTasks.length === 0) {
|
||||
message.info('没有可执行的待完成任务')
|
||||
return
|
||||
}
|
||||
|
||||
dialog.confirm({
|
||||
title: '批量执行任务',
|
||||
content: `确定要执行 ${pendingTasks.length} 个待完成任务吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
for (const task of pendingTasks) {
|
||||
try {
|
||||
await executeTask(task.id)
|
||||
successCount++
|
||||
} catch (error) {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
message.info(`批量执行完成:成功 ${successCount} 个,失败 ${failCount} 个`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const markAllCompleted = () => {
|
||||
const pendingTasks = tasks.value.filter(task => !task.completed)
|
||||
|
||||
if (pendingTasks.length === 0) {
|
||||
message.info('所有任务都已完成')
|
||||
return
|
||||
}
|
||||
|
||||
dialog.confirm({
|
||||
title: '标记所有任务为已完成',
|
||||
content: `确定要将 ${pendingTasks.length} 个待完成任务标记为已完成吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
pendingTasks.forEach(task => {
|
||||
task.completed = true
|
||||
task.completedAt = new Date().toISOString()
|
||||
})
|
||||
message.success('所有任务已标记为完成')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetAllTasks = () => {
|
||||
dialog.confirm({
|
||||
title: '重置所有任务状态',
|
||||
content: '确定要重置所有任务状态吗?此操作将清除所有完成记录。',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
tasks.value.forEach(task => {
|
||||
task.completed = false
|
||||
task.completedAt = null
|
||||
})
|
||||
message.success('所有任务状态已重置')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
// 确保用户已登录
|
||||
if (!authStore.isAuthenticated) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化游戏角色数据
|
||||
if (gameRolesStore.gameRoles.length === 0) {
|
||||
await gameRolesStore.fetchGameRoles()
|
||||
}
|
||||
|
||||
// 设置默认选中的角色
|
||||
if (gameRolesStore.selectedRole) {
|
||||
selectedRoleId.value = gameRolesStore.selectedRole.id
|
||||
// 尝试从本地存储加载任务数据
|
||||
const savedTasks = localStorage.getItem(`dailyTasks_${selectedRoleId.value}`)
|
||||
if (savedTasks) {
|
||||
try {
|
||||
tasks.value = JSON.parse(savedTasks)
|
||||
} catch (error) {
|
||||
console.error('解析任务数据失败:', error)
|
||||
refreshTasks()
|
||||
}
|
||||
} else {
|
||||
refreshTasks()
|
||||
}
|
||||
} else if (gameRolesStore.gameRoles.length > 0) {
|
||||
selectedRoleId.value = gameRolesStore.gameRoles[0].id
|
||||
onRoleChange(selectedRoleId.value)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听选中角色变化
|
||||
watch(() => gameRolesStore.selectedRole, (newRole) => {
|
||||
if (newRole && newRole.id !== selectedRoleId.value) {
|
||||
selectedRoleId.value = newRole.id
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.daily-tasks-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: var(--spacing-xl) 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: var(--font-size-lg);
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.role-selector-section {
|
||||
background: white;
|
||||
padding: var(--spacing-lg) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.role-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.selector-label {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.role-stats {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: white;
|
||||
padding: var(--spacing-md) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.tasks-section {
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.tasks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1200px) {
|
||||
.tasks-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.role-selector {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.role-stats {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tasks-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
840
src/views/Dashboard.vue
Normal file
840
src/views/Dashboard.vue
Normal file
@@ -0,0 +1,840 @@
|
||||
<template>
|
||||
<div class="dashboard-page">
|
||||
<!-- 顶部导航 -->
|
||||
<nav class="dashboard-nav">
|
||||
<div class="nav-container">
|
||||
<div class="nav-brand">
|
||||
<img
|
||||
src="/icons/logo.png"
|
||||
alt="XYZW"
|
||||
class="brand-logo"
|
||||
>
|
||||
<span class="brand-text">XYZW 控制台</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu">
|
||||
<router-link
|
||||
to="/dashboard"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><Home /></n-icon>
|
||||
<span>首页</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/game-features"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><Cube /></n-icon>
|
||||
<span>游戏功能</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/tokens"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><PersonCircle /></n-icon>
|
||||
<span>Token管理</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/daily-tasks"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><Settings /></n-icon>
|
||||
<span>任务管理</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/message-test"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><ChatBubbles /></n-icon>
|
||||
<span>消息测试</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/profile"
|
||||
class="nav-item"
|
||||
active-class="active"
|
||||
>
|
||||
<n-icon><Settings /></n-icon>
|
||||
<span>个人设置</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="nav-user">
|
||||
<n-dropdown
|
||||
:options="userMenuOptions"
|
||||
@select="handleUserAction"
|
||||
>
|
||||
<div class="user-info">
|
||||
<n-avatar
|
||||
size="medium"
|
||||
fallback-src="/icons/xiaoyugan.png"
|
||||
/>
|
||||
<span class="username">{{ tokenStore.selectedToken?.name || '未选择Token' }}</span>
|
||||
<n-icon><ChevronDown /></n-icon>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="dashboard-main">
|
||||
<div class="main-container">
|
||||
<!-- 欢迎区域 -->
|
||||
<section class="welcome-section">
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-text">
|
||||
<h1>欢迎回来,{{ tokenStore.selectedToken?.name || '游戏玩家' }}!</h1>
|
||||
<p>今天是 {{ currentDate }},继续您的游戏管理之旅吧</p>
|
||||
</div>
|
||||
<div class="welcome-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="router.push('/game-features')"
|
||||
>
|
||||
进入游戏功能
|
||||
</n-button>
|
||||
<n-button
|
||||
size="large"
|
||||
@click="router.push('/tokens')"
|
||||
>
|
||||
管理Token
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<section class="stats-section">
|
||||
<div class="stats-grid">
|
||||
<div
|
||||
v-for="stat in statistics"
|
||||
:key="stat.id"
|
||||
class="stat-card"
|
||||
>
|
||||
<div
|
||||
class="stat-icon"
|
||||
:style="{ color: stat.color }"
|
||||
>
|
||||
<component :is="stat.icon" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">
|
||||
{{ stat.value }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
{{ stat.label }}
|
||||
</div>
|
||||
<div
|
||||
class="stat-change"
|
||||
:class="stat.changeType"
|
||||
>
|
||||
{{ stat.change }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<section class="quick-actions-section">
|
||||
<h2 class="section-title">
|
||||
快速操作
|
||||
</h2>
|
||||
<div class="actions-grid">
|
||||
<div
|
||||
v-for="action in quickActions"
|
||||
:key="action.id"
|
||||
class="action-card"
|
||||
@click="handleQuickAction(action)"
|
||||
>
|
||||
<div class="action-icon">
|
||||
<component :is="action.icon" />
|
||||
</div>
|
||||
<div class="action-content">
|
||||
<h3>{{ action.title }}</h3>
|
||||
<p>{{ action.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<section class="recent-activity-section">
|
||||
<div class="activity-header">
|
||||
<h2 class="section-title">
|
||||
最近活动
|
||||
</h2>
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="refreshActivity"
|
||||
>
|
||||
刷新
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="recentActivities.length"
|
||||
class="activity-list"
|
||||
>
|
||||
<div
|
||||
v-for="activity in recentActivities"
|
||||
:key="activity.id"
|
||||
class="activity-item"
|
||||
>
|
||||
<div
|
||||
class="activity-icon"
|
||||
:class="activity.type"
|
||||
>
|
||||
<component :is="getActivityIcon(activity.type)" />
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">
|
||||
{{ activity.message }}
|
||||
</div>
|
||||
<div class="activity-time">
|
||||
{{ formatTime(activity.timestamp) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="empty-activity"
|
||||
>
|
||||
<n-empty description="暂无活动记录" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import {
|
||||
Home,
|
||||
PersonCircle,
|
||||
Cube,
|
||||
Settings,
|
||||
ChevronDown,
|
||||
Ribbon,
|
||||
CheckmarkCircle,
|
||||
Time,
|
||||
TrendingUp,
|
||||
Add,
|
||||
Chatbubbles,
|
||||
Cloud
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 响应式数据
|
||||
const recentActivities = ref([])
|
||||
|
||||
// 计算属性
|
||||
const currentDate = computed(() => {
|
||||
return new Date().toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long'
|
||||
})
|
||||
})
|
||||
|
||||
const statistics = computed(() => [
|
||||
{
|
||||
id: 1,
|
||||
icon: PersonCircle,
|
||||
label: '游戏Token',
|
||||
value: tokenStore.gameTokens.length,
|
||||
change: '+2 本月',
|
||||
changeType: 'positive',
|
||||
color: '#18a058'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: CheckmarkCircle,
|
||||
label: '已完成任务',
|
||||
value: '156',
|
||||
change: '+12 今日',
|
||||
changeType: 'positive',
|
||||
color: '#2080f0'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: Time,
|
||||
label: '节省时间',
|
||||
value: '24.5h',
|
||||
change: '+3.2h 本周',
|
||||
changeType: 'positive',
|
||||
color: '#f0a020'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: TrendingUp,
|
||||
label: '效率提升',
|
||||
value: '85%',
|
||||
change: '+15% 本月',
|
||||
changeType: 'positive',
|
||||
color: '#d03050'
|
||||
}
|
||||
])
|
||||
|
||||
const quickActions = ref([
|
||||
{
|
||||
id: 1,
|
||||
icon: Cube,
|
||||
title: '游戏功能',
|
||||
description: '访问所有游戏功能模块',
|
||||
action: 'game-features'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: Add,
|
||||
title: '添加Token',
|
||||
description: '快速添加新的游戏Token',
|
||||
action: 'add-token'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: CheckmarkCircle,
|
||||
title: '执行任务',
|
||||
description: '一键执行所有待完成任务',
|
||||
action: 'execute-tasks'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: Cloud,
|
||||
title: 'WebSocket测试',
|
||||
description: '测试WebSocket连接和游戏命令',
|
||||
action: 'websocket-test'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
icon: Settings,
|
||||
title: '系统设置',
|
||||
description: '配置个人偏好和系统选项',
|
||||
action: 'open-settings'
|
||||
}
|
||||
])
|
||||
|
||||
const userMenuOptions = [
|
||||
{
|
||||
label: '个人资料',
|
||||
key: 'profile'
|
||||
},
|
||||
{
|
||||
label: '账户设置',
|
||||
key: 'settings'
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout'
|
||||
}
|
||||
]
|
||||
|
||||
// 方法
|
||||
const handleUserAction = (key) => {
|
||||
switch (key) {
|
||||
case 'profile':
|
||||
router.push('/profile')
|
||||
break
|
||||
case 'settings':
|
||||
router.push('/settings')
|
||||
break
|
||||
case 'logout':
|
||||
tokenStore.clearAllTokens()
|
||||
message.success('已清除所有Token')
|
||||
router.push('/tokens')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuickAction = (action) => {
|
||||
switch (action.action) {
|
||||
case 'game-features':
|
||||
router.push('/game-features')
|
||||
break
|
||||
case 'add-token':
|
||||
router.push('/tokens')
|
||||
break
|
||||
case 'execute-tasks':
|
||||
router.push('/game-features')
|
||||
break
|
||||
case 'websocket-test':
|
||||
router.push('/websocket-test')
|
||||
break
|
||||
case 'open-settings':
|
||||
router.push('/profile')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const refreshActivity = () => {
|
||||
// 模拟刷新活动数据
|
||||
recentActivities.value = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'success',
|
||||
message: '成功完成日常任务:每日签到',
|
||||
timestamp: Date.now() - 30 * 60 * 1000
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'info',
|
||||
message: '添加了新的游戏角色:剑士小明',
|
||||
timestamp: Date.now() - 2 * 60 * 60 * 1000
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'warning',
|
||||
message: '任务执行遇到错误,请检查网络连接',
|
||||
timestamp: Date.now() - 4 * 60 * 60 * 1000
|
||||
}
|
||||
]
|
||||
message.success('活动数据已刷新')
|
||||
}
|
||||
|
||||
const getActivityIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return CheckmarkCircle
|
||||
case 'warning':
|
||||
return Time
|
||||
case 'info':
|
||||
default:
|
||||
return Cube
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
const diff = Date.now() - timestamp
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}天前`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时前`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟前`
|
||||
} else {
|
||||
return '刚刚'
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
// 确保有Token
|
||||
if (!tokenStore.hasTokens) {
|
||||
router.push('/tokens')
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化Token数据
|
||||
tokenStore.initTokenStore()
|
||||
refreshActivity()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dashboard-page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
// 导航栏
|
||||
.dashboard-nav {
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
padding: 0 var(--spacing-lg);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-sticky);
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
margin-right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--border-radius-medium);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary-color-light);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-user {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--border-radius-medium);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
// 主要内容
|
||||
.dashboard-main {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.main-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// 欢迎区域
|
||||
.welcome-section {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-2xl);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-lg);
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
// 统计区域
|
||||
.stats-section {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-light);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: var(--spacing-md);
|
||||
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
&.positive {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 快速操作区域
|
||||
.quick-actions-section {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.action-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-light);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: var(--spacing-md);
|
||||
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.action-content {
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 最近活动区域
|
||||
.recent-activity-section {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--border-radius-medium);
|
||||
transition: background var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.success {
|
||||
background: rgba(24, 160, 88, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: rgba(240, 160, 32, 0.1);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: rgba(32, 128, 240, 0.1);
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
:deep(svg) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-text {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.empty-activity {
|
||||
text-align: center;
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1024px) {
|
||||
.welcome-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-main {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.welcome-text h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
570
src/views/GameFeatures.vue
Normal file
570
src/views/GameFeatures.vue
Normal file
@@ -0,0 +1,570 @@
|
||||
<template>
|
||||
<div class="game-features-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<h1 class="page-title">
|
||||
游戏功能
|
||||
</h1>
|
||||
<p class="page-subtitle">
|
||||
{{ tokenStore.selectedToken?.name || '未选择Token' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<div
|
||||
class="connection-status"
|
||||
:class="connectionStatus"
|
||||
>
|
||||
<n-icon><CloudDone /></n-icon>
|
||||
<span>{{ connectionStatusText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 反馈提示区域 -->
|
||||
<div
|
||||
v-if="showFeedback"
|
||||
class="feedback-section"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 功能模块网格 -->
|
||||
<div class="features-grid-section">
|
||||
<div class="container">
|
||||
<GameStatus />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket 连接状态 -->
|
||||
<div class="ws-status-section">
|
||||
<div class="container">
|
||||
<div class="ws-status-card">
|
||||
<div class="status-header">
|
||||
<h3>连接状态</h3>
|
||||
<n-button
|
||||
text
|
||||
@click="toggleConnection"
|
||||
>
|
||||
{{ isConnected ? '断开连接' : '重新连接' }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="status-content">
|
||||
<div class="status-item">
|
||||
<span>WebSocket状态:</span>
|
||||
<span :class="connectionClass">{{ connectionStatusText }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="tokenStore.selectedToken"
|
||||
class="status-item"
|
||||
>
|
||||
<span>当前Token:</span>
|
||||
<span>{{ tokenStore.selectedToken.name }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="lastActivity"
|
||||
class="status-item"
|
||||
>
|
||||
<span>最后活动:</span>
|
||||
<span>{{ lastActivity }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import GameStatus from '@/components/GameStatus.vue'
|
||||
import { CloudDone } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 响应式数据
|
||||
const showFeedback = ref(true)
|
||||
const lastActivity = ref(null)
|
||||
|
||||
// 计算属性
|
||||
const connectionStatus = computed(() => {
|
||||
if (!tokenStore.selectedToken) return 'disconnected'
|
||||
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
return status === 'connected' ? 'connected' : 'disconnected'
|
||||
})
|
||||
|
||||
const connectionStatusText = computed(() => {
|
||||
if (!tokenStore.selectedToken) return '未选择Token'
|
||||
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
return status === 'connected' ? '已连接' : '未连接'
|
||||
})
|
||||
|
||||
const connectionClass = computed(() => {
|
||||
return connectionStatus.value === 'connected' ? 'status-connected' : 'status-disconnected'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleFeatureAction = (featureType) => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
router.push('/tokens')
|
||||
return
|
||||
}
|
||||
|
||||
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
if (status !== 'connected') {
|
||||
message.warning('WebSocket未连接,请先建立连接')
|
||||
return
|
||||
}
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
const actions = {
|
||||
'team-challenge': () => {
|
||||
message.info('开始执行队伍挑战...')
|
||||
tokenStore.sendMessage(tokenId, 'fight_startareaarena')
|
||||
},
|
||||
'daily-tasks': () => {
|
||||
message.info('启动每日任务服务...')
|
||||
tokenStore.sendMessage(tokenId, 'task_claimdailyreward')
|
||||
},
|
||||
'salt-robot': () => {
|
||||
message.info('领取盐罐机器人奖励...')
|
||||
tokenStore.sendMessage(tokenId, 'bottlehelper_claim')
|
||||
},
|
||||
'idle-time': () => {
|
||||
message.info('领取挂机时间奖励...')
|
||||
tokenStore.sendMessage(tokenId, 'system_claimhangupreward')
|
||||
},
|
||||
'power-switch': () => {
|
||||
message.info('执行威震大开关...')
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
},
|
||||
'club-ranking': () => {
|
||||
message.info('报名俱乐部排位...')
|
||||
tokenStore.sendMessage(tokenId, 'legionmatch_rolesignup')
|
||||
},
|
||||
'club-checkin': () => {
|
||||
message.info('执行俱乐部签到...')
|
||||
tokenStore.sendMessage(tokenId, 'legion_signin')
|
||||
},
|
||||
'tower-challenge': () => {
|
||||
message.info('开始爬塔挑战...')
|
||||
tokenStore.sendMessage(tokenId, 'fight_starttower')
|
||||
}
|
||||
}
|
||||
|
||||
const action = actions[featureType]
|
||||
if (action) {
|
||||
action()
|
||||
} else {
|
||||
message.warning('功能暂未实现')
|
||||
}
|
||||
}
|
||||
|
||||
// 已移除 sendWebSocketMessage,使用 tokenStore.sendMessage 代替
|
||||
|
||||
const connectWebSocket = () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择一个Token')
|
||||
router.push('/tokens')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
const token = tokenStore.selectedToken.token
|
||||
|
||||
// 使用 tokenStore 的 WebSocket 连接管理
|
||||
tokenStore.createWebSocketConnection(tokenId, token)
|
||||
message.info('正在建立 WebSocket 连接...')
|
||||
|
||||
// 等待连接建立
|
||||
setTimeout(async () => {
|
||||
const status = tokenStore.getWebSocketStatus(tokenId)
|
||||
if (status === 'connected') {
|
||||
message.success('WebSocket 连接成功')
|
||||
// 连接成功后自动初始化游戏数据
|
||||
await initializeGameData()
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('WebSocket连接失败:', error)
|
||||
message.error('WebSocket连接失败')
|
||||
}
|
||||
}
|
||||
|
||||
const disconnectWebSocket = () => {
|
||||
if (tokenStore.selectedToken) {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.closeWebSocketConnection(tokenId)
|
||||
message.info('WebSocket连接已断开')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleConnection = () => {
|
||||
if (connectionStatus.value === 'connected') {
|
||||
disconnectWebSocket()
|
||||
} else {
|
||||
connectWebSocket()
|
||||
}
|
||||
}
|
||||
|
||||
// handleWebSocketMessage 已移除,消息处理由 tokenStore 负责
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 检查是否需要连接 WebSocket
|
||||
if (tokenStore.selectedToken) {
|
||||
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
if (status !== 'connected') {
|
||||
connectWebSocket()
|
||||
} else {
|
||||
// 如果已连接,立即获取初始数据
|
||||
initializeGameData()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化游戏数据
|
||||
const initializeGameData = async () => {
|
||||
if (!tokenStore.selectedToken) return
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
console.log('🎮 初始化游戏数据...')
|
||||
|
||||
// 获取角色信息
|
||||
console.log('🎮 正在获取角色信息...')
|
||||
const roleResult = tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
console.log('🎮 角色信息请求结果:', roleResult)
|
||||
|
||||
// 获取塔信息
|
||||
console.log('🎮 正在获取塔信息...')
|
||||
const towerResult = tokenStore.sendMessage(tokenId, 'tower_getinfo')
|
||||
console.log('🎮 塔信息请求结果:', towerResult)
|
||||
|
||||
// 获取队伍信息
|
||||
console.log('🎮 正在获取队伍信息...')
|
||||
const teamResult = tokenStore.sendMessage(tokenId, 'presetteam_getteam')
|
||||
console.log('🎮 队伍信息请求结果:', teamResult)
|
||||
|
||||
console.log('🎮 游戏数据初始化请求已发送')
|
||||
} catch (error) {
|
||||
console.warn('初始化游戏数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
// WebSocket 连接由 tokenStore 管理,不需要手动清理
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.game-features-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
// 页面头部
|
||||
.page-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
padding: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--border-radius-medium);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
&.connected {
|
||||
background: rgba(24, 160, 88, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
background: rgba(208, 48, 80, 0.1);
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 反馈提示区域
|
||||
.feedback-section {
|
||||
padding: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
// 功能模块网格
|
||||
.features-grid-section {
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
// 不同功能的主题色
|
||||
&.team-challenge { border-left-color: #2080f0; }
|
||||
&.daily-tasks { border-left-color: #f0a020; }
|
||||
&.salt-robot { border-left-color: #18a058; }
|
||||
&.idle-time { border-left-color: #d03050; }
|
||||
&.power-switch { border-left-color: #7c3aed; }
|
||||
&.club-ranking { border-left-color: #f59e0b; }
|
||||
&.club-checkin { border-left-color: #10b981; }
|
||||
&.tower-challenge { border-left-color: #6366f1; }
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
background: var(--primary-color-light);
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(svg) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.feature-badge, .feature-status {
|
||||
flex-shrink: 0;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-small);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.feature-status {
|
||||
&.in-progress {
|
||||
background: rgba(240, 160, 32, 0.1);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: rgba(24, 160, 88, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.waiting {
|
||||
background: rgba(32, 128, 240, 0.1);
|
||||
color: var(--info-color);
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
|
||||
.stage-text {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
// WebSocket状态区域
|
||||
.ws-status-section {
|
||||
padding: 0 0 var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.ws-status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1024px) {
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
575
src/views/GameRoles.vue
Normal file
575
src/views/GameRoles.vue
Normal file
@@ -0,0 +1,575 @@
|
||||
<template>
|
||||
<div class="game-roles-page">
|
||||
<div class="container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<h1>游戏角色</h1>
|
||||
<p>管理您的所有游戏角色</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="showAddModal = true"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><Add /></n-icon>
|
||||
</template>
|
||||
添加角色
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<div
|
||||
v-if="gameRolesStore.gameRoles.length"
|
||||
class="roles-grid"
|
||||
>
|
||||
<div
|
||||
v-for="role in gameRolesStore.gameRoles"
|
||||
:key="role.id"
|
||||
class="role-card"
|
||||
:class="{ active: role.id === gameRolesStore.selectedRole?.id }"
|
||||
@click="selectRole(role)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="role-avatar">
|
||||
<img
|
||||
:src="role.avatar || '/icons/xiaoyugan.png'"
|
||||
:alt="role.name"
|
||||
>
|
||||
</div>
|
||||
<div class="role-actions">
|
||||
<n-dropdown
|
||||
:options="roleMenuOptions"
|
||||
@select="(key) => handleRoleAction(key, role)"
|
||||
>
|
||||
<n-button text>
|
||||
<template #icon>
|
||||
<n-icon><EllipsisHorizontal /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<h3 class="role-name">
|
||||
{{ role.name }}
|
||||
</h3>
|
||||
<p class="role-info">
|
||||
{{ role.server }} | {{ role.level }}级
|
||||
</p>
|
||||
<div class="role-tags">
|
||||
<n-tag
|
||||
size="small"
|
||||
:type="role.isActive ? 'success' : 'default'"
|
||||
>
|
||||
{{ role.isActive ? '活跃' : '离线' }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
v-if="role.vip"
|
||||
size="small"
|
||||
>
|
||||
VIP
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="role-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">经验</span>
|
||||
<span class="stat-value">{{ role.exp || '0' }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">金币</span>
|
||||
<span class="stat-value">{{ formatNumber(role.gold || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-else
|
||||
class="empty-state"
|
||||
>
|
||||
<n-empty
|
||||
description="暂无游戏角色"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon size="64">
|
||||
<PersonCircle />
|
||||
</n-icon>
|
||||
</template>
|
||||
<template #extra>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="showAddModal = true"
|
||||
>
|
||||
添加第一个角色
|
||||
</n-button>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑角色模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showAddModal"
|
||||
preset="card"
|
||||
title="添加游戏角色"
|
||||
style="width: 500px"
|
||||
>
|
||||
<n-form
|
||||
ref="roleFormRef"
|
||||
:model="roleForm"
|
||||
:rules="roleRules"
|
||||
label-placement="left"
|
||||
label-width="80px"
|
||||
>
|
||||
<n-form-item
|
||||
label="角色名称"
|
||||
path="name"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="roleForm.name"
|
||||
placeholder="请输入角色名称"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item
|
||||
label="服务器"
|
||||
path="server"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="roleForm.server"
|
||||
:options="serverOptions"
|
||||
placeholder="请选择服务器"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item
|
||||
label="职业"
|
||||
path="profession"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="roleForm.profession"
|
||||
:options="professionOptions"
|
||||
placeholder="请选择职业"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item
|
||||
label="等级"
|
||||
path="level"
|
||||
>
|
||||
<n-input-number
|
||||
v-model:value="roleForm.level"
|
||||
:min="1"
|
||||
:max="200"
|
||||
placeholder="角色等级"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="账号信息">
|
||||
<n-input
|
||||
v-model:value="roleForm.account"
|
||||
placeholder="游戏账号(可选)"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="备注">
|
||||
<n-input
|
||||
v-model:value="roleForm.note"
|
||||
type="textarea"
|
||||
placeholder="角色备注信息(可选)"
|
||||
:rows="3"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="modal-actions">
|
||||
<n-button @click="showAddModal = false">
|
||||
取消
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="isSubmitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ editingRole ? '保存' : '添加' }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage, useDialog } from 'naive-ui'
|
||||
import { useGameRolesStore } from '@/stores/gameRoles'
|
||||
import { PersonCircle, Add, EllipsisHorizontal } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const gameRolesStore = useGameRolesStore()
|
||||
|
||||
// 响应式数据
|
||||
const showAddModal = ref(false)
|
||||
const isSubmitting = ref(false)
|
||||
const editingRole = ref(null)
|
||||
const roleFormRef = ref(null)
|
||||
|
||||
// 角色表单
|
||||
const roleForm = reactive({
|
||||
name: '',
|
||||
server: '',
|
||||
profession: '',
|
||||
level: 1,
|
||||
account: '',
|
||||
note: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const roleRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
],
|
||||
server: [
|
||||
{ required: true, message: '请选择服务器', trigger: 'change' }
|
||||
],
|
||||
profession: [
|
||||
{ required: true, message: '请选择职业', trigger: 'change' }
|
||||
],
|
||||
level: [
|
||||
{ required: true, type: 'number', message: '请输入角色等级', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 选项数据
|
||||
const serverOptions = [
|
||||
{ label: '风云服', value: '风云服' },
|
||||
{ label: '神话服', value: '神话服' },
|
||||
{ label: '传奇服', value: '传奇服' },
|
||||
{ label: '梦幻服', value: '梦幻服' },
|
||||
{ label: '英雄服', value: '英雄服' }
|
||||
]
|
||||
|
||||
const professionOptions = [
|
||||
{ label: '战士', value: '战士' },
|
||||
{ label: '法师', value: '法师' },
|
||||
{ label: '道士', value: '道士' },
|
||||
{ label: '刺客', value: '刺客' },
|
||||
{ label: '弓手', value: '弓手' },
|
||||
{ label: '牧师', value: '牧师' }
|
||||
]
|
||||
|
||||
const roleMenuOptions = [
|
||||
{ label: '编辑', key: 'edit' },
|
||||
{ label: '设为主角色', key: 'set-primary' },
|
||||
{ label: '查看详情', key: 'view-details' },
|
||||
{ type: 'divider' },
|
||||
{ label: '删除', key: 'delete' }
|
||||
]
|
||||
|
||||
// 方法
|
||||
const selectRole = (role) => {
|
||||
gameRolesStore.selectRole(role)
|
||||
message.success(`已切换到角色:${role.name}`)
|
||||
}
|
||||
|
||||
const handleRoleAction = async (key, role) => {
|
||||
switch (key) {
|
||||
case 'edit':
|
||||
editRole(role)
|
||||
break
|
||||
case 'set-primary':
|
||||
selectRole(role)
|
||||
break
|
||||
case 'view-details':
|
||||
viewRoleDetails(role)
|
||||
break
|
||||
case 'delete':
|
||||
deleteRole(role)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const editRole = (role) => {
|
||||
editingRole.value = role
|
||||
Object.assign(roleForm, role)
|
||||
showAddModal.value = true
|
||||
}
|
||||
|
||||
const viewRoleDetails = (role) => {
|
||||
message.info('角色详情功能开发中...')
|
||||
}
|
||||
|
||||
const deleteRole = (role) => {
|
||||
dialog.warning({
|
||||
title: '删除角色',
|
||||
content: `确定要删除角色 "${role.name}" 吗?此操作无法恢复。`,
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
const result = await gameRolesStore.deleteGameRole(role.id)
|
||||
if (result.success) {
|
||||
message.success(result.message)
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!roleFormRef.value) return
|
||||
|
||||
try {
|
||||
await roleFormRef.value.validate()
|
||||
isSubmitting.value = true
|
||||
|
||||
let result
|
||||
if (editingRole.value) {
|
||||
// 编辑模式
|
||||
result = await gameRolesStore.updateGameRole(editingRole.value.id, roleForm)
|
||||
} else {
|
||||
// 添加模式
|
||||
result = await gameRolesStore.addGameRole(roleForm)
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
message.success(result.message)
|
||||
showAddModal.value = false
|
||||
resetForm()
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
// 表单验证失败
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.keys(roleForm).forEach(key => {
|
||||
roleForm[key] = key === 'level' ? 1 : ''
|
||||
})
|
||||
editingRole.value = null
|
||||
}
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num >= 100000000) {
|
||||
return (num / 100000000).toFixed(1) + '亿'
|
||||
} else if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + '万'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
// 获取游戏角色列表
|
||||
if (gameRolesStore.gameRoles.length === 0) {
|
||||
await gameRolesStore.fetchGameRoles()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.game-roles-page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-secondary);
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-lg);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.roles-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.role-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
box-shadow: var(--shadow-light);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-normal);
|
||||
border: 2px solid transparent;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-lg) var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.role-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--border-light);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.role-actions {
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast);
|
||||
|
||||
.role-card:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.role-info {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.role-tags {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: var(--spacing-md) var(--spacing-lg) var(--spacing-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.role-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roles-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.role-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
605
src/views/Home.vue
Normal file
605
src/views/Home.vue
Normal file
@@ -0,0 +1,605 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar glass">
|
||||
<div class="container">
|
||||
<div class="nav-content">
|
||||
<div class="nav-brand">
|
||||
<img
|
||||
src="/icons/logo.png"
|
||||
alt="XYZW"
|
||||
class="brand-logo"
|
||||
>
|
||||
<span class="brand-text">XYZW 游戏管理系统</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-actions">
|
||||
<template v-if="!authStore.isAuthenticated">
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="router.push('/login')"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="router.push('/register')"
|
||||
>
|
||||
注册
|
||||
</n-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="router.push('/dashboard')"
|
||||
>
|
||||
进入控制台
|
||||
</n-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="main-content">
|
||||
<!-- 英雄区域 -->
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<div class="hero-text">
|
||||
<h1 class="hero-title">
|
||||
专业的游戏管理平台
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
让游戏变得更简单,让管理变得更高效
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="hero-button"
|
||||
@click="router.push(authStore.isAuthenticated ? '/dashboard' : '/register')"
|
||||
>
|
||||
{{ authStore.isAuthenticated ? '进入控制台' : '立即开始' }}
|
||||
</n-button>
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
size="large"
|
||||
class="hero-button"
|
||||
@click="scrollToFeatures"
|
||||
>
|
||||
了解更多
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-visual">
|
||||
<div class="feature-cards">
|
||||
<div
|
||||
v-for="card in featureCards"
|
||||
:key="card.id"
|
||||
class="feature-card"
|
||||
>
|
||||
<div class="card-icon">
|
||||
<component :is="card.icon" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3>{{ card.title }}</h3>
|
||||
<p>{{ card.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 功能特性 -->
|
||||
<section
|
||||
ref="featuresSection"
|
||||
class="features-section"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
核心功能
|
||||
</h2>
|
||||
<p class="section-subtitle">
|
||||
为您提供全方位的游戏管理解决方案
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="features-grid">
|
||||
<div
|
||||
v-for="feature in features"
|
||||
:key="feature.id"
|
||||
class="feature-item"
|
||||
>
|
||||
<div class="feature-icon">
|
||||
<component :is="feature.icon" />
|
||||
</div>
|
||||
<h3 class="feature-title">
|
||||
{{ feature.title }}
|
||||
</h3>
|
||||
<p class="feature-description">
|
||||
{{ feature.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<section class="stats-section">
|
||||
<div class="container">
|
||||
<div class="stats-grid">
|
||||
<div
|
||||
v-for="stat in stats"
|
||||
:key="stat.id"
|
||||
class="stat-item"
|
||||
>
|
||||
<div class="stat-number">
|
||||
{{ stat.number }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
{{ stat.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<img
|
||||
src="/icons/logo.png"
|
||||
alt="XYZW"
|
||||
class="footer-logo"
|
||||
>
|
||||
<span class="footer-text">XYZW 游戏管理系统</span>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a
|
||||
href="#"
|
||||
class="footer-link"
|
||||
>关于我们</a>
|
||||
<a
|
||||
href="#"
|
||||
class="footer-link"
|
||||
>隐私政策</a>
|
||||
<a
|
||||
href="#"
|
||||
class="footer-link"
|
||||
>服务条款</a>
|
||||
<a
|
||||
href="#"
|
||||
class="footer-link"
|
||||
>联系我们</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2024 XYZW. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
PersonCircle,
|
||||
Cube,
|
||||
Ribbon,
|
||||
Home,
|
||||
Settings
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const featuresSection = ref(null)
|
||||
|
||||
// 功能卡片数据
|
||||
const featureCards = ref([
|
||||
{
|
||||
id: 1,
|
||||
icon: PersonCircle,
|
||||
title: '角色管理',
|
||||
description: '统一管理游戏角色'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: Cube,
|
||||
title: '任务系统',
|
||||
description: '自动化日常任务'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: Ribbon,
|
||||
title: '数据统计',
|
||||
description: '全面的数据分析'
|
||||
}
|
||||
])
|
||||
|
||||
// 功能特性数据
|
||||
const features = ref([
|
||||
{
|
||||
id: 1,
|
||||
icon: PersonCircle,
|
||||
title: '角色管理',
|
||||
description: '轻松管理多个游戏角色,统一查看角色信息、等级进度和装备状态'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: Cube,
|
||||
title: '任务自动化',
|
||||
description: '智能日常任务系统,自动完成重复性任务,节省您的宝贵时间'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: Ribbon,
|
||||
title: '数据分析',
|
||||
description: '详细的数据统计和分析报告,帮助您更好地了解游戏进度'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: Settings,
|
||||
title: '个性化设置',
|
||||
description: '灵活的配置选项,根据您的需求定制最适合的管理方案'
|
||||
}
|
||||
])
|
||||
|
||||
// 统计数据
|
||||
const stats = ref([
|
||||
{ id: 1, number: '1000+', label: '活跃用户' },
|
||||
{ id: 2, number: '50K+', label: '管理角色' },
|
||||
{ id: 3, number: '100K+', label: '完成任务' },
|
||||
{ id: 4, number: '99.9%', label: '系统稳定性' }
|
||||
])
|
||||
|
||||
// 滚动到功能区域
|
||||
const scrollToFeatures = () => {
|
||||
if (featuresSection.value) {
|
||||
featuresSection.value.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化认证状态
|
||||
authStore.initAuth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
// 导航栏
|
||||
.navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: var(--z-fixed);
|
||||
padding: var(--spacing-md) 0;
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
// 主要内容
|
||||
.main-content {
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
// 英雄区域
|
||||
.hero-section {
|
||||
padding: var(--spacing-2xl) 0;
|
||||
min-height: 80vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-2xl);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3.5rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height-tight);
|
||||
margin-bottom: var(--spacing-md);
|
||||
background: linear-gradient(45deg, #fff, #e6f7ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: var(--font-size-xl);
|
||||
opacity: 0.9;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.hero-button {
|
||||
padding: var(--spacing-md) var(--spacing-xl);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
// 功能卡片
|
||||
.feature-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: #fff;
|
||||
margin-bottom: var(--spacing-md);
|
||||
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
color: white;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.card-content p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
// 功能特性区域
|
||||
.features-section {
|
||||
padding: var(--spacing-2xl) 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--text-secondary);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
text-align: center;
|
||||
padding: var(--spacing-xl);
|
||||
border-radius: var(--border-radius-large);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-heavy);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto var(--spacing-lg);
|
||||
color: var(--primary-color);
|
||||
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
color: var(--text-secondary);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
// 统计区域
|
||||
.stats-section {
|
||||
padding: var(--spacing-2xl) 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 3rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-size-lg);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
// 页脚
|
||||
.footer {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
text-align: center;
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.hero-content {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
554
src/views/Login.vue
Normal file
554
src/views/Login.vue
Normal file
@@ -0,0 +1,554 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-container">
|
||||
<!-- 登录表单卡片 -->
|
||||
<div class="login-card glass">
|
||||
<div class="card-header">
|
||||
<div class="brand">
|
||||
<img
|
||||
src="/icons/logo.png"
|
||||
alt="XYZW"
|
||||
class="brand-logo"
|
||||
>
|
||||
<h1 class="brand-title">
|
||||
XYZW 游戏管理系统
|
||||
</h1>
|
||||
</div>
|
||||
<p class="welcome-text">
|
||||
欢迎回来,请登录您的账户
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<n-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
:show-label="false"
|
||||
>
|
||||
<n-form-item path="username">
|
||||
<n-input
|
||||
v-model:value="loginForm.username"
|
||||
placeholder="用户名或邮箱"
|
||||
:input-props="{ autocomplete: 'username' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<PersonCircle />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
v-model:value="loginForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
:input-props="{ autocomplete: 'current-password' }"
|
||||
@keydown.enter="handleLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon>
|
||||
<Lock />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<div class="form-options">
|
||||
<n-checkbox v-model:checked="loginForm.rememberMe">
|
||||
记住我
|
||||
</n-checkbox>
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="router.push('/forgot-password')"
|
||||
>
|
||||
忘记密码?
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="authStore.isLoading"
|
||||
class="login-button"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
</n-form>
|
||||
|
||||
<n-divider>
|
||||
<span class="divider-text">其他登录方式</span>
|
||||
</n-divider>
|
||||
|
||||
<div class="social-login">
|
||||
<n-button
|
||||
size="large"
|
||||
class="social-button"
|
||||
@click="handleSocialLogin('qq')"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<PersonCircle />
|
||||
</n-icon>
|
||||
</template>
|
||||
QQ登录
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
size="large"
|
||||
class="social-button"
|
||||
@click="handleSocialLogin('wechat')"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<PersonCircle />
|
||||
</n-icon>
|
||||
</template>
|
||||
微信登录
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="register-prompt">
|
||||
<span>还没有账户?</span>
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="router.push('/register')"
|
||||
>
|
||||
立即注册
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能展示 -->
|
||||
<div class="features-showcase">
|
||||
<div class="showcase-header">
|
||||
<h2>为什么选择 XYZW?</h2>
|
||||
<p>专业的游戏管理平台,让游戏变得更轻松</p>
|
||||
</div>
|
||||
|
||||
<div class="features-list">
|
||||
<div
|
||||
v-for="feature in features"
|
||||
:key="feature.id"
|
||||
class="feature-item"
|
||||
>
|
||||
<div class="feature-icon">
|
||||
<component :is="feature.icon" />
|
||||
</div>
|
||||
<div class="feature-content">
|
||||
<h3>{{ feature.title }}</h3>
|
||||
<p>{{ feature.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 背景装饰 -->
|
||||
<div class="background-decoration">
|
||||
<div class="decoration-circle circle-1" />
|
||||
<div class="decoration-circle circle-2" />
|
||||
<div class="decoration-circle circle-3" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { PersonCircle, Lock, Cube, Ribbon, Settings } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
const loginFormRef = ref(null)
|
||||
|
||||
// 登录表单数据
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const loginRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入用户名或邮箱',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: '密码长度不能少于6位',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 功能特性数据
|
||||
const features = [
|
||||
{
|
||||
id: 1,
|
||||
icon: PersonCircle,
|
||||
title: '多角色管理',
|
||||
description: '统一管理多个游戏角色,随时切换查看'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: Cube,
|
||||
title: '任务自动化',
|
||||
description: '智能执行日常任务,解放双手节省时间'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: Ribbon,
|
||||
title: '数据统计',
|
||||
description: '详细的进度统计,让游戏数据一目了然'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: Settings,
|
||||
title: '个性化配置',
|
||||
description: '灵活的设置选项,打造专属管理方案'
|
||||
}
|
||||
]
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
try {
|
||||
await loginFormRef.value.validate()
|
||||
|
||||
const result = await authStore.login({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password,
|
||||
rememberMe: loginForm.rememberMe
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
message.success('登录成功')
|
||||
|
||||
// 跳转到dashboard或之前访问的页面
|
||||
const redirect = router.currentRoute.value.query.redirect || '/dashboard'
|
||||
router.push(redirect)
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
// 表单验证失败
|
||||
console.error('Login validation failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理社交登录
|
||||
const handleSocialLogin = (provider) => {
|
||||
message.info(`${provider === 'qq' ? 'QQ' : '微信'}登录功能开发中...`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 如果已经登录,直接跳转
|
||||
if (authStore.isAuthenticated) {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-2xl);
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-2xl);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
.n-form {
|
||||
.n-form-item {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 48px;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.social-login {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.social-button {
|
||||
height: 44px;
|
||||
border: 1px solid var(--border-light);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.register-prompt {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
|
||||
span {
|
||||
margin-right: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
// 功能展示区域
|
||||
.features-showcase {
|
||||
color: white;
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.showcase-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-lg);
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.features-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--border-radius-large);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
transform: translateX(8px);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
p {
|
||||
opacity: 0.8;
|
||||
line-height: var(--line-height-relaxed);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 背景装饰
|
||||
.background-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.decoration-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
top: 10%;
|
||||
right: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
bottom: 20%;
|
||||
left: 15%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
top: 60%;
|
||||
right: 20%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1024px) {
|
||||
.login-container {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.features-showcase {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.showcase-header h2 {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.login-container {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.social-login {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.decoration-circle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
116
src/views/NotFound.vue
Normal file
116
src/views/NotFound.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="not-found-page">
|
||||
<div class="container">
|
||||
<div class="error-content">
|
||||
<div class="error-visual">
|
||||
<div class="error-number">
|
||||
404
|
||||
</div>
|
||||
<div class="error-icon">
|
||||
<n-icon size="120">
|
||||
<Search />
|
||||
</n-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error-text">
|
||||
<h1>页面未找到</h1>
|
||||
<p>抱歉,您访问的页面不存在或已被移除。</p>
|
||||
</div>
|
||||
|
||||
<div class="error-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
返回首页
|
||||
</n-button>
|
||||
<n-button
|
||||
size="large"
|
||||
@click="router.back()"
|
||||
>
|
||||
返回上一页
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Search } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.not-found-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.error-content {
|
||||
text-align: center;
|
||||
color: white;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.error-visual {
|
||||
position: relative;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.error-number {
|
||||
font-size: 12rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
opacity: 0.1;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-lg);
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.error-number {
|
||||
font-size: 8rem;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
558
src/views/Profile.vue
Normal file
558
src/views/Profile.vue
Normal file
@@ -0,0 +1,558 @@
|
||||
<template>
|
||||
<div class="profile-page">
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1>个人资料</h1>
|
||||
<p>管理您的账户信息和偏好设置</p>
|
||||
</div>
|
||||
|
||||
<div class="profile-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="profile-section">
|
||||
<h2>基本信息</h2>
|
||||
<div class="info-card">
|
||||
<div class="avatar-section">
|
||||
<n-avatar
|
||||
size="large"
|
||||
:src="userInfo.avatar"
|
||||
fallback-src="/icons/xiaoyugan.png"
|
||||
/>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="changeAvatar"
|
||||
>
|
||||
更换头像
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="info-form">
|
||||
<n-form
|
||||
:model="userInfo"
|
||||
label-placement="left"
|
||||
label-width="80px"
|
||||
>
|
||||
<n-form-item label="用户名">
|
||||
<n-input
|
||||
v-model:value="userInfo.username"
|
||||
readonly
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱">
|
||||
<n-input v-model:value="userInfo.email" />
|
||||
</n-form-item>
|
||||
<n-form-item label="昵称">
|
||||
<n-input
|
||||
v-model:value="userInfo.nickname"
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="手机">
|
||||
<n-input
|
||||
v-model:value="userInfo.phone"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<div class="form-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="saveProfile"
|
||||
>
|
||||
保存更改
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码修改 -->
|
||||
<div class="profile-section">
|
||||
<h2>密码修改</h2>
|
||||
<div class="info-card">
|
||||
<n-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
label-placement="left"
|
||||
label-width="100px"
|
||||
>
|
||||
<n-form-item
|
||||
label="当前密码"
|
||||
path="currentPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="passwordForm.currentPassword"
|
||||
type="password"
|
||||
placeholder="请输入当前密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="新密码"
|
||||
path="newPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="passwordForm.newPassword"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="确认新密码"
|
||||
path="confirmPassword"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<div class="form-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="changePassword"
|
||||
>
|
||||
修改密码
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统偏好 -->
|
||||
<div class="profile-section">
|
||||
<h2>系统偏好</h2>
|
||||
<div class="info-card">
|
||||
<div class="preferences-grid">
|
||||
<div class="preference-item">
|
||||
<div class="preference-label">
|
||||
<h3>主题设置</h3>
|
||||
<p>选择您喜欢的界面主题</p>
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="preferences.theme"
|
||||
:options="themeOptions"
|
||||
@update:value="updateTheme"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="preference-item">
|
||||
<div class="preference-label">
|
||||
<h3>语言设置</h3>
|
||||
<p>选择界面显示语言</p>
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="preferences.language"
|
||||
:options="languageOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="preference-item">
|
||||
<div class="preference-label">
|
||||
<h3>通知设置</h3>
|
||||
<p>接收任务完成通知</p>
|
||||
</div>
|
||||
<n-switch v-model:value="preferences.notifications" />
|
||||
</div>
|
||||
|
||||
<div class="preference-item">
|
||||
<div class="preference-label">
|
||||
<h3>自动执行</h3>
|
||||
<p>默认开启任务自动执行</p>
|
||||
</div>
|
||||
<n-switch v-model:value="preferences.autoExecute" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="savePreferences"
|
||||
>
|
||||
保存偏好
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token管理 -->
|
||||
<div class="profile-section">
|
||||
<h2>Token管理</h2>
|
||||
<TokenManager />
|
||||
</div>
|
||||
|
||||
<!-- 账户安全 -->
|
||||
<div class="profile-section">
|
||||
<h2>账户安全</h2>
|
||||
<div class="info-card">
|
||||
<div class="security-items">
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<h3>两步验证</h3>
|
||||
<p>为您的账户添加额外的安全保护</p>
|
||||
</div>
|
||||
<n-button @click="setupTwoFactor">
|
||||
设置
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<h3>登录历史</h3>
|
||||
<p>查看最近的登录记录</p>
|
||||
</div>
|
||||
<n-button @click="viewLoginHistory">
|
||||
查看
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<h3>数据导出</h3>
|
||||
<p>导出您的所有数据</p>
|
||||
</div>
|
||||
<n-button @click="exportData">
|
||||
导出
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="security-item danger">
|
||||
<div class="security-info">
|
||||
<h3>删除账户</h3>
|
||||
<p>永久删除您的账户和所有数据</p>
|
||||
</div>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="deleteAccount"
|
||||
>
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage, useDialog } from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import TokenManager from '@/components/TokenManager.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const authStore = useAuthStore()
|
||||
const passwordFormRef = ref(null)
|
||||
|
||||
// 用户信息
|
||||
const userInfo = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
avatar: ''
|
||||
})
|
||||
|
||||
// 密码表单
|
||||
const passwordForm = reactive({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 系统偏好
|
||||
const preferences = reactive({
|
||||
theme: 'auto',
|
||||
language: 'zh-CN',
|
||||
notifications: true,
|
||||
autoExecute: false
|
||||
})
|
||||
|
||||
// 密码验证规则
|
||||
const passwordRules = {
|
||||
currentPassword: [
|
||||
{ required: true, message: '请输入当前密码', trigger: 'blur' }
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value) => value === passwordForm.newPassword,
|
||||
message: '两次输入的密码不一致',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 选项数据
|
||||
const themeOptions = [
|
||||
{ label: '跟随系统', value: 'auto' },
|
||||
{ label: '浅色主题', value: 'light' },
|
||||
{ label: '深色主题', value: 'dark' }
|
||||
]
|
||||
|
||||
const languageOptions = [
|
||||
{ label: '简体中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' }
|
||||
]
|
||||
|
||||
// 方法
|
||||
const saveProfile = async () => {
|
||||
try {
|
||||
// 这里应该调用API保存用户信息
|
||||
message.success('个人信息保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const changePassword = async () => {
|
||||
if (!passwordFormRef.value) return
|
||||
|
||||
try {
|
||||
await passwordFormRef.value.validate()
|
||||
|
||||
// 这里应该调用API修改密码
|
||||
message.success('密码修改成功')
|
||||
|
||||
// 清空表单
|
||||
Object.keys(passwordForm).forEach(key => {
|
||||
passwordForm[key] = ''
|
||||
})
|
||||
} catch (error) {
|
||||
// 验证失败
|
||||
}
|
||||
}
|
||||
|
||||
const savePreferences = () => {
|
||||
// 保存偏好设置
|
||||
localStorage.setItem('userPreferences', JSON.stringify(preferences))
|
||||
message.success('偏好设置保存成功')
|
||||
}
|
||||
|
||||
const updateTheme = (theme) => {
|
||||
preferences.theme = theme
|
||||
// 这里可以实际应用主题变更
|
||||
}
|
||||
|
||||
const changeAvatar = () => {
|
||||
message.info('头像更换功能开发中...')
|
||||
}
|
||||
|
||||
const setupTwoFactor = () => {
|
||||
message.info('两步验证设置功能开发中...')
|
||||
}
|
||||
|
||||
const viewLoginHistory = () => {
|
||||
message.info('登录历史查看功能开发中...')
|
||||
}
|
||||
|
||||
const exportData = () => {
|
||||
message.info('数据导出功能开发中...')
|
||||
}
|
||||
|
||||
const deleteAccount = () => {
|
||||
dialog.warning({
|
||||
title: '删除账户',
|
||||
content: '此操作将永久删除您的账户和所有数据,且无法恢复。确定要继续吗?',
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
message.error('账户删除功能暂未开放')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 加载用户信息
|
||||
if (authStore.userInfo) {
|
||||
Object.assign(userInfo, authStore.userInfo)
|
||||
}
|
||||
|
||||
// 加载用户偏好
|
||||
const savedPreferences = localStorage.getItem('userPreferences')
|
||||
if (savedPreferences) {
|
||||
try {
|
||||
Object.assign(preferences, JSON.parse(savedPreferences))
|
||||
} catch (error) {
|
||||
console.error('解析用户偏好失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-secondary);
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-lg);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-section {
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
padding-bottom: var(--spacing-xl);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.info-form {
|
||||
.n-form-item {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: var(--spacing-xl);
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.preferences-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-xl);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.preference-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.preference-label {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.security-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.security-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-lg);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--border-radius-medium);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
border-color: var(--error-color);
|
||||
background: rgba(208, 48, 80, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.security-info {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-card {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.preference-item,
|
||||
.security-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.preference-item .n-select,
|
||||
.preference-item .n-switch {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
333
src/views/Register.vue
Normal file
333
src/views/Register.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="register-page">
|
||||
<div class="register-container">
|
||||
<div class="register-card glass">
|
||||
<div class="card-header">
|
||||
<div class="brand">
|
||||
<img
|
||||
src="/icons/logo.png"
|
||||
alt="XYZW"
|
||||
class="brand-logo"
|
||||
>
|
||||
<h1 class="brand-title">
|
||||
注册 XYZW 账户
|
||||
</h1>
|
||||
</div>
|
||||
<p class="welcome-text">
|
||||
加入我们,开始您的游戏管理之旅
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<n-form
|
||||
ref="registerFormRef"
|
||||
:model="registerForm"
|
||||
:rules="registerRules"
|
||||
size="large"
|
||||
:show-label="false"
|
||||
>
|
||||
<n-form-item path="username">
|
||||
<n-input
|
||||
v-model:value="registerForm.username"
|
||||
placeholder="用户名"
|
||||
:input-props="{ autocomplete: 'username' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><PersonCircle /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="email">
|
||||
<n-input
|
||||
v-model:value="registerForm.email"
|
||||
placeholder="邮箱地址"
|
||||
:input-props="{ autocomplete: 'email' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><Mail /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
v-model:value="registerForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
:input-props="{ autocomplete: 'new-password' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><Lock /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="confirmPassword">
|
||||
<n-input
|
||||
v-model:value="registerForm.confirmPassword"
|
||||
type="password"
|
||||
placeholder="确认密码"
|
||||
:input-props="{ autocomplete: 'new-password' }"
|
||||
@keydown.enter="handleRegister"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><Lock /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<div class="form-options">
|
||||
<n-checkbox v-model:checked="registerForm.agreeTerms">
|
||||
我已阅读并同意
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="showTerms = true"
|
||||
>
|
||||
服务条款
|
||||
</n-button>
|
||||
和
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="showPrivacy = true"
|
||||
>
|
||||
隐私政策
|
||||
</n-button>
|
||||
</n-checkbox>
|
||||
</div>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="authStore.isLoading"
|
||||
:disabled="!registerForm.agreeTerms"
|
||||
class="register-button"
|
||||
@click="handleRegister"
|
||||
>
|
||||
注册账户
|
||||
</n-button>
|
||||
</n-form>
|
||||
|
||||
<div class="login-prompt">
|
||||
<span>已有账户?</span>
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
@click="router.push('/login')"
|
||||
>
|
||||
立即登录
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { PersonCircle, Lock, Mail } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
const registerFormRef = ref(null)
|
||||
|
||||
// 注册表单数据
|
||||
const registerForm = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agreeTerms: false
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const registerRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入用户名',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
min: 3,
|
||||
max: 20,
|
||||
message: '用户名长度应在3-20个字符之间',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
email: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入邮箱地址',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
message: '请输入正确的邮箱格式',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: '密码长度不能少于6位',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
confirmPassword: [
|
||||
{
|
||||
required: true,
|
||||
message: '请确认密码',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
return value === registerForm.password
|
||||
},
|
||||
message: '两次输入的密码不一致',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 处理注册
|
||||
const handleRegister = async () => {
|
||||
if (!registerFormRef.value) return
|
||||
|
||||
try {
|
||||
await registerFormRef.value.validate()
|
||||
|
||||
if (!registerForm.agreeTerms) {
|
||||
message.warning('请先同意服务条款和隐私政策')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await authStore.register({
|
||||
username: registerForm.username,
|
||||
email: registerForm.email,
|
||||
password: registerForm.password
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
message.success('注册成功,请登录')
|
||||
router.push('/login')
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration validation failed:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.register-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.register-container {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.register-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-2xl);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-md);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
.n-form {
|
||||
.n-form-item {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-options {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
|
||||
:deep(.n-checkbox) {
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
.register-button {
|
||||
height: 48px;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.login-prompt {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
|
||||
span {
|
||||
margin-right: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.register-card {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1044
src/views/TokenImport.vue
Normal file
1044
src/views/TokenImport.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user