Initial public release
This commit is contained in:
582
src/components/DailyTaskCard.vue
Normal file
582
src/components/DailyTaskCard.vue
Normal file
@@ -0,0 +1,582 @@
|
||||
<template>
|
||||
<div
|
||||
class="daily-task-card"
|
||||
:class="{ completed: task.completed }"
|
||||
>
|
||||
<!-- 卡片头部 -->
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<img
|
||||
:src="task.icon || '/icons/ta.png'"
|
||||
:alt="task.title"
|
||||
class="task-icon"
|
||||
>
|
||||
<div class="title-container">
|
||||
<h3 class="title">
|
||||
{{ task.title }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="task.subtitle"
|
||||
class="subtitle"
|
||||
>
|
||||
{{ task.subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="{ completed: task.completed }"
|
||||
@click="toggleStatus"
|
||||
>
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="{ completed: task.completed }"
|
||||
/>
|
||||
<span>{{ task.completed ? '已完成' : '待完成' }}</span>
|
||||
</div>
|
||||
|
||||
<n-button
|
||||
text
|
||||
class="settings-button"
|
||||
@click="showSettings = true"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon class="settings-icon">
|
||||
<Settings />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度信息 -->
|
||||
<div
|
||||
v-if="task.progress"
|
||||
class="progress-container"
|
||||
>
|
||||
<div class="info-container">
|
||||
<div class="info-item">
|
||||
<span class="info-label">当前进度</span>
|
||||
<span class="info-value">{{ task.progress.current }}/{{ task.progress.total }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.reward"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">奖励</span>
|
||||
<span class="info-value">{{ task.reward }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.nextReset"
|
||||
class="info-item"
|
||||
>
|
||||
<span class="info-label">重置时间</span>
|
||||
<span class="info-value">{{ formatResetTime(task.nextReset) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div
|
||||
v-if="!task.completed"
|
||||
class="actions-container"
|
||||
>
|
||||
<n-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="isExecuting"
|
||||
:disabled="!task.canExecute"
|
||||
class="complete-button"
|
||||
@click="executeTask"
|
||||
>
|
||||
{{ getButtonText() }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 设置模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showSettings"
|
||||
preset="card"
|
||||
title="任务设置"
|
||||
style="width: 480px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon class="modal-icon">
|
||||
<Settings />
|
||||
</n-icon>
|
||||
<span>{{ task.title }} - 设置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="settings-content">
|
||||
<div class="settings-grid">
|
||||
<div class="setting-item">
|
||||
<n-checkbox
|
||||
v-model:checked="taskSettings.autoExecute"
|
||||
@update:checked="updateSetting('autoExecute', $event)"
|
||||
>
|
||||
自动执行
|
||||
</n-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">执行延迟 (秒)</label>
|
||||
<n-input-number
|
||||
v-model:value="taskSettings.delay"
|
||||
:min="0"
|
||||
:max="300"
|
||||
@update:value="updateSetting('delay', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<n-checkbox
|
||||
v-model:checked="taskSettings.notification"
|
||||
@update:checked="updateSetting('notification', $event)"
|
||||
>
|
||||
完成通知
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务详情 -->
|
||||
<div
|
||||
v-if="task.details"
|
||||
class="task-details"
|
||||
>
|
||||
<h4>任务详情</h4>
|
||||
<div class="task-list">
|
||||
<div
|
||||
v-for="detail in task.details"
|
||||
:key="detail.id"
|
||||
class="task-item"
|
||||
>
|
||||
<div class="task-item-left">
|
||||
<n-icon
|
||||
class="task-status-icon"
|
||||
:class="{ completed: detail.completed }"
|
||||
>
|
||||
<CheckCircle v-if="detail.completed" />
|
||||
<Clock v-else />
|
||||
</n-icon>
|
||||
<span class="task-name">{{ detail.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 执行日志 -->
|
||||
<div
|
||||
v-if="task.logs && task.logs.length"
|
||||
class="execution-log"
|
||||
>
|
||||
<h4>执行日志</h4>
|
||||
<div class="log-container">
|
||||
<div
|
||||
v-for="log in task.logs.slice(-5)"
|
||||
:key="log.id"
|
||||
class="log-item"
|
||||
>
|
||||
<span class="log-time">{{ formatLogTime(log.timestamp) }}</span>
|
||||
<span
|
||||
class="log-message"
|
||||
:class="{
|
||||
error: log.type === 'error',
|
||||
success: log.type === 'success'
|
||||
}"
|
||||
>
|
||||
{{ log.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { Settings, Checkmark as CheckCircle, Time as Clock } from '@vicons/ionicons5'
|
||||
|
||||
const props = defineProps({
|
||||
task: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:task', 'execute', 'toggle-status'])
|
||||
|
||||
const message = useMessage()
|
||||
const showSettings = ref(false)
|
||||
const isExecuting = ref(false)
|
||||
|
||||
// 任务设置
|
||||
const taskSettings = ref({
|
||||
autoExecute: props.task.settings?.autoExecute || false,
|
||||
delay: props.task.settings?.delay || 0,
|
||||
notification: props.task.settings?.notification || true
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const getButtonText = () => {
|
||||
if (isExecuting.value) return '执行中...'
|
||||
if (!props.task.canExecute) return '不可执行'
|
||||
return '立即执行'
|
||||
}
|
||||
|
||||
// 方法
|
||||
const toggleStatus = () => {
|
||||
emit('toggle-status', props.task.id)
|
||||
}
|
||||
|
||||
const executeTask = async () => {
|
||||
if (isExecuting.value || !props.task.canExecute) return
|
||||
|
||||
try {
|
||||
isExecuting.value = true
|
||||
await emit('execute', props.task.id)
|
||||
|
||||
if (taskSettings.value.notification) {
|
||||
message.success(`任务 "${props.task.title}" 执行成功`)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(`任务执行失败: ${error.message}`)
|
||||
} finally {
|
||||
isExecuting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateSetting = (key, value) => {
|
||||
taskSettings.value[key] = value
|
||||
|
||||
// 发出设置更新事件
|
||||
emit('update:task', {
|
||||
...props.task,
|
||||
settings: { ...taskSettings.value }
|
||||
})
|
||||
}
|
||||
|
||||
const formatResetTime = (timestamp) => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const formatLogTime = (timestamp) => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 监听任务设置变化
|
||||
watch(() => props.task.settings, (newSettings) => {
|
||||
if (newSettings) {
|
||||
taskSettings.value = { ...taskSettings.value, ...newSettings }
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.daily-task-card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--border-radius-large);
|
||||
box-shadow: var(--shadow-light);
|
||||
min-height: 184px;
|
||||
padding: var(--spacing-lg);
|
||||
position: relative;
|
||||
transition: all var(--transition-normal);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
border-left-color: var(--success-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: 9999px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background-color: rgba(24, 160, 88, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--text-secondary);
|
||||
border-radius: 50%;
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
&.completed {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
padding: var(--spacing-xs);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
color: var(--text-secondary);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.complete-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
||||
&.n-button--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// 模态框样式
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-details,
|
||||
.execution-log {
|
||||
margin-top: var(--spacing-xl);
|
||||
|
||||
h4 {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.task-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-status-icon {
|
||||
color: var(--text-tertiary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&.completed {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.task-name {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
font-size: var(--font-size-sm);
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 640px) {
|
||||
.daily-task-card {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.info-container {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
751
src/components/DailyTaskStatus.vue
Normal file
751
src/components/DailyTaskStatus.vue
Normal file
@@ -0,0 +1,751 @@
|
||||
<template>
|
||||
<div class="daily-task-container">
|
||||
<div class="task-header">
|
||||
<div class="header-left">
|
||||
<img src="/icons/174023274867420.png" alt="每日任务" class="task-icon">
|
||||
<div class="title-container">
|
||||
<h3>每日任务</h3>
|
||||
<p>当前进度</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div
|
||||
class="status-indicator"
|
||||
:class="{ completed: isAllCompleted }"
|
||||
@click="showTaskDetails = true"
|
||||
>
|
||||
<div class="status-dot" :class="{ completed: isAllCompleted }"></div>
|
||||
<span>任务详情</span>
|
||||
</div>
|
||||
|
||||
<button class="settings-button" @click="showSettings = true">
|
||||
<n-icon><Settings /></n-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-container">
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="progressPercentage"
|
||||
:height="8"
|
||||
:border-radius="4"
|
||||
:color="progressColor"
|
||||
rail-color="#f3f4f6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="info-container">
|
||||
右上角小齿轮有惊喜
|
||||
</div>
|
||||
|
||||
<!-- 一键执行按钮 -->
|
||||
<button
|
||||
class="execute-button"
|
||||
:disabled="isExecuting"
|
||||
@click="executeAllTasks"
|
||||
>
|
||||
<span v-if="isExecuting" class="loading-text">
|
||||
<svg class="loading-icon" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 22c5.421 0 10-4.579 10-10h-2c0 4.337-3.663 8-8 8s-8-3.663-8-8c0-4.336 3.663-8 8-8V2C6.579 2 2 6.58 2 12c0 5.421 4.579 10 10 10z"/>
|
||||
</svg>
|
||||
执行中...
|
||||
</span>
|
||||
<span v-else>一键补差</span>
|
||||
</button>
|
||||
|
||||
<!-- 任务设置模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showSettings"
|
||||
preset="card"
|
||||
title="任务设置"
|
||||
style="width: 400px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon><Settings /></n-icon>
|
||||
<span>任务设置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="settings-content">
|
||||
<div class="settings-grid">
|
||||
<!-- 竞技场设置 -->
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">竞技场阵容</label>
|
||||
<n-select
|
||||
v-model:value="taskSettings.arenaFormation"
|
||||
:options="formationOptions"
|
||||
size="small"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- BOSS设置 -->
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">BOSS阵容</label>
|
||||
<n-select
|
||||
v-model:value="taskSettings.bossFormation"
|
||||
:options="formationOptions"
|
||||
size="small"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- BOSS次数 -->
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">BOSS次数</label>
|
||||
<n-select
|
||||
v-model:value="taskSettings.bossTimes"
|
||||
:options="bossTimesOptions"
|
||||
size="small"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 功能开关 -->
|
||||
<div class="setting-switches">
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">领罐子</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.claimBottle"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">领挂机</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.claimHangUp"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">竞技场</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.arenaEnable"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">开宝箱</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.openBox"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">领取邮件奖励</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.claimEmail"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="switch-row">
|
||||
<span class="switch-label">付费招募</span>
|
||||
<n-switch
|
||||
v-model:value="taskSettings.payRecruit"
|
||||
@update:value="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<!-- 任务详情模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showTaskDetails"
|
||||
preset="card"
|
||||
title="每日任务详情"
|
||||
style="width: 400px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon><Calendar /></n-icon>
|
||||
<span>每日任务详情</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="task-list">
|
||||
<div
|
||||
v-for="task in dailyTasks"
|
||||
:key="task.id"
|
||||
class="task-item"
|
||||
>
|
||||
<div class="task-item-left">
|
||||
<n-icon
|
||||
class="task-status-icon"
|
||||
:class="{ completed: task.completed }"
|
||||
>
|
||||
<CheckmarkCircle v-if="task.completed" />
|
||||
<EllipseOutline v-else />
|
||||
</n-icon>
|
||||
<span class="task-name">{{ task.name }}</span>
|
||||
</div>
|
||||
<n-tag
|
||||
:type="task.completed ? 'success' : 'default'"
|
||||
size="small"
|
||||
>
|
||||
{{ task.completed ? '已完成' : '未完成' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<!-- 执行日志模态框 -->
|
||||
<n-modal
|
||||
v-model:show="showLog"
|
||||
preset="card"
|
||||
title="任务执行日志"
|
||||
style="width: 500px"
|
||||
>
|
||||
<template #header>
|
||||
<div class="modal-header">
|
||||
<n-icon><DocumentText /></n-icon>
|
||||
<span>任务执行日志</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="log-container" ref="logContainer">
|
||||
<div
|
||||
v-for="log in executionLogs"
|
||||
:key="log.id"
|
||||
class="log-item"
|
||||
>
|
||||
<span class="log-time">{{ formatTime(log.time) }}</span>
|
||||
<span
|
||||
class="log-message"
|
||||
:class="{
|
||||
error: log.type === 'error',
|
||||
success: log.type === 'success'
|
||||
}"
|
||||
>
|
||||
{{ log.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import {
|
||||
Settings,
|
||||
Calendar,
|
||||
CheckmarkCircle,
|
||||
EllipseOutline,
|
||||
DocumentText
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const showSettings = ref(false)
|
||||
const showTaskDetails = ref(false)
|
||||
const showLog = ref(false)
|
||||
const isExecuting = ref(false)
|
||||
const logContainer = ref(null)
|
||||
const executionLogs = ref([])
|
||||
|
||||
// 任务设置
|
||||
const taskSettings = ref({
|
||||
arenaFormation: 1,
|
||||
bossFormation: 1,
|
||||
bossTimes: 4,
|
||||
claimBottle: true,
|
||||
payRecruit: true,
|
||||
openBox: true,
|
||||
arenaEnable: true,
|
||||
claimHangUp: true,
|
||||
claimEmail: true
|
||||
})
|
||||
|
||||
// 每日任务列表
|
||||
const dailyTasks = ref([
|
||||
{ id: 1, name: '登录一次游戏', completed: false, loading: false },
|
||||
{ id: 2, name: '分享一次游戏', completed: false, loading: false },
|
||||
{ id: 3, name: '赠送好友3次金币', completed: false, loading: false },
|
||||
{ id: 4, name: '进行2次招募', completed: false, loading: false },
|
||||
{ id: 5, name: '领取5次挂机奖励', completed: false, loading: false },
|
||||
{ id: 6, name: '进行3次点金', completed: false, loading: false },
|
||||
{ id: 7, name: '开启3次宝箱', completed: false, loading: false },
|
||||
{ id: 12, name: '黑市购买1次物品(请设置采购清单)', completed: false, loading: false },
|
||||
{ id: 13, name: '进行1场竞技场战斗', completed: false, loading: false },
|
||||
{ id: 14, name: '收获1个任意盐罐', completed: false, loading: false }
|
||||
])
|
||||
|
||||
// 选项配置
|
||||
const formationOptions = [
|
||||
{ label: '阵容1', value: 1 },
|
||||
{ label: '阵容2', value: 2 },
|
||||
{ label: '阵容3', value: 3 },
|
||||
{ label: '阵容4', value: 4 }
|
||||
]
|
||||
|
||||
const bossTimesOptions = [
|
||||
{ label: '0次', value: 0 },
|
||||
{ label: '1次', value: 1 },
|
||||
{ label: '2次', value: 2 },
|
||||
{ label: '3次', value: 3 },
|
||||
{ label: '4次', value: 4 }
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const roleInfo = computed(() => {
|
||||
return tokenStore.gameData?.roleInfo
|
||||
})
|
||||
|
||||
const dailyTaskData = computed(() => {
|
||||
return roleInfo.value?.role?.dailyTask
|
||||
})
|
||||
|
||||
const progressPercentage = computed(() => {
|
||||
const current = dailyTaskData.value?.dailyPoint || 0
|
||||
return current > 100 ? 100 : current
|
||||
})
|
||||
|
||||
const isAllCompleted = computed(() => {
|
||||
return progressPercentage.value === 100
|
||||
})
|
||||
|
||||
const progressColor = computed(() => {
|
||||
return progressPercentage.value === 100 ? '#10b981' : '#3b82f6'
|
||||
})
|
||||
|
||||
// 更新任务完成状态
|
||||
const updateTaskStatus = () => {
|
||||
if (!dailyTaskData.value?.complete) return
|
||||
|
||||
const completed = dailyTaskData.value.complete
|
||||
dailyTasks.value.forEach(task => {
|
||||
const taskStatus = completed[task.id.toString()]
|
||||
task.completed = taskStatus === -1 // -1 表示已完成
|
||||
})
|
||||
}
|
||||
|
||||
// 添加日志
|
||||
const addLog = (message, type = 'info') => {
|
||||
const log = {
|
||||
id: Date.now() + Math.random(),
|
||||
time: new Date().toLocaleTimeString(),
|
||||
message,
|
||||
type
|
||||
}
|
||||
executionLogs.value.push(log)
|
||||
|
||||
// 自动滚动到底部
|
||||
nextTick(() => {
|
||||
if (logContainer.value) {
|
||||
logContainer.value.scrollTop = logContainer.value.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const saveSettings = () => {
|
||||
// 这里可以保存到 localStorage 或发送到服务器
|
||||
localStorage.setItem('taskSettings', JSON.stringify(taskSettings.value))
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeString) => {
|
||||
return timeString
|
||||
}
|
||||
|
||||
// 执行所有任务
|
||||
const executeAllTasks = async () => {
|
||||
if (!tokenStore.selectedToken || isExecuting.value) return
|
||||
|
||||
isExecuting.value = true
|
||||
showLog.value = true
|
||||
executionLogs.value = []
|
||||
|
||||
addLog('开始执行任务...')
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 获取角色信息
|
||||
addLog('获取角色信息...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'role_getroleinfo')
|
||||
addLog('获取角色信息成功', 'success')
|
||||
|
||||
// 执行各种任务
|
||||
if (taskSettings.value.claimHangUp) {
|
||||
addLog('领取挂机奖励...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'system_claimhangupreward')
|
||||
}
|
||||
|
||||
if (taskSettings.value.claimBottle) {
|
||||
addLog('领取盐罐奖励...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'bottlehelper_claim')
|
||||
}
|
||||
|
||||
if (taskSettings.value.payRecruit) {
|
||||
addLog('进行招募...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'hero_recruit', {
|
||||
byClub: false,
|
||||
recruitNumber: 1,
|
||||
recruitType: 3
|
||||
})
|
||||
}
|
||||
|
||||
if (taskSettings.value.openBox) {
|
||||
addLog('开启宝箱...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'item_openbox', {
|
||||
itemId: 2001,
|
||||
number: 3
|
||||
})
|
||||
}
|
||||
|
||||
if (taskSettings.value.arenaEnable) {
|
||||
addLog('进行竞技场战斗...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'fight_startareaarena', {
|
||||
targetId: 530479307
|
||||
})
|
||||
}
|
||||
|
||||
if (taskSettings.value.claimEmail) {
|
||||
addLog('领取邮件奖励...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'mail_claimallattachment', {
|
||||
category: 0
|
||||
})
|
||||
}
|
||||
|
||||
// 最后再次获取角色信息更新状态
|
||||
addLog('更新角色信息...')
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'role_getroleinfo')
|
||||
|
||||
addLog('任务执行完成!', 'success')
|
||||
message.success('任务处理完成')
|
||||
|
||||
} catch (error) {
|
||||
addLog(`任务执行失败: ${error.message}`, 'error')
|
||||
message.error('任务执行失败')
|
||||
} finally {
|
||||
isExecuting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听角色信息变化
|
||||
watch(dailyTaskData, () => {
|
||||
updateTaskStatus()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 初始化设置
|
||||
const initSettings = () => {
|
||||
const saved = localStorage.getItem('taskSettings')
|
||||
if (saved) {
|
||||
try {
|
||||
taskSettings.value = { ...taskSettings.value, ...JSON.parse(saved) }
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initSettings()
|
||||
updateTaskStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.daily-task-container {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-full);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
padding: var(--spacing-xs);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-medium);
|
||||
transition: background var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.info-container {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.execute-button {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-medium);
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 模态框样式
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.setting-switches {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.switch-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.task-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.task-status-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
&.completed {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) 0;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--text-tertiary);
|
||||
min-width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.task-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
673
src/components/GameStatus.vue
Normal file
673
src/components/GameStatus.vue
Normal file
@@ -0,0 +1,673 @@
|
||||
<template>
|
||||
<div class="game-status-container">
|
||||
<!-- 队伍状态 -->
|
||||
<TeamStatus />
|
||||
|
||||
<!-- 每日任务状态 -->
|
||||
<DailyTaskStatus />
|
||||
|
||||
<!-- 咸将塔状态 -->
|
||||
<TowerStatus />
|
||||
|
||||
<!-- 其他游戏状态卡片 -->
|
||||
<!-- 盐罐机器人状态 -->
|
||||
<div class="status-card bottle-helper">
|
||||
<div class="card-header">
|
||||
<img src="/icons/173746572831736.png" alt="盐罐图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>盐罐机器人</h3>
|
||||
<p>剩余时间</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: bottleHelper.isRunning }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ bottleHelper.isRunning ? '运行中' : '已停止' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="time-display">
|
||||
{{ formatTime(bottleHelper.remainingTime) }}
|
||||
</div>
|
||||
<button
|
||||
class="action-button"
|
||||
:class="{ active: bottleHelper.isRunning }"
|
||||
@click="handleBottleHelper"
|
||||
>
|
||||
{{ bottleHelper.isRunning ? '重启服务' : '启动服务' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 挂机状态 -->
|
||||
<div class="status-card hang-up">
|
||||
<div class="card-header">
|
||||
<img src="/icons/174061875626614.png" alt="挂机图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>挂机时间</h3>
|
||||
<p>已挂机:{{ formatTime(hangUp.elapsedTime) }}</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: hangUp.isActive }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ hangUp.isActive ? '挂机中' : '已完成' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="time-display">
|
||||
{{ formatTime(hangUp.remainingTime) }}
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button class="action-button secondary" @click="extendHangUp">
|
||||
加钟
|
||||
</button>
|
||||
<button class="action-button primary" @click="claimHangUpReward">
|
||||
领取奖励
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 俱乐部排位 -->
|
||||
<div class="status-card legion-match">
|
||||
<div class="card-header">
|
||||
<img src="/icons/1733492491706152.png" alt="俱乐部图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>俱乐部排位</h3>
|
||||
<p>赛事状态</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: legionMatch.isRegistered }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ legionMatch.isRegistered ? '已报名' : '未报名' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="description">
|
||||
每逢周三周四周五有比赛<br>
|
||||
立即报名参与精彩对决!
|
||||
</p>
|
||||
<button
|
||||
class="action-button"
|
||||
:disabled="legionMatch.isRegistered"
|
||||
@click="registerLegionMatch"
|
||||
>
|
||||
{{ legionMatch.isRegistered ? '已报名' : '立即报名' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 俱乐部签到 -->
|
||||
<div class="status-card legion-signin">
|
||||
<div class="card-header">
|
||||
<img src="/icons/1733492491706148.png" alt="签到图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>俱乐部签到</h3>
|
||||
<p>每日签到状态</p>
|
||||
</div>
|
||||
<div class="status-badge" :class="{ active: legionSignin.isSignedIn }">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ legionSignin.isSignedIn ? '已签到' : '待签到' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="club-name" v-if="legionSignin.clubName">
|
||||
当前俱乐部<br>
|
||||
<strong>{{ legionSignin.clubName }}</strong>
|
||||
</p>
|
||||
<p class="description" v-else>
|
||||
尚未加入任何俱乐部
|
||||
</p>
|
||||
<button
|
||||
class="action-button"
|
||||
:disabled="legionSignin.isSignedIn"
|
||||
@click="signInLegion"
|
||||
>
|
||||
{{ legionSignin.isSignedIn ? '已签到' : '立即签到' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 咸鱼大冲关 -->
|
||||
<div class="status-card study">
|
||||
<div class="card-header">
|
||||
<img src="/icons/1736425783912140.png" alt="学习图标" class="status-icon">
|
||||
<div class="status-info">
|
||||
<h3>咸鱼大冲关</h3>
|
||||
<p>每日知识挑战</p>
|
||||
</div>
|
||||
<div class="status-badge weekly">
|
||||
<div class="status-dot"></div>
|
||||
<span>每周任务</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="description">
|
||||
没有什么可以阻挡我求知的欲望!
|
||||
</p>
|
||||
<button
|
||||
class="action-button"
|
||||
:disabled="study.isAnswering"
|
||||
@click="startStudy"
|
||||
>
|
||||
<span v-if="study.isAnswering" class="loading-text">
|
||||
<svg class="loading-icon" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 22c5.421 0 10-4.579 10-10h-2c0 4.337-3.663 8-8 8s-8-3.663-8-8c0-4.336 3.663-8 8-8V2C6.579 2 2 6.58 2 12c0 5.421 4.579 10 10 10z"/>
|
||||
</svg>
|
||||
答题中...
|
||||
</span>
|
||||
<span v-else>一键答题</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import TeamStatus from './TeamStatus.vue'
|
||||
import DailyTaskStatus from './DailyTaskStatus.vue'
|
||||
import TowerStatus from './TowerStatus.vue'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const bottleHelper = ref({
|
||||
isRunning: false,
|
||||
remainingTime: 0,
|
||||
stopTime: 0
|
||||
})
|
||||
|
||||
const hangUp = ref({
|
||||
isActive: false,
|
||||
remainingTime: 0,
|
||||
elapsedTime: 0,
|
||||
lastTime: 0,
|
||||
hangUpTime: 0
|
||||
})
|
||||
|
||||
const legionMatch = ref({
|
||||
isRegistered: false
|
||||
})
|
||||
|
||||
const legionSignin = ref({
|
||||
isSignedIn: false,
|
||||
clubName: ''
|
||||
})
|
||||
|
||||
const study = ref({
|
||||
isAnswering: false
|
||||
})
|
||||
|
||||
|
||||
// 计算属性
|
||||
const roleInfo = computed(() => {
|
||||
return tokenStore.gameData?.roleInfo || null
|
||||
})
|
||||
|
||||
// 格式化时间 - 确保显示到秒
|
||||
const formatTime = (seconds) => {
|
||||
// 确保传入值为数字,并向下取整到秒
|
||||
const totalSeconds = Math.floor(Number(seconds) || 0)
|
||||
|
||||
if (totalSeconds <= 0) return '00:00:00'
|
||||
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const secs = totalSeconds % 60
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
const updateGameStatus = () => {
|
||||
if (!roleInfo.value) return
|
||||
|
||||
const role = roleInfo.value.role
|
||||
|
||||
// 更新盐罐机器人状态
|
||||
if (role.bottleHelpers) {
|
||||
const now = Date.now() / 1000
|
||||
bottleHelper.value.stopTime = role.bottleHelpers.helperStopTime
|
||||
bottleHelper.value.isRunning = role.bottleHelpers.helperStopTime > now
|
||||
// 确保剩余时间为整数秒
|
||||
bottleHelper.value.remainingTime = Math.max(0, Math.floor(role.bottleHelpers.helperStopTime - now))
|
||||
console.log('🤖 盐罐机器人状态更新:', {
|
||||
stopTime: role.bottleHelpers.helperStopTime,
|
||||
now: now,
|
||||
remainingTime: bottleHelper.value.remainingTime,
|
||||
isRunning: bottleHelper.value.isRunning
|
||||
})
|
||||
}
|
||||
|
||||
// 更新挂机状态
|
||||
if (role.hangUp) {
|
||||
const now = Date.now() / 1000
|
||||
hangUp.value.lastTime = role.hangUp.lastTime
|
||||
hangUp.value.hangUpTime = role.hangUp.hangUpTime
|
||||
|
||||
const elapsed = now - hangUp.value.lastTime
|
||||
if (elapsed <= hangUp.value.hangUpTime) {
|
||||
// 确保剩余时间为整数秒
|
||||
hangUp.value.remainingTime = Math.floor(hangUp.value.hangUpTime - elapsed)
|
||||
hangUp.value.isActive = true
|
||||
} else {
|
||||
hangUp.value.remainingTime = 0
|
||||
hangUp.value.isActive = false
|
||||
}
|
||||
// 确保已挂机时间为整数秒
|
||||
hangUp.value.elapsedTime = Math.floor(hangUp.value.hangUpTime - hangUp.value.remainingTime)
|
||||
|
||||
console.log('⏰ 挂机状态更新:', {
|
||||
lastTime: hangUp.value.lastTime,
|
||||
hangUpTime: hangUp.value.hangUpTime,
|
||||
elapsed: elapsed,
|
||||
remainingTime: hangUp.value.remainingTime,
|
||||
elapsedTime: hangUp.value.elapsedTime,
|
||||
isActive: hangUp.value.isActive
|
||||
})
|
||||
}
|
||||
|
||||
// 更新俱乐部排位状态
|
||||
if (role.statistics) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const todayTimestamp = today.getTime() / 1000
|
||||
|
||||
legionMatch.value.isRegistered =
|
||||
Number(role.statistics['last:legion:match:sign:up:time']) > todayTimestamp
|
||||
}
|
||||
|
||||
// 更新俱乐部签到状态
|
||||
if (role.statisticsTime) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const todayTimestamp = today.getTime() / 1000
|
||||
|
||||
legionSignin.value.isSignedIn =
|
||||
role.statisticsTime['legion:sign:in'] > todayTimestamp
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 定时器更新
|
||||
let timer = null
|
||||
const startTimer = () => {
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
// 更新盐罐机器人剩余时间
|
||||
if (bottleHelper.value.isRunning && bottleHelper.value.remainingTime > 0) {
|
||||
bottleHelper.value.remainingTime = Math.max(0, bottleHelper.value.remainingTime - 1)
|
||||
if (bottleHelper.value.remainingTime <= 0) {
|
||||
bottleHelper.value.isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新挂机剩余时间
|
||||
if (hangUp.value.isActive && hangUp.value.remainingTime > 0) {
|
||||
hangUp.value.remainingTime = Math.max(0, hangUp.value.remainingTime - 1)
|
||||
hangUp.value.elapsedTime = hangUp.value.elapsedTime + 1
|
||||
if (hangUp.value.remainingTime <= 0) {
|
||||
hangUp.value.isActive = false
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 盐罐机器人操作
|
||||
const handleBottleHelper = () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 停止后重启
|
||||
tokenStore.sendMessage(tokenId, 'bottlehelper_stop')
|
||||
setTimeout(() => {
|
||||
tokenStore.sendMessage(tokenId, 'bottlehelper_start')
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
}, 500)
|
||||
|
||||
message.info(bottleHelper.value.isRunning ? '重启盐罐机器人' : '启动盐罐机器人')
|
||||
}
|
||||
|
||||
// 挂机操作
|
||||
const extendHangUp = () => {
|
||||
if (!tokenStore.selectedToken) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 发送4次分享回调请求来加钟
|
||||
for (let i = 0; i < 4; i++) {
|
||||
setTimeout(() => {
|
||||
tokenStore.sendMessage(tokenId, 'system_mysharecallback', {
|
||||
isSkipShareCard: true,
|
||||
type: 2
|
||||
})
|
||||
}, i * 200)
|
||||
}
|
||||
|
||||
message.info('正在加钟...')
|
||||
}
|
||||
|
||||
const claimHangUpReward = () => {
|
||||
if (!tokenStore.selectedToken) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
// 领取挂机奖励
|
||||
tokenStore.sendMessage(tokenId, 'system_mysharecallback')
|
||||
tokenStore.sendMessage(tokenId, 'system_claimhangupreward')
|
||||
tokenStore.sendMessage(tokenId, 'system_mysharecallback', {
|
||||
isSkipShareCard: true,
|
||||
type: 2
|
||||
})
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
|
||||
message.info('领取挂机奖励')
|
||||
}
|
||||
|
||||
// 俱乐部排位报名
|
||||
const registerLegionMatch = () => {
|
||||
if (!tokenStore.selectedToken || legionMatch.value.isRegistered) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'legionmatch_rolesignup')
|
||||
|
||||
message.info('报名俱乐部排位')
|
||||
}
|
||||
|
||||
// 俱乐部签到
|
||||
const signInLegion = () => {
|
||||
if (!tokenStore.selectedToken || legionSignin.value.isSignedIn) return
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'legion_signin')
|
||||
tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
|
||||
message.info('俱乐部签到')
|
||||
}
|
||||
|
||||
// 学习答题
|
||||
const startStudy = () => {
|
||||
if (!tokenStore.selectedToken || study.value.isAnswering) return
|
||||
|
||||
study.value.isAnswering = true
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'study_startgame')
|
||||
|
||||
setTimeout(() => {
|
||||
study.value.isAnswering = false
|
||||
}, 3000)
|
||||
|
||||
message.info('开始答题')
|
||||
}
|
||||
|
||||
|
||||
// 监听角色信息变化
|
||||
watch(roleInfo, (newValue) => {
|
||||
if (newValue) {
|
||||
updateGameStatus()
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
updateGameStatus()
|
||||
startTimer()
|
||||
|
||||
// 获取俱乐部信息
|
||||
if (tokenStore.selectedToken) {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'legion_getinfo')
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.game-status-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
min-height: 200px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-full);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: rgba(156, 163, 175, 0.1);
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.active {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.weekly {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
&.energy {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.energy-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
.time-display {
|
||||
font-size: 1.5rem; /* text-2xl */
|
||||
font-weight: 700; /* font-bold */
|
||||
color: #111827; /* text-gray-900 */
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', monospace;
|
||||
letter-spacing: 0.1em;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.club-name {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.tower-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
.label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.tower-level {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-medium);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--primary-color-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: var(--secondary-color);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--secondary-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--info-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--info-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.game-status-container {
|
||||
grid-template-columns: 1fr;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.status-card {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
319
src/components/MessageTester.vue
Normal file
319
src/components/MessageTester.vue
Normal file
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div class="message-tester">
|
||||
<n-card
|
||||
title="消息加解密测试"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<!-- 选择Token -->
|
||||
<div>
|
||||
<n-select
|
||||
v-model:value="selectedTokenId"
|
||||
:options="tokenOptions"
|
||||
placeholder="选择要测试的Token"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket连接状态 -->
|
||||
<div v-if="selectedTokenId">
|
||||
<n-tag :type="wsStatusType">
|
||||
{{ wsStatusText }}
|
||||
</n-tag>
|
||||
<n-button
|
||||
v-if="wsStatus !== 'connected'"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
@click="connectWebSocket"
|
||||
>
|
||||
连接WebSocket
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 预设消息测试 -->
|
||||
<n-divider title-placement="left">
|
||||
预设消息测试
|
||||
</n-divider>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendHeartbeat"
|
||||
>
|
||||
💗 发送心跳
|
||||
</n-button>
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendGetRoleInfo"
|
||||
>
|
||||
👤 获取角色信息
|
||||
</n-button>
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendGetDataVersion"
|
||||
>
|
||||
📦 获取数据版本
|
||||
</n-button>
|
||||
<n-button
|
||||
:disabled="!canSendMessage"
|
||||
@click="sendSignIn"
|
||||
>
|
||||
📅 签到
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 自定义消息发送 -->
|
||||
<n-divider title-placement="left">
|
||||
自定义消息
|
||||
</n-divider>
|
||||
<div class="space-y-2">
|
||||
<n-input
|
||||
v-model:value="customCmd"
|
||||
placeholder="命令 (例如: role_getroleinfo)"
|
||||
class="w-full"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="customBody"
|
||||
type="textarea"
|
||||
placeholder="消息体 JSON (例如: {"clientVersion": "1.65.3-wx"})"
|
||||
:rows="3"
|
||||
class="w-full"
|
||||
/>
|
||||
<n-button
|
||||
:disabled="!canSendMessage || !customCmd"
|
||||
type="primary"
|
||||
@click="sendCustomMessage"
|
||||
>
|
||||
🚀 发送自定义消息
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 消息历史 -->
|
||||
<n-divider title-placement="left">
|
||||
消息历史
|
||||
</n-divider>
|
||||
<div class="message-history max-h-96 overflow-y-auto">
|
||||
<div
|
||||
v-for="(message, index) in messageHistory"
|
||||
:key="index"
|
||||
class="message-item p-3 mb-2 rounded border"
|
||||
:class="message.type === 'sent' ? 'bg-blue-50 border-blue-200' : 'bg-green-50 border-green-200'"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-semibold">
|
||||
{{ message.type === 'sent' ? '📤 发送' : '📨 接收' }}
|
||||
<span class="text-sm text-gray-500 ml-2">{{ formatTime(message.timestamp) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.cmd"
|
||||
class="text-sm"
|
||||
>
|
||||
<strong>命令:</strong> {{ message.cmd }}
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<n-collapse>
|
||||
<n-collapse-item
|
||||
title="查看详细数据"
|
||||
name="detail"
|
||||
>
|
||||
<pre class="text-xs bg-gray-100 p-2 rounded overflow-x-auto">{{ formatJSON(message.data) }}</pre>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="messageHistory.length === 0"
|
||||
class="text-center text-gray-500"
|
||||
>
|
||||
暂无消息历史
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useTokenStore } from '../stores/tokenStore'
|
||||
import { useGameRolesStore } from '../stores/gameRoles'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const gameRolesStore = useGameRolesStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const selectedTokenId = ref(null)
|
||||
const customCmd = ref('')
|
||||
const customBody = ref('{}')
|
||||
const messageHistory = ref([])
|
||||
|
||||
// 计算属性
|
||||
const tokenOptions = computed(() => {
|
||||
return gameRolesStore.gameRoles.map(role => ({
|
||||
label: role.name,
|
||||
value: role.id
|
||||
}))
|
||||
})
|
||||
|
||||
const wsStatus = computed(() => {
|
||||
return selectedTokenId.value ? tokenStore.getWebSocketStatus(selectedTokenId.value) : 'disconnected'
|
||||
})
|
||||
|
||||
const wsStatusType = computed(() => {
|
||||
switch (wsStatus.value) {
|
||||
case 'connected': return 'success'
|
||||
case 'connecting': return 'warning'
|
||||
case 'error': return 'error'
|
||||
default: return 'default'
|
||||
}
|
||||
})
|
||||
|
||||
const wsStatusText = computed(() => {
|
||||
switch (wsStatus.value) {
|
||||
case 'connected': return '🟢 已连接'
|
||||
case 'connecting': return '🟡 连接中'
|
||||
case 'error': return '🔴 连接错误'
|
||||
default: return '⚪ 未连接'
|
||||
}
|
||||
})
|
||||
|
||||
const canSendMessage = computed(() => {
|
||||
return selectedTokenId.value && wsStatus.value === 'connected'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const connectWebSocket = () => {
|
||||
if (!selectedTokenId.value) return
|
||||
|
||||
const role = gameRolesStore.gameRoles.find(r => r.id === selectedTokenId.value)
|
||||
if (role) {
|
||||
gameRolesStore.selectRole(role)
|
||||
message.success('正在建立WebSocket连接...')
|
||||
}
|
||||
}
|
||||
|
||||
const addToHistory = (type, data, cmd = null) => {
|
||||
messageHistory.value.unshift({
|
||||
type,
|
||||
timestamp: new Date().toISOString(),
|
||||
cmd,
|
||||
data
|
||||
})
|
||||
|
||||
// 保持历史记录在合理范围内
|
||||
if (messageHistory.value.length > 50) {
|
||||
messageHistory.value = messageHistory.value.slice(0, 50)
|
||||
}
|
||||
}
|
||||
|
||||
const sendHeartbeat = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendHeartbeat(selectedTokenId.value)
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: '_sys/ack' }, '_sys/ack')
|
||||
message.success('心跳消息已发送')
|
||||
} else {
|
||||
message.error('心跳消息发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendGetRoleInfo = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendGetRoleInfo(selectedTokenId.value)
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: 'role_getroleinfo' }, 'role_getroleinfo')
|
||||
message.success('角色信息请求已发送')
|
||||
} else {
|
||||
message.error('角色信息请求发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendGetDataVersion = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendGameMessage(selectedTokenId.value, 'system_getdatabundlever', { isAudit: false })
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: 'system_getdatabundlever' }, 'system_getdatabundlever')
|
||||
message.success('数据版本请求已发送')
|
||||
} else {
|
||||
message.error('数据版本请求发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendSignIn = () => {
|
||||
if (!canSendMessage.value) return
|
||||
|
||||
const success = tokenStore.sendGameMessage(selectedTokenId.value, 'system_signinreward', {})
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: 'system_signinreward' }, 'system_signinreward')
|
||||
message.success('签到请求已发送')
|
||||
} else {
|
||||
message.error('签到请求发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
const sendCustomMessage = () => {
|
||||
if (!canSendMessage.value || !customCmd.value) return
|
||||
|
||||
try {
|
||||
const body = JSON.parse(customBody.value || '{}')
|
||||
const success = tokenStore.sendGameMessage(selectedTokenId.value, customCmd.value, body)
|
||||
|
||||
if (success) {
|
||||
addToHistory('sent', { cmd: customCmd.value, body }, customCmd.value)
|
||||
message.success(`自定义消息 ${customCmd.value} 已发送`)
|
||||
|
||||
// 清空输入
|
||||
customCmd.value = ''
|
||||
customBody.value = '{}'
|
||||
} else {
|
||||
message.error('自定义消息发送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('消息体JSON格式错误: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleTimeString()
|
||||
}
|
||||
|
||||
const formatJSON = (data) => {
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
|
||||
// 监听WebSocket消息(模拟,实际需要在tokenStore中触发事件)
|
||||
watch(() => tokenStore.wsConnections, (connections) => {
|
||||
if (!selectedTokenId.value || !connections[selectedTokenId.value]) return
|
||||
|
||||
const connection = connections[selectedTokenId.value]
|
||||
if (connection.lastMessage) {
|
||||
const lastMessage = connection.lastMessage
|
||||
addToHistory('received', lastMessage.parsed, lastMessage.parsed?.cmd)
|
||||
}
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-tester {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.message-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
454
src/components/TeamStatus.vue
Normal file
454
src/components/TeamStatus.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div class="team-status-card">
|
||||
<div class="card-header">
|
||||
<img src="/icons/Ob7pyorzmHiJcbab2c25af264d0758b527bc1b61cc3b.png" alt="队伍图标" class="team-icon">
|
||||
<div class="team-info">
|
||||
<h3>队伍阵容</h3>
|
||||
<p>当前使用的战斗阵容</p>
|
||||
</div>
|
||||
<div class="team-selector">
|
||||
<button
|
||||
v-for="teamId in availableTeams"
|
||||
:key="teamId"
|
||||
@click="selectTeam(teamId)"
|
||||
:class="[
|
||||
'team-button',
|
||||
{ active: currentTeam === teamId }
|
||||
]"
|
||||
>
|
||||
{{ teamId }}
|
||||
</button>
|
||||
<button
|
||||
@click="refreshTeamData"
|
||||
class="team-button refresh-button"
|
||||
title="刷新队伍数据"
|
||||
>
|
||||
🔄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="team-display">
|
||||
<div class="current-team-info">
|
||||
<span class="label">当前阵容</span>
|
||||
<span class="team-number">阵容 {{ currentTeam }}</span>
|
||||
</div>
|
||||
|
||||
<div class="heroes-container">
|
||||
<div class="heroes-grid">
|
||||
<div
|
||||
v-for="hero in currentTeamHeroes"
|
||||
:key="hero.id || hero.name"
|
||||
class="hero-card"
|
||||
>
|
||||
<img
|
||||
v-if="hero.avatar"
|
||||
:src="hero.avatar"
|
||||
:alt="hero.name"
|
||||
class="hero-avatar"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="hero-placeholder"
|
||||
>
|
||||
{{ hero.name?.substring(0, 2) || '?' }}
|
||||
</div>
|
||||
<span class="hero-name">{{ hero.name || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!currentTeamHeroes.length" class="empty-team">
|
||||
<p>暂无队伍信息</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const currentTeam = ref(1)
|
||||
const availableTeams = ref([1, 2, 3, 4])
|
||||
|
||||
// 计算属性
|
||||
const presetTeamInfo = computed(() => {
|
||||
return tokenStore.gameData?.presetTeam || null
|
||||
})
|
||||
|
||||
const currentTeamHeroes = computed(() => {
|
||||
if (!presetTeamInfo.value) {
|
||||
console.log('👥 TeamStatus: presetTeamInfo 为空')
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('👥 TeamStatus: 当前队伍信息结构:', {
|
||||
presetTeamInfo: presetTeamInfo.value,
|
||||
currentTeam: currentTeam.value,
|
||||
hasPresetTeamInfo: !!presetTeamInfo.value.presetTeamInfo,
|
||||
presetTeamInfoKeys: presetTeamInfo.value.presetTeamInfo ? Object.keys(presetTeamInfo.value.presetTeamInfo) : []
|
||||
})
|
||||
|
||||
// 尝试多种可能的数据结构
|
||||
let teamData = null
|
||||
|
||||
// 方式1: 标准结构 presetTeamInfo[teamId].teamInfo
|
||||
if (presetTeamInfo.value.presetTeamInfo?.[currentTeam.value]?.teamInfo) {
|
||||
teamData = presetTeamInfo.value.presetTeamInfo[currentTeam.value].teamInfo
|
||||
console.log('👥 TeamStatus: 使用标准结构获取队伍数据')
|
||||
}
|
||||
// 方式2: 直接在presetTeamInfo[teamId]下
|
||||
else if (presetTeamInfo.value.presetTeamInfo?.[currentTeam.value]) {
|
||||
const teamInfo = presetTeamInfo.value.presetTeamInfo[currentTeam.value]
|
||||
if (typeof teamInfo === 'object' && !Array.isArray(teamInfo)) {
|
||||
teamData = teamInfo
|
||||
console.log('👥 TeamStatus: 使用直接结构获取队伍数据')
|
||||
}
|
||||
}
|
||||
// 方式3: 查找任何包含英雄数据的结构
|
||||
else if (presetTeamInfo.value.presetTeamInfo) {
|
||||
for (const [key, value] of Object.entries(presetTeamInfo.value.presetTeamInfo)) {
|
||||
if (value && typeof value === 'object') {
|
||||
// 查找包含heroId或类似字段的数据
|
||||
if (value.teamInfo || value.heroes || value.formation ||
|
||||
Object.values(value).some(v => v && v.heroId)) {
|
||||
teamData = value.teamInfo || value.heroes || value.formation || value
|
||||
console.log(`👥 TeamStatus: 在 ${key} 中找到队伍数据`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!teamData) {
|
||||
console.log('👥 TeamStatus: 未找到队伍数据')
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('👥 TeamStatus: 解析队伍数据:', teamData)
|
||||
|
||||
// 转换队伍信息为英雄数组
|
||||
const heroes = []
|
||||
|
||||
// 处理不同的数据格式
|
||||
if (Array.isArray(teamData)) {
|
||||
// 数组格式
|
||||
teamData.forEach((hero, index) => {
|
||||
if (hero && (hero.heroId || hero.id)) {
|
||||
heroes.push({
|
||||
id: hero.heroId || hero.id,
|
||||
name: getHeroName(hero.heroId || hero.id),
|
||||
position: index + 1,
|
||||
level: hero.level || 1
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (typeof teamData === 'object') {
|
||||
// 对象格式(position => hero)
|
||||
for (const [position, hero] of Object.entries(teamData)) {
|
||||
if (hero && (hero.heroId || hero.id)) {
|
||||
heroes.push({
|
||||
id: hero.heroId || hero.id,
|
||||
name: getHeroName(hero.heroId || hero.id),
|
||||
position: position,
|
||||
level: hero.level || 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('👥 TeamStatus: 解析出的英雄列表:', heroes)
|
||||
return heroes
|
||||
})
|
||||
|
||||
// 从presetTeamInfo获取可用队伍数量
|
||||
const updateAvailableTeams = () => {
|
||||
if (!presetTeamInfo.value?.presetTeamInfo) return
|
||||
|
||||
const teams = Object.keys(presetTeamInfo.value.presetTeamInfo)
|
||||
.map(Number)
|
||||
.filter(num => !isNaN(num))
|
||||
.sort((a, b) => a - b)
|
||||
|
||||
if (teams.length > 0) {
|
||||
availableTeams.value = teams
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前队伍
|
||||
const updateCurrentTeam = () => {
|
||||
if (presetTeamInfo.value?.presetTeamInfo?.useTeamId) {
|
||||
currentTeam.value = presetTeamInfo.value.presetTeamInfo.useTeamId
|
||||
}
|
||||
}
|
||||
|
||||
// 获取英雄名称(这里需要英雄数据字典)
|
||||
const getHeroName = (heroId) => {
|
||||
// 暂时返回英雄ID,后续可以添加英雄名称映射
|
||||
const heroNames = {
|
||||
1: '剑士',
|
||||
2: '法师',
|
||||
3: '弓手',
|
||||
4: '盗贼',
|
||||
5: '牧师'
|
||||
// 更多英雄映射...
|
||||
}
|
||||
|
||||
return heroNames[heroId] || `英雄${heroId}`
|
||||
}
|
||||
|
||||
// 选择队伍
|
||||
const selectTeam = (teamId) => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
currentTeam.value = teamId
|
||||
|
||||
// 发送切换队伍的消息
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
tokenStore.sendMessage(tokenId, 'presetteam_saveteam', { teamId })
|
||||
|
||||
message.info(`切换到阵容 ${teamId}`)
|
||||
}
|
||||
|
||||
// 监听预设队伍信息变化
|
||||
watch(presetTeamInfo, (newValue) => {
|
||||
if (newValue) {
|
||||
updateAvailableTeams()
|
||||
updateCurrentTeam()
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 刷新队伍数据
|
||||
const refreshTeamData = () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
console.log('👥 手动刷新队伍数据')
|
||||
|
||||
// 发送多个可能的队伍相关命令
|
||||
const commands = [
|
||||
'presetteam_getteam',
|
||||
'role_gettargetteam',
|
||||
'role_getroleinfo' // 角色信息中可能包含队伍数据
|
||||
]
|
||||
|
||||
commands.forEach(cmd => {
|
||||
tokenStore.sendMessage(tokenId, cmd, {})
|
||||
console.log(`👥 发送命令: ${cmd}`)
|
||||
})
|
||||
|
||||
message.info('正在刷新队伍数据...')
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 获取队伍信息
|
||||
refreshTeamData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.team-status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.team-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-info {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.team-selector {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.team-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.refresh-button {
|
||||
background: var(--success-color, #10b981);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: var(--success-color-dark, #059669);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
.current-team-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.team-number {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.heroes-container {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.heroes-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--border-radius-medium);
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-fast);
|
||||
min-width: 80px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hero-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.empty-team {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.team-selector {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.heroes-grid {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
507
src/components/TokenManager.vue
Normal file
507
src/components/TokenManager.vue
Normal file
@@ -0,0 +1,507 @@
|
||||
<template>
|
||||
<div class="token-manager">
|
||||
<div class="header">
|
||||
<h3>Token管理器</h3>
|
||||
<div class="header-actions">
|
||||
<n-button
|
||||
size="small"
|
||||
@click="refreshTokens"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><Refresh /></n-icon>
|
||||
</template>
|
||||
刷新
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="warning"
|
||||
@click="exportTokens"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><Download /></n-icon>
|
||||
</template>
|
||||
导出
|
||||
</n-button>
|
||||
<n-upload
|
||||
:show-file-list="false"
|
||||
accept=".json"
|
||||
@change="importTokens"
|
||||
>
|
||||
<n-button
|
||||
size="small"
|
||||
type="info"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><CloudUpload /></n-icon>
|
||||
</template>
|
||||
导入
|
||||
</n-button>
|
||||
</n-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户Token -->
|
||||
<div class="token-section">
|
||||
<h4>用户认证Token</h4>
|
||||
<div
|
||||
v-if="localTokenStore.userToken"
|
||||
class="token-item"
|
||||
>
|
||||
<div class="token-info">
|
||||
<span class="token-label">Token:</span>
|
||||
<span class="token-value">{{ maskToken(localTokenStore.userToken) }}</span>
|
||||
</div>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="error"
|
||||
@click="clearUserToken"
|
||||
>
|
||||
清除
|
||||
</n-button>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="empty-token"
|
||||
>
|
||||
<span>未设置用户Token</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏Token列表 -->
|
||||
<div class="token-section">
|
||||
<h4>游戏角色Token ({{ Object.keys(localTokenStore.gameTokens).length }}个)</h4>
|
||||
<div class="game-tokens-list">
|
||||
<div
|
||||
v-for="(tokenData, roleId) in localTokenStore.gameTokens"
|
||||
:key="roleId"
|
||||
class="game-token-item"
|
||||
>
|
||||
<div class="token-header">
|
||||
<div class="role-info">
|
||||
<span class="role-name">{{ tokenData.roleName }}</span>
|
||||
<span class="role-server">{{ tokenData.server }}</span>
|
||||
</div>
|
||||
<div class="token-actions">
|
||||
<n-button
|
||||
size="tiny"
|
||||
:type="getWSStatus(roleId) === 'connected' ? 'success' : 'default'"
|
||||
@click="toggleWebSocket(roleId, tokenData)"
|
||||
>
|
||||
{{ getWSStatus(roleId) === 'connected' ? '断开WS' : '连接WS' }}
|
||||
</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="warning"
|
||||
@click="regenerateToken(roleId)"
|
||||
>
|
||||
重生成
|
||||
</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="error"
|
||||
@click="removeToken(roleId)"
|
||||
>
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="token-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Token:</span>
|
||||
<span class="detail-value">{{ maskToken(tokenData.token) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">WebSocket URL:</span>
|
||||
<span class="detail-value">{{ tokenData.wsUrl }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">创建时间:</span>
|
||||
<span class="detail-value">{{ formatTime(tokenData.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">最后使用:</span>
|
||||
<span class="detail-value">{{ formatTime(tokenData.lastUsed) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">连接状态:</span>
|
||||
<n-tag
|
||||
size="small"
|
||||
:type="getWSStatusType(getWSStatus(roleId))"
|
||||
>
|
||||
{{ getWSStatusText(getWSStatus(roleId)) }}
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作 -->
|
||||
<div class="bulk-actions">
|
||||
<n-button
|
||||
type="warning"
|
||||
@click="cleanExpiredTokens"
|
||||
>
|
||||
清理过期Token
|
||||
</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
@click="clearAllTokens"
|
||||
>
|
||||
清除所有Token
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useMessage, useDialog } from 'naive-ui'
|
||||
import { useLocalTokenStore } from '@/stores/localTokenManager'
|
||||
import { useGameRolesStore } from '@/stores/gameRoles'
|
||||
import {
|
||||
Refresh,
|
||||
Download,
|
||||
CloudUpload
|
||||
} from '@vicons/ionicons5'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const localTokenStore = useLocalTokenStore()
|
||||
const gameRolesStore = useGameRolesStore()
|
||||
|
||||
// 方法
|
||||
const maskToken = (token) => {
|
||||
if (!token) return ''
|
||||
const len = token.length
|
||||
if (len <= 8) return token
|
||||
return token.substring(0, 4) + '***' + token.substring(len - 4)
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const getWSStatus = (roleId) => {
|
||||
return localTokenStore.getWebSocketStatus(roleId)
|
||||
}
|
||||
|
||||
const getWSStatusType = (status) => {
|
||||
switch (status) {
|
||||
case 'connected': return 'success'
|
||||
case 'error': return 'error'
|
||||
case 'connecting': return 'warning'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const getWSStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'connected': return '已连接'
|
||||
case 'error': return '连接错误'
|
||||
case 'connecting': return '连接中'
|
||||
default: return '未连接'
|
||||
}
|
||||
}
|
||||
|
||||
const refreshTokens = () => {
|
||||
localTokenStore.initTokenManager()
|
||||
message.success('Token数据已刷新')
|
||||
}
|
||||
|
||||
const clearUserToken = () => {
|
||||
dialog.warning({
|
||||
title: '清除用户Token',
|
||||
content: '确定要清除用户认证Token吗?这将会退出登录。',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
localTokenStore.clearUserToken()
|
||||
message.success('用户Token已清除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toggleWebSocket = (roleId, tokenData) => {
|
||||
const status = getWSStatus(roleId)
|
||||
|
||||
if (status === 'connected') {
|
||||
localTokenStore.closeWebSocketConnection(roleId)
|
||||
message.info('WebSocket连接已断开')
|
||||
} else {
|
||||
try {
|
||||
localTokenStore.createWebSocketConnection(roleId, tokenData.token, tokenData.wsUrl)
|
||||
message.success('正在建立WebSocket连接...')
|
||||
} catch (error) {
|
||||
message.error('建立WebSocket连接失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const regenerateToken = (roleId) => {
|
||||
dialog.info({
|
||||
title: '重新生成Token',
|
||||
content: '确定要为此角色重新生成游戏Token吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
const oldTokenData = localTokenStore.getGameToken(roleId)
|
||||
if (oldTokenData) {
|
||||
const newToken = 'game_token_' + Date.now() + '_' + Math.random().toString(36).substr(2, 16)
|
||||
localTokenStore.updateGameToken(roleId, {
|
||||
token: newToken,
|
||||
regeneratedAt: new Date().toISOString()
|
||||
})
|
||||
message.success('Token已重新生成')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const removeToken = (roleId) => {
|
||||
dialog.warning({
|
||||
title: '删除Token',
|
||||
content: '确定要删除此角色的游戏Token吗?这将断开相关的WebSocket连接。',
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
localTokenStore.removeGameToken(roleId)
|
||||
message.success('Token已删除')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const exportTokens = () => {
|
||||
try {
|
||||
const tokenData = localTokenStore.exportTokens()
|
||||
const dataStr = JSON.stringify(tokenData, null, 2)
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(dataBlob)
|
||||
link.download = `tokens_backup_${new Date().toISOString().split('T')[0]}.json`
|
||||
link.click()
|
||||
|
||||
message.success('Token数据已导出')
|
||||
} catch (error) {
|
||||
message.error('导出失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const importTokens = ({ file }) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const tokenData = JSON.parse(e.target.result)
|
||||
const result = localTokenStore.importTokens(tokenData)
|
||||
|
||||
if (result.success) {
|
||||
message.success(result.message)
|
||||
// 刷新游戏角色数据
|
||||
gameRolesStore.fetchGameRoles()
|
||||
} else {
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('导入失败:文件格式错误')
|
||||
}
|
||||
}
|
||||
reader.readAsText(file.file)
|
||||
}
|
||||
|
||||
const cleanExpiredTokens = () => {
|
||||
dialog.info({
|
||||
title: '清理过期Token',
|
||||
content: '确定要清理超过24小时未使用的Token吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
const cleanedCount = localTokenStore.cleanExpiredTokens()
|
||||
message.success(`已清理 ${cleanedCount} 个过期Token`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clearAllTokens = () => {
|
||||
dialog.error({
|
||||
title: '清除所有Token',
|
||||
content: '确定要清除所有游戏Token吗?这将断开所有WebSocket连接。此操作不可恢复!',
|
||||
positiveText: '确定清除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
localTokenStore.clearAllGameTokens()
|
||||
message.success('所有游戏Token已清除')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.token-manager {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--spacing-lg);
|
||||
margin: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.token-section {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
|
||||
h4 {
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
}
|
||||
|
||||
.token-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.token-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.token-label {
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.token-value {
|
||||
font-family: monospace;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.empty-token {
|
||||
padding: var(--spacing-md);
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.game-tokens-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.game-token-item {
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.token-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.role-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.role-server {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.token-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.token-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-primary);
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.token-item {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.token-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.token-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
404
src/components/TowerStatus.vue
Normal file
404
src/components/TowerStatus.vue
Normal file
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<div class="tower-status-card">
|
||||
<div class="card-header">
|
||||
<div class="header-info">
|
||||
<img src="/icons/1733492491706148.png" alt="爬塔图标" class="tower-icon">
|
||||
<div class="tower-info">
|
||||
<h3>咸将塔</h3>
|
||||
<p>一个不小心就过了</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="energy-display">
|
||||
<img src="/icons/xiaoyugan.png" alt="小鱼干" class="energy-icon">
|
||||
<span class="energy-count">{{ towerEnergy }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="tower-floor">
|
||||
<span class="label">当前层数</span>
|
||||
<span class="floor-number">{{ currentFloor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button
|
||||
:class="[
|
||||
'climb-button',
|
||||
{
|
||||
'active': canClimb,
|
||||
'disabled': !canClimb
|
||||
}
|
||||
]"
|
||||
:disabled="!canClimb"
|
||||
@click="startTowerClimb"
|
||||
>
|
||||
开始爬塔
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const tokenStore = useTokenStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 响应式数据
|
||||
const isClimbing = ref(false)
|
||||
|
||||
// 计算属性 - 从gameData中获取塔相关信息
|
||||
const roleInfo = computed(() => {
|
||||
const data = tokenStore.gameData?.roleInfo || null
|
||||
console.log('🗼 TowerStatus roleInfo 计算属性更新:', data)
|
||||
if (data?.role?.tower) {
|
||||
console.log('🗼 TowerStatus 发现tower数据:', data.role.tower)
|
||||
} else {
|
||||
console.log('🗼 TowerStatus 没有找到tower数据, gameData:', tokenStore.gameData)
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const currentFloor = computed(() => {
|
||||
const tower = roleInfo.value?.role?.tower
|
||||
console.log('🗼 TowerStatus currentFloor 计算属性更新')
|
||||
console.log('🗼 TowerStatus 输入的tower数据:', tower)
|
||||
console.log('🗼 TowerStatus 完整的roleInfo:', roleInfo.value)
|
||||
|
||||
if (!tower) {
|
||||
console.log('🗼 没有tower对象,显示默认值')
|
||||
return "0 - 0"
|
||||
}
|
||||
|
||||
if (!tower.id && tower.id !== 0) {
|
||||
console.log('🗼 没有塔ID或ID无效,显示默认值, tower.id:', tower.id)
|
||||
return "0 - 0"
|
||||
}
|
||||
|
||||
const towerId = tower.id
|
||||
const floor = Math.floor(towerId / 10) + 1
|
||||
const layer = towerId % 10 + 1
|
||||
const result = `${floor} - ${layer}`
|
||||
console.log(`🗼 计算层数: towerId=${towerId} -> floor=${floor}, layer=${layer} -> ${result}`)
|
||||
return result
|
||||
})
|
||||
|
||||
const towerEnergy = computed(() => {
|
||||
const tower = roleInfo.value?.role?.tower
|
||||
console.log('🗼 TowerStatus towerEnergy 计算属性更新')
|
||||
console.log('🗼 TowerStatus tower对象:', tower)
|
||||
|
||||
const energy = tower?.energy || 0
|
||||
console.log('🗼 TowerStatus 计算出的energy:', energy)
|
||||
return energy
|
||||
})
|
||||
|
||||
const canClimb = computed(() => {
|
||||
return towerEnergy.value > 0 && !isClimbing.value
|
||||
})
|
||||
|
||||
// 方法
|
||||
const startTowerClimb = async () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
message.warning('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
if (!canClimb.value) {
|
||||
message.warning('体力不足或正在爬塔中')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isClimbing.value = true
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
|
||||
message.info('开始爬塔挑战...')
|
||||
|
||||
// 发送爬塔命令
|
||||
await tokenStore.sendMessageWithPromise(tokenId, 'fight_starttower', {}, 10000)
|
||||
|
||||
message.success('爬塔命令已发送')
|
||||
|
||||
// 立即查询塔信息以获取最新状态
|
||||
console.log('🗼 爬塔完成,立即查询塔信息')
|
||||
await getTowerInfo()
|
||||
|
||||
// 再延迟查询一次确保数据同步
|
||||
setTimeout(async () => {
|
||||
console.log('🗼 延迟查询塔信息')
|
||||
await getTowerInfo()
|
||||
}, 3000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('爬塔失败:', error)
|
||||
message.error('爬塔失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
isClimbing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getTowerInfo = async () => {
|
||||
if (!tokenStore.selectedToken) {
|
||||
console.warn('🗼 getTowerInfo: 没有选中的Token')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenId = tokenStore.selectedToken.id
|
||||
console.log('🗼 getTowerInfo: 开始获取塔信息, tokenId:', tokenId)
|
||||
|
||||
// 检查WebSocket连接状态
|
||||
const wsStatus = tokenStore.getWebSocketStatus(tokenId)
|
||||
console.log('🗼 getTowerInfo: WebSocket状态:', wsStatus)
|
||||
|
||||
if (wsStatus !== 'connected') {
|
||||
console.warn('🗼 getTowerInfo: WebSocket未连接,无法获取数据')
|
||||
return
|
||||
}
|
||||
|
||||
// 首先获取角色信息,这包含了塔的数据
|
||||
console.log('🗼 getTowerInfo: 正在请求角色信息...')
|
||||
const roleResult = tokenStore.sendMessage(tokenId, 'role_getroleinfo')
|
||||
console.log('🗼 getTowerInfo: 角色信息请求结果:', roleResult)
|
||||
|
||||
// 直接请求塔信息
|
||||
console.log('🗼 getTowerInfo: 正在请求塔信息...')
|
||||
const towerResult = tokenStore.sendMessage(tokenId, 'tower_getinfo')
|
||||
console.log('🗼 getTowerInfo: 塔信息请求结果:', towerResult)
|
||||
|
||||
// 检查当前gameData状态
|
||||
console.log('🗼 getTowerInfo: 当前gameData:', tokenStore.gameData)
|
||||
console.log('🗼 getTowerInfo: 当前roleInfo:', tokenStore.gameData?.roleInfo)
|
||||
console.log('🗼 getTowerInfo: 当前tower数据:', tokenStore.gameData?.roleInfo?.role?.tower)
|
||||
|
||||
if (!roleResult && !towerResult) {
|
||||
console.error('🗼 getTowerInfo: 所有请求都失败了')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('🗼 getTowerInfo: 获取塔信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 监听WebSocket连接状态变化
|
||||
const wsStatus = computed(() => {
|
||||
if (!tokenStore.selectedToken) return 'disconnected'
|
||||
return tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
|
||||
})
|
||||
|
||||
// 监听WebSocket连接状态,连接成功后自动获取塔信息
|
||||
watch(wsStatus, (newStatus, oldStatus) => {
|
||||
console.log(`🗼 WebSocket状态变化: ${oldStatus} -> ${newStatus}`)
|
||||
|
||||
if (newStatus === 'connected' && oldStatus !== 'connected') {
|
||||
console.log('🗼 WebSocket已连接,自动获取塔信息')
|
||||
// 延迟一点时间让WebSocket完全就绪
|
||||
setTimeout(() => {
|
||||
getTowerInfo()
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听选中Token变化
|
||||
watch(() => tokenStore.selectedToken, (newToken, oldToken) => {
|
||||
if (newToken && newToken.id !== oldToken?.id) {
|
||||
console.log('🗼 Token已切换,获取新的塔信息')
|
||||
// 检查WebSocket是否已连接
|
||||
const status = tokenStore.getWebSocketStatus(newToken.id)
|
||||
if (status === 'connected') {
|
||||
getTowerInfo()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
console.log('🗼 TowerStatus 组件已挂载')
|
||||
console.log('🗼 当前选中Token:', tokenStore.selectedToken?.name)
|
||||
console.log('🗼 当前选中Token ID:', tokenStore.selectedToken?.id)
|
||||
console.log('🗼 当前WebSocket状态:', wsStatus.value)
|
||||
console.log('🗼 当前游戏数据:', tokenStore.gameData)
|
||||
console.log('🗼 当前roleInfo:', tokenStore.gameData?.roleInfo)
|
||||
console.log('🗼 当前tower数据:', tokenStore.gameData?.roleInfo?.role?.tower)
|
||||
|
||||
// 检查WebSocket客户端
|
||||
if (tokenStore.selectedToken) {
|
||||
const client = tokenStore.getWebSocketClient(tokenStore.selectedToken.id)
|
||||
console.log('🗼 WebSocket客户端:', client)
|
||||
console.log('🗼 WebSocket客户端状态:', client ? 'exists' : 'null')
|
||||
}
|
||||
|
||||
// 组件挂载时获取塔信息
|
||||
if (tokenStore.selectedToken && wsStatus.value === 'connected') {
|
||||
console.log('🗼 条件满足,开始获取塔信息')
|
||||
getTowerInfo()
|
||||
} else if (!tokenStore.selectedToken) {
|
||||
console.log('🗼 没有选中的Token,无法获取塔信息')
|
||||
} else {
|
||||
console.log('🗼 WebSocket未连接,等待连接后自动获取塔信息')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.tower-status-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-xl);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all var(--transition-normal);
|
||||
border-left: 4px solid #6366f1;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.tower-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tower-info {
|
||||
h3 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.energy-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
background: var(--bg-tertiary);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
|
||||
.energy-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.energy-count {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.tower-floor {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.floor-number {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-primary);
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
|
||||
.climb-button {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&.active {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #5855eb;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
margin-top: var(--spacing-sm);
|
||||
padding: var(--spacing-xs);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--border-radius-small);
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
|
||||
small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.energy-display {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
496
src/components/WebSocketTester.vue
Normal file
496
src/components/WebSocketTester.vue
Normal file
@@ -0,0 +1,496 @@
|
||||
<template>
|
||||
<div class="websocket-tester">
|
||||
<n-card
|
||||
title="WebSocket连接测试"
|
||||
class="mb-4"
|
||||
>
|
||||
<n-space
|
||||
direction="vertical"
|
||||
size="large"
|
||||
>
|
||||
<!-- 连接状态 -->
|
||||
<n-card
|
||||
title="连接状态"
|
||||
size="small"
|
||||
>
|
||||
<n-space align="center">
|
||||
<n-tag
|
||||
:type="getStatusType(status)"
|
||||
size="large"
|
||||
>
|
||||
{{ getStatusText(status) }}
|
||||
</n-tag>
|
||||
<n-button
|
||||
v-if="selectedRoleId && status !== 'connected'"
|
||||
type="primary"
|
||||
:loading="status === 'connecting'"
|
||||
@click="connectWebSocket"
|
||||
>
|
||||
连接WebSocket
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="status === 'connected'"
|
||||
type="error"
|
||||
@click="disconnectWebSocket"
|
||||
>
|
||||
断开连接
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<!-- 角色选择 -->
|
||||
<n-form-item label="选择角色">
|
||||
<n-select
|
||||
v-model:value="selectedRoleId"
|
||||
placeholder="请选择要测试的角色"
|
||||
:options="roleOptions"
|
||||
@update:value="onRoleChange"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<!-- 连接详情 -->
|
||||
<n-card
|
||||
v-if="connectionDetails"
|
||||
title="连接详情"
|
||||
size="small"
|
||||
>
|
||||
<n-descriptions
|
||||
:column="2"
|
||||
bordered
|
||||
size="small"
|
||||
>
|
||||
<n-descriptions-item label="角色ID">
|
||||
{{ connectionDetails.roleId }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="状态">
|
||||
<n-tag :type="getStatusType(connectionDetails.status)">
|
||||
{{ getStatusText(connectionDetails.status) }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="WebSocket URL">
|
||||
<n-text
|
||||
code
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
{{ connectionDetails.wsUrl }}
|
||||
</n-text>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="连接时间">
|
||||
{{ formatTime(connectionDetails.connectedAt) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item
|
||||
v-if="connectionDetails.lastError"
|
||||
label="最后错误"
|
||||
>
|
||||
<n-text type="error">
|
||||
{{ connectionDetails.lastError }}
|
||||
</n-text>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item
|
||||
v-if="connectionDetails.reconnectAttempt > 0"
|
||||
label="重连次数"
|
||||
>
|
||||
{{ connectionDetails.reconnectAttempt }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
|
||||
<!-- 游戏命令测试 -->
|
||||
<n-card
|
||||
v-if="status === 'connected'"
|
||||
title="游戏命令测试"
|
||||
size="small"
|
||||
>
|
||||
<n-space direction="vertical">
|
||||
<n-form-item label="选择命令">
|
||||
<n-select
|
||||
v-model:value="selectedCommand"
|
||||
placeholder="请选择要测试的命令"
|
||||
:options="commandOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item
|
||||
v-if="selectedCommand"
|
||||
label="命令参数 (JSON)"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="commandParams"
|
||||
type="textarea"
|
||||
placeholder="例如: {"roleId": 123456}"
|
||||
:rows="3"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-space>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!selectedCommand"
|
||||
:loading="sendingCommand"
|
||||
@click="sendCommand"
|
||||
>
|
||||
发送命令
|
||||
</n-button>
|
||||
<n-button
|
||||
type="success"
|
||||
:disabled="!selectedCommand"
|
||||
:loading="waitingResponse"
|
||||
@click="sendCommandWithPromise"
|
||||
>
|
||||
发送并等待响应
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<!-- 消息日志 -->
|
||||
<n-card
|
||||
title="消息日志"
|
||||
size="small"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="clearLog"
|
||||
>
|
||||
清空日志
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<div class="message-log">
|
||||
<div
|
||||
v-for="(msg, index) in messageLog"
|
||||
:key="index"
|
||||
class="message-item"
|
||||
:class="`message-${msg.type}`"
|
||||
>
|
||||
<div class="message-header">
|
||||
<n-tag
|
||||
:type="msg.type === 'sent' ? 'info' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
{{ msg.type === 'sent' ? '发送' : '接收' }}
|
||||
</n-tag>
|
||||
<n-text
|
||||
depth="3"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
{{ formatTime(msg.timestamp) }}
|
||||
</n-text>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<pre>{{ JSON.stringify(msg.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="messageLog.length === 0"
|
||||
class="no-messages"
|
||||
>
|
||||
暂无消息日志
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useTokenStore } from '../stores/tokenStore.js'
|
||||
|
||||
const message = useMessage()
|
||||
const tokenStore = useTokenStore()
|
||||
|
||||
// 响应式数据
|
||||
const selectedRoleId = ref(null)
|
||||
const status = ref('disconnected')
|
||||
const connectionDetails = ref(null)
|
||||
const selectedCommand = ref(null)
|
||||
const commandParams = ref('{}')
|
||||
const sendingCommand = ref(false)
|
||||
const waitingResponse = ref(false)
|
||||
const messageLog = ref([])
|
||||
|
||||
// Token选项
|
||||
const roleOptions = computed(() => {
|
||||
return tokenStore.gameTokens.map(token => ({
|
||||
label: `${token.name} (${token.server})`,
|
||||
value: token.id
|
||||
}))
|
||||
})
|
||||
|
||||
// 命令选项
|
||||
const commandOptions = [
|
||||
{ label: '获取角色信息', value: 'role_getroleinfo' },
|
||||
{ label: '获取数据包版本', value: 'system_getdatabundlever' },
|
||||
{ label: '签到奖励', value: 'system_signinreward' },
|
||||
{ label: '领取每日任务奖励', value: 'task_claimdailyreward' },
|
||||
{ label: '获取邮件列表', value: 'mail_getlist' },
|
||||
{ label: '领取所有邮件附件', value: 'mail_claimallattachment' },
|
||||
{ label: '获取军团信息', value: 'legion_getinfo' },
|
||||
{ label: '英雄招募', value: 'hero_recruit' },
|
||||
{ label: '领取挂机奖励', value: 'system_claimhangupreward' }
|
||||
]
|
||||
|
||||
// 方法
|
||||
const getStatusType = (statusValue) => {
|
||||
const typeMap = {
|
||||
connected: 'success',
|
||||
connecting: 'warning',
|
||||
disconnected: 'default',
|
||||
reconnecting: 'info',
|
||||
error: 'error'
|
||||
}
|
||||
return typeMap[statusValue] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (statusValue) => {
|
||||
const textMap = {
|
||||
connected: '已连接',
|
||||
connecting: '连接中',
|
||||
disconnected: '已断开',
|
||||
reconnecting: '重连中',
|
||||
error: '连接错误'
|
||||
}
|
||||
return textMap[statusValue] || '未知状态'
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return '-'
|
||||
return new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const onRoleChange = () => {
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
const updateStatus = () => {
|
||||
if (!selectedRoleId.value) {
|
||||
status.value = 'disconnected'
|
||||
connectionDetails.value = null
|
||||
return
|
||||
}
|
||||
|
||||
status.value = tokenStore.getWebSocketStatus(selectedRoleId.value)
|
||||
const connection = tokenStore.wsConnections[selectedRoleId.value]
|
||||
if (connection) {
|
||||
connectionDetails.value = {
|
||||
roleId: selectedRoleId.value,
|
||||
status: connection.status,
|
||||
connectedAt: connection.connectedAt,
|
||||
wsUrl: connection.wsUrl
|
||||
}
|
||||
} else {
|
||||
connectionDetails.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const connectWebSocket = async () => {
|
||||
if (!selectedRoleId.value) {
|
||||
message.error('请先选择Token')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
status.value = 'connecting'
|
||||
const token = tokenStore.gameTokens.find(t => t.id === selectedRoleId.value)
|
||||
|
||||
if (!token) {
|
||||
message.error('未找到Token数据')
|
||||
return
|
||||
}
|
||||
|
||||
tokenStore.createWebSocketConnection(
|
||||
selectedRoleId.value,
|
||||
token.token,
|
||||
token.wsUrl
|
||||
)
|
||||
|
||||
// 监听消息
|
||||
startMessageListener()
|
||||
|
||||
message.success('WebSocket连接已启动')
|
||||
} catch (error) {
|
||||
console.error('WebSocket连接失败:', error)
|
||||
message.error('WebSocket连接失败: ' + error.message)
|
||||
} finally {
|
||||
setTimeout(updateStatus, 1000) // 延迟更新状态以等待连接建立
|
||||
}
|
||||
}
|
||||
|
||||
const disconnectWebSocket = () => {
|
||||
if (!selectedRoleId.value) return
|
||||
|
||||
tokenStore.closeWebSocketConnection(selectedRoleId.value)
|
||||
status.value = 'disconnected'
|
||||
connectionDetails.value = null
|
||||
message.info('WebSocket连接已断开')
|
||||
}
|
||||
|
||||
const sendCommand = async () => {
|
||||
if (!selectedCommand.value) {
|
||||
message.error('请选择要发送的命令')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
sendingCommand.value = true
|
||||
|
||||
let params = {}
|
||||
if (commandParams.value.trim()) {
|
||||
params = JSON.parse(commandParams.value)
|
||||
}
|
||||
|
||||
const success = tokenStore.sendMessage(
|
||||
selectedRoleId.value,
|
||||
selectedCommand.value,
|
||||
params
|
||||
)
|
||||
|
||||
if (success) {
|
||||
addToLog('sent', {
|
||||
command: selectedCommand.value,
|
||||
params
|
||||
})
|
||||
message.success('命令发送成功')
|
||||
} else {
|
||||
message.error('命令发送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送命令失败:', error)
|
||||
message.error('发送命令失败: ' + error.message)
|
||||
} finally {
|
||||
sendingCommand.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const sendCommandWithPromise = async () => {
|
||||
if (!selectedCommand.value) {
|
||||
message.error('请选择要发送的命令')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
waitingResponse.value = true
|
||||
|
||||
let params = {}
|
||||
if (commandParams.value.trim()) {
|
||||
params = JSON.parse(commandParams.value)
|
||||
}
|
||||
|
||||
const response = await tokenStore.sendMessageWithPromise(
|
||||
selectedRoleId.value,
|
||||
selectedCommand.value,
|
||||
params
|
||||
)
|
||||
|
||||
addToLog('sent', {
|
||||
command: selectedCommand.value,
|
||||
params
|
||||
})
|
||||
|
||||
addToLog('received', response)
|
||||
|
||||
message.success('命令执行成功,已收到响应')
|
||||
} catch (error) {
|
||||
console.error('发送命令失败:', error)
|
||||
message.error('发送命令失败: ' + error.message)
|
||||
} finally {
|
||||
waitingResponse.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startMessageListener = () => {
|
||||
// 这里简化处理,实际应该通过WebSocket客户端的onMessage事件来接收消息
|
||||
// 由于消息处理在store中,这里只是演示
|
||||
}
|
||||
|
||||
const addToLog = (type, data) => {
|
||||
messageLog.value.unshift({
|
||||
type,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
// 限制日志条数
|
||||
if (messageLog.value.length > 100) {
|
||||
messageLog.value = messageLog.value.slice(0, 100)
|
||||
}
|
||||
}
|
||||
|
||||
const clearLog = () => {
|
||||
messageLog.value = []
|
||||
}
|
||||
|
||||
// 定时更新状态
|
||||
let statusTimer = null
|
||||
|
||||
onMounted(() => {
|
||||
// 定时更新连接状态
|
||||
statusTimer = setInterval(() => {
|
||||
if (selectedRoleId.value) {
|
||||
updateStatus()
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (statusTimer) {
|
||||
clearInterval(statusTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.websocket-tester {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.message-log {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e6;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.message-sent {
|
||||
border-left: 3px solid #2080f0;
|
||||
}
|
||||
|
||||
.message-received {
|
||||
border-left: 3px solid #18a058;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.message-content pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.no-messages {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user