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

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>