Initial public release

This commit is contained in:
steve
2025-08-22 17:30:53 +08:00
commit 498849dc1e
64 changed files with 20416 additions and 0 deletions

78
src/App.vue Normal file
View 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
View 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

View 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);
}
}

View 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;
}

View 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>

View 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>

View 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>

View 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 (例如: {&quot;clientVersion&quot;: &quot;1.65.3-wx&quot;})"
: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>

View 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>

View 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>

View 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>

View 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="例如: {&quot;roleId&quot;: 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
View 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
View 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
View 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
View 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
}
})

View 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
View 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()
// 优先使用rawDataProtoMsg自动解码然后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
View 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 → rawDatabon.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;
}
/** 对外encodebon.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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>&copy; 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff