feat(DailyTasks): 进入日常页面时自动加载阵容数据- 新增 loadTeamDataWithConnection 函数,用于检查 WebSocket 连接状态并加载阵容数据

- 在页面进入时调用该函数,确保 WebSocket 连接成功后再加载阵容数据
- 增加重试机制,提高加载成功率
- 优化错误处理,提升用户体验
This commit is contained in:
steve
2025-09-03 19:04:41 +08:00
parent b5e7615564
commit 29ce1d4683
6 changed files with 1029 additions and 748 deletions

View File

@@ -55,7 +55,7 @@
<!-- 一键执行按钮 -->
<button
class="execute-button"
:disabled="busy"
:disabled="busy || !isConnected"
@click="runDailyFix"
>
<span
@@ -73,6 +73,7 @@
</svg>
执行中...
</span>
<span v-else-if="!isConnected">WebSocket未连接</span>
<span v-else>一键补差</span>
</button>
@@ -158,6 +159,12 @@
v-model:value="settings.claimEmail"
/>
</div>
<div class="switch-row">
<span class="switch-label">黑市购买物品</span>
<n-switch
v-model:value="settings.blackMarketPurchase"
/>
</div>
<div class="switch-row">
<span class="switch-label">付费招募</span>
@@ -181,6 +188,14 @@
<div class="modal-header">
<n-icon><Calendar /></n-icon>
<span>每日任务详情</span>
<button
class="refresh-button"
:disabled="busy"
@click="handleRefreshTaskStatus"
>
<n-icon><Refresh /></n-icon>
刷新状态
</button>
</div>
</template>
@@ -238,7 +253,8 @@
class="log-message"
:class="{
error: logItem.type === 'error',
success: logItem.type === 'success'
success: logItem.type === 'success',
warning: logItem.type === 'warning'
}"
>
{{ logItem.message }}
@@ -258,7 +274,8 @@ import {
Calendar,
CheckmarkCircle,
EllipseOutline,
DocumentText
DocumentText,
Refresh
} from '@vicons/ionicons5'
const tokenStore = useTokenStore()
@@ -271,7 +288,7 @@ const showLog = ref(false)
const busy = ref(false)
const logContainer = ref(null)
// 任务设置 - 基于参考代码
// 任务设置
const settings = reactive({
arenaFormation: 1,
bossFormation: 1,
@@ -281,10 +298,11 @@ const settings = reactive({
openBox: true,
arenaEnable: true,
claimHangUp: true,
claimEmail: true
claimEmail: true,
blackMarketPurchase: true
})
// 每日任务列表 - 基于参考代码
// 每日任务列表
const tasks = ref([
{ id: 1, name: '登录一次游戏', completed: false, loading: false },
{ id: 2, name: '分享一次游戏', completed: false, loading: false },
@@ -302,7 +320,7 @@ const tasks = ref([
const formationOptions = [1,2,3,4].map(v => ({ label: `阵容${v}`, value: v }))
const bossTimesOptions = [0,1,2,3,4].map(v => ({ label: `${v}`, value: v }))
// 计算属性 - 基于参考代码逻辑
// 计算属性
const roleInfo = computed(() => {
return tokenStore.selectedTokenRoleInfo
})
@@ -311,12 +329,18 @@ const roleDailyPoint = computed(() => {
return roleInfo.value?.role?.dailyTask?.dailyPoint ?? 0
})
// 进度 - 基于参考代码
const dailyPoint = computed(() => Math.min(roleDailyPoint.value, 100))
const isFull = computed(() => dailyPoint.value >= 100)
const progressColor = computed(() => isFull.value ? '#10b981' : '#3b82f6')
// 日志系统 - 基于参考代码的log函数
// WebSocket连接状态
const isConnected = computed(() => {
if (!tokenStore.selectedToken) return false
const status = tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
return status === 'connected'
})
// 日志系统
const logList = ref([])
const LOG_MAX = 500
@@ -335,82 +359,468 @@ const log = (message, type = 'info') => {
})
}
// 超时执行器 - 使用tokenStore的sendMessageWithPromise
const callWithRetry = async (fn, opt = {}) => {
const timeoutMs = opt?.timeoutMs ?? 10000 // 降低超时时间到10秒
const retries = opt?.retries ?? 1
const delayMs = opt?.delayMs ?? 600
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const r = await Promise.race([
fn(),
new Promise((_, rej) => setTimeout(() => rej(new Error(`请求超时(${timeoutMs}ms)`)), timeoutMs))
])
return r
} catch (err) {
if (attempt === retries) throw err
await new Promise(res => setTimeout(res, delayMs * (attempt + 1)))
}
}
throw new Error('unexpected')
}
// 消息错误处理 - 基于参考代码的onRaw函数
const onRaw = (evt) => {
const err = evt?._raw?.error
if (err) log(String(err), 'error')
}
// 同步服务器任务完成状态 - 基于参考代码的 syncCompleteFromServer 函数
// 同步服务器任务完成状态
const syncCompleteFromServer = (resp) => {
if (!resp?.role?.dailyTask?.complete) return
if (!resp?.role?.dailyTask?.complete) {
log('角色信息中无任务完成数据', 'warning')
return
}
const complete = resp.role.dailyTask.complete
const isDone = (v) => v === -1
log('开始同步任务完成状态...')
log(`服务器返回的任务完成数据: ${JSON.stringify(complete)}`)
let syncedCount = 0
let completedCount = 0
// 先重置所有任务为未完成,然后根据服务器数据更新
tasks.value.forEach(task => {
task.completed = false
})
// 同步服务器返回的完成状态
Object.keys(complete).forEach(k => {
const id = Number(k)
const idx = tasks.value.findIndex(t => t.id === id)
if (idx >= 0) tasks.value[idx].completed = isDone(complete[k])
if (idx >= 0) {
const isCompleted = isDone(complete[k])
tasks.value[idx].completed = isCompleted
syncedCount++
if (isCompleted) {
completedCount++
}
log(`任务${id} "${tasks.value[idx].name}": ${isCompleted ? '已完成' : '未完成'}`,
isCompleted ? 'success' : 'info')
} else {
log(`服务器返回未知任务ID: ${id} (完成值: ${complete[k]})`, 'warning')
}
})
// 检查本地定义但服务器未返回的任务
tasks.value.forEach(task => {
if (!(task.id.toString() in complete)) {
log(`本地任务${task.id} "${task.name}" 在服务器数据中缺失`, 'warning')
}
})
log(`任务状态同步完成: ${completedCount}/${syncedCount} 已完成`)
log(`当前进度: ${roleDailyPoint.value}/100`)
}
// 刷新角色信息 - 使用tokenStore的方法
// 刷新角色信息
const refreshRoleInfo = async () => {
if (!tokenStore.selectedToken) {
throw new Error('没有选中的Token')
}
const tokenId = tokenStore.selectedToken.id
log('正在获取角色信息...')
try {
// 直接使用sendMessageWithPromise方法
log('正在获取角色信息...')
// 先检查tokenStore方法是否存在
if (typeof tokenStore.sendMessageWithPromise !== 'function') {
throw new Error('sendMessageWithPromise方法不存在')
}
const response = await tokenStore.sendMessageWithPromise(tokenId, 'role_getroleinfo', {}, 8000)
const response = await tokenStore.sendGetRoleInfo(tokenId)
log('角色信息获取成功', 'success')
// 更新gameData以便其他组件使用
// 同步任务状态
if (response) {
tokenStore.gameData.roleInfo = response
syncCompleteFromServer(response)
}
return response
} catch (error) {
log(`获取角色信息失败: ${error.message}`, 'error')
console.error('详细错误信息:', error)
throw error
}
}
// 一键补差 - 简化测试版本
// 执行单个游戏指令的封装
const executeGameCommand = async (tokenId, cmd, params = {}, description = '', timeout = 8000) => {
try {
if (description) log(`执行: ${description}`)
const result = await tokenStore.sendMessageWithPromise(tokenId, cmd, params, timeout)
if (description) log(`${description} - 成功`, 'success')
return result
} catch (error) {
if (description) log(`${description} - 失败: ${error.message}`, 'error')
throw error
}
}
// 检查是否今日可用(简化版本)
const isTodayAvailable = (statisticsTime) => {
if (!statisticsTime) return true
// 如果有时间戳,检查是否为今天
const today = new Date().toDateString()
const recordDate = new Date(statisticsTime).toDateString()
return today !== recordDate
}
// 获取今日BOSS ID
const getTodayBossId = () => {
const DAY_BOSS_MAP = [9904, 9905, 9901, 9902, 9903, 9904, 9905] // 周日~周六
const dayOfWeek = new Date().getDay()
return DAY_BOSS_MAP[dayOfWeek]
}
// 智能阵容切换辅助函数
const switchToFormationIfNeeded = async (tokenId, targetFormation, formationName, logFn) => {
try {
// 首先尝试从本地缓存获取当前阵容信息
const cachedTeamInfo = tokenStore.gameData?.presetTeam?.presetTeamInfo
let currentFormation = cachedTeamInfo?.useTeamId
if (currentFormation) {
logFn(`从缓存获取当前阵容: ${currentFormation}`)
} else {
// 缓存中没有数据,从服务器获取
logFn(`缓存中无阵容信息,从服务器获取...`)
const teamInfo = await executeGameCommand(tokenId, 'presetteam_getinfo', {}, '获取阵容信息')
currentFormation = teamInfo?.presetTeamInfo?.useTeamId
logFn(`从服务器获取当前阵容: ${currentFormation}`)
}
if (currentFormation === targetFormation) {
logFn(`当前已是${formationName}${targetFormation},无需切换`, 'success')
return false // 不需要切换
}
logFn(`当前阵容: ${currentFormation}, 目标阵容: ${targetFormation},开始切换...`)
await executeGameCommand(tokenId, 'presetteam_saveteam',
{ teamId: targetFormation }, `切换到${formationName}${targetFormation}`)
logFn(`成功切换到${formationName}${targetFormation}`, 'success')
return true // 已切换
} catch (error) {
logFn(`阵容检查失败,直接切换: ${error.message}`, 'warning')
// 如果检查失败,还是执行切换操作
try {
await executeGameCommand(tokenId, 'presetteam_saveteam',
{ teamId: targetFormation }, `强制切换到${formationName}${targetFormation}`)
return true
} catch (fallbackError) {
logFn(`强制切换也失败: ${fallbackError.message}`, 'error')
throw fallbackError
}
}
}
// 每日任务执行器
const executeDailyTasks = async (roleInfoResp, logFn, progressFn) => {
const tokenId = tokenStore.selectedToken.id
const roleData = roleInfoResp?.role
if (!roleData) {
throw new Error('角色数据不存在')
}
logFn('开始执行每日任务补差')
// 检查已完成的任务
const completedTasks = roleData.dailyTask?.complete ?? {}
const isTaskCompleted = (taskId) => completedTasks[taskId] === -1
// 统计数据
const statistics = roleData.statistics ?? {}
const statisticsTime = roleData.statisticsTime ?? {}
// 构建任务列表
const taskList = []
// 1. 基础任务(根据完成状态决定是否执行)
// 分享游戏 (任务ID: 2)
if (!isTaskCompleted(2)) {
taskList.push({
name: '分享一次游戏',
execute: () => executeGameCommand(tokenId, 'system_mysharecallback',
{ isSkipShareCard: true, type: 2 }, '分享游戏')
})
}
// 赠送好友金币 (任务ID: 3)
if (!isTaskCompleted(3)) {
taskList.push({
name: '赠送好友金币',
execute: () => executeGameCommand(tokenId, 'friend_batch', {}, '赠送好友金币')
})
}
// 招募 (任务ID: 4)
if (!isTaskCompleted(4)) {
taskList.push({
name: '免费招募',
execute: () => executeGameCommand(tokenId, 'hero_recruit',
{ recruitType: 3, recruitNumber: 1 }, '免费招募')
})
if (settings.payRecruit) {
taskList.push({
name: '付费招募',
execute: () => executeGameCommand(tokenId, 'hero_recruit',
{ recruitType: 1, recruitNumber: 1 }, '付费招募')
})
}
}
// 点金 (任务ID: 6)
if (!isTaskCompleted(6) && isTodayAvailable(statisticsTime['buy:gold'])) {
for (let i = 0; i < 3; i++) {
taskList.push({
name: `免费点金 ${i + 1}/3`,
execute: () => executeGameCommand(tokenId, 'system_buygold',
{ buyNum: 1 }, `免费点金 ${i + 1}`)
})
}
}
// 挂机奖励 (任务ID: 5)
if (!isTaskCompleted(5) && settings.claimHangUp) {
// 先加钟4次
for (let i = 0; i < 4; i++) {
taskList.push({
name: `挂机加钟 ${i + 1}/4`,
execute: () => executeGameCommand(tokenId, 'system_mysharecallback',
{ isSkipShareCard: true, type: 2 }, `挂机加钟 ${i + 1}`)
})
}
// 然后领取奖励
taskList.push({
name: '领取挂机奖励',
execute: () => executeGameCommand(tokenId, 'system_claimhangupreward', {}, '领取挂机奖励')
})
// 最后再加1次钟
taskList.push({
name: '挂机加钟 5/5',
execute: () => executeGameCommand(tokenId, 'system_mysharecallback',
{ isSkipShareCard: true, type: 2 }, '挂机加钟 5')
})
}
// 开宝箱 (任务ID: 7)
if (!isTaskCompleted(7) && settings.openBox) {
taskList.push({
name: '开启木质宝箱',
execute: () => executeGameCommand(tokenId, 'item_openbox',
{ itemId: 2001, number: 10 }, '开启木质宝箱10个')
})
}
// 盐罐 (任务ID: 14)
if (!isTaskCompleted(14) && settings.claimBottle) {
taskList.push({
name: '领取盐罐奖励',
execute: () => executeGameCommand(tokenId, 'bottlehelper_claim', {}, '领取盐罐奖励')
})
}
// 2. 竞技场 (任务ID: 13)
if (!isTaskCompleted(13) && settings.arenaEnable) {
taskList.push({
name: '竞技场战斗',
execute: async () => {
logFn('开始竞技场战斗流程')
// 智能切换到竞技场阵容
await switchToFormationIfNeeded(tokenId, settings.arenaFormation, '竞技场阵容', logFn)
// 开始竞技场
await executeGameCommand(tokenId, 'arena_startarea', {}, '开始竞技场')
// 进行3场战斗
for (let i = 1; i <= 3; i++) {
logFn(`竞技场战斗 ${i}/3`)
// 获取目标
const targets = await executeGameCommand(tokenId, 'arena_getareatarget',
{ refresh: false }, `获取竞技场目标${i}`)
const targetId = targets?.roleList?.[0]?.roleId
if (targetId) {
await executeGameCommand(tokenId, 'fight_startareaarena',
{ targetId }, `竞技场战斗${i}`, 10000)
} else {
logFn(`竞技场战斗${i} - 未找到目标`, 'warning')
}
// 战斗间隔
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
})
}
// 3. BOSS战斗
if (settings.bossTimes > 0) {
// 军团BOSS
const alreadyLegionBoss = statistics['legion:boss'] ?? 0
const remainingLegionBoss = Math.max(settings.bossTimes - alreadyLegionBoss, 0)
if (remainingLegionBoss > 0) {
// 为军团BOSS智能切换阵容
taskList.push({
name: '军团BOSS阵容检查',
execute: () => switchToFormationIfNeeded(tokenId, settings.bossFormation, 'BOSS阵容', logFn)
})
for (let i = 0; i < remainingLegionBoss; i++) {
taskList.push({
name: `军团BOSS ${i + 1}/${remainingLegionBoss}`,
execute: () => executeGameCommand(tokenId, 'fight_startlegionboss', {}, `军团BOSS ${i + 1}`, 12000)
})
}
}
// 每日BOSS
const todayBossId = getTodayBossId()
if (remainingLegionBoss === 0) {
// 如果没有军团BOSS为每日BOSS切换阵容
taskList.push({
name: '每日BOSS阵容检查',
execute: () => switchToFormationIfNeeded(tokenId, settings.bossFormation, 'BOSS阵容', logFn)
})
}
for (let i = 0; i < 3; i++) {
taskList.push({
name: `每日BOSS ${i + 1}/3`,
execute: () => executeGameCommand(tokenId, 'fight_startboss',
{ bossId: todayBossId }, `每日BOSS ${i + 1}`, 12000)
})
}
}
// 4. 固定奖励领取
const fixedRewards = [
{ name: '福利签到', cmd: 'system_signinreward' },
{ name: '俱乐部签到', cmd: 'legion_signin' },
{ name: '领取每日礼包', cmd: 'discount_claimreward' },
{ name: '领取免费礼包', cmd: 'card_claimreward' },
{ name: '领取永久卡礼包', cmd: 'card_claimreward', params: { cardId: 4003 } }
]
if (settings.claimEmail) {
fixedRewards.push({ name: '领取邮件奖励', cmd: 'mail_claimallattachment' })
}
fixedRewards.forEach(reward => {
taskList.push({
name: reward.name,
execute: () => executeGameCommand(tokenId, reward.cmd, reward.params || {}, reward.name)
})
})
// 5. 免费活动
// 免费钓鱼
if (isTodayAvailable(statisticsTime['artifact:normal:lottery:time'])) {
for (let i = 0; i < 3; i++) {
taskList.push({
name: `免费钓鱼 ${i + 1}/3`,
execute: () => executeGameCommand(tokenId, 'artifact_lottery',
{ lotteryNumber: 1, newFree: true, type: 1 }, `免费钓鱼 ${i + 1}`)
})
}
}
// 灯神免费扫荡
const kingdoms = ['魏国', '蜀国', '吴国', '群雄']
for (let gid = 1; gid <= 4; gid++) {
if (isTodayAvailable(statisticsTime[`genie:daily:free:${gid}`])) {
taskList.push({
name: `${kingdoms[gid-1]}灯神免费扫荡`,
execute: () => executeGameCommand(tokenId, 'genie_sweep',
{ genieId: gid }, `${kingdoms[gid-1]}灯神免费扫荡`)
})
}
}
// 灯神免费扫荡卷
for (let i = 0; i < 3; i++) {
taskList.push({
name: `领取免费扫荡卷 ${i + 1}/3`,
execute: () => executeGameCommand(tokenId, 'genie_buysweep', {}, `领取免费扫荡卷 ${i + 1}`)
})
}
// 6. 黑市购买任务 (任务ID: 12)
if (!isTaskCompleted(12) && settings.blackMarketPurchase) {
taskList.push({
name: '黑市购买1次物品',
execute: () => executeGameCommand(tokenId, 'store_purchase', { goodsId: 1 }, '黑市购买1次物品')
})
}
// 7. 任务奖励领取
for (let taskId = 1; taskId <= 10; taskId++) {
taskList.push({
name: `领取任务奖励${taskId}`,
execute: () => executeGameCommand(tokenId, 'task_claimdailypoint',
{ taskId }, `领取任务奖励${taskId}`, 5000)
})
}
// 日常和周常奖励
taskList.push(
{
name: '领取日常任务奖励',
execute: () => executeGameCommand(tokenId, 'task_claimdailyreward', {}, '领取日常任务奖励')
},
{
name: '领取周常任务奖励',
execute: () => executeGameCommand(tokenId, 'task_claimweekreward', {}, '领取周常任务奖励')
}
)
// 执行任务列表
const totalTasks = taskList.length
logFn(`共有 ${totalTasks} 个任务待执行`)
for (let i = 0; i < taskList.length; i++) {
const task = taskList[i]
try {
await task.execute()
// 更新进度
const progress = Math.floor(((i + 1) / totalTasks) * 100)
if (progressFn) progressFn(tokenId, progress)
// 任务间隔
await new Promise(resolve => setTimeout(resolve, 500))
} catch (error) {
logFn(`任务执行失败: ${task.name} - ${error.message}`, 'error')
// 继续执行下一个任务
}
}
// 确保进度为100%
if (progressFn) progressFn(tokenId, 100)
logFn('所有任务执行完成', 'success')
// 最后刷新一次角色信息
await new Promise(resolve => setTimeout(resolve, 2000))
await refreshRoleInfo()
}
// 一键补差主函数
const runDailyFix = async () => {
if (!tokenStore.selectedToken || busy.value) {
log('没有选中Token或正在执行中', 'error')
message.warning('没有选中Token或正在执行中')
return
}
if (!isConnected.value) {
message.error('WebSocket连接未建立请检查连接状态')
return
}
@@ -418,209 +828,63 @@ const runDailyFix = async () => {
showLog.value = true
logList.value = []
log('开始执行任务...')
const tokenId = tokenStore.selectedToken.id
log(`当前Token ID: ${tokenId}`)
// 检查tokenStore方法
log(`检查tokenStore方法:`)
log(`- sendMessageWithPromise: ${typeof tokenStore.sendMessageWithPromise}`)
log(`- getWebSocketStatus: ${typeof tokenStore.getWebSocketStatus}`)
log(`- selectedToken: ${!!tokenStore.selectedToken}`)
// 检查WebSocket连接状态
const wsStatus = tokenStore.getWebSocketStatus(tokenId)
log(`WebSocket状态: ${JSON.stringify(wsStatus)}`)
// 修复状态检查逻辑 - wsStatus可能直接是字符串也可能是对象
const actualStatus = typeof wsStatus === 'string' ? wsStatus : wsStatus?.status
log(`实际状态: ${actualStatus}`)
if (actualStatus !== 'connected') {
log('WebSocket未连接无法继续执行', 'error')
busy.value = false
return
}
try {
log('尝试获取游戏内角色信息...')
log('发送 role_getroleinfo 命令到游戏服务器...')
log('期望响应: role_getroleinforesp')
log('=== 开始执行一键补差任务 ===')
// 先检查WebSocket客户端是否能接收到消息
tokenStore.setMessageListener((message) => {
if (message?.cmd) {
log(`收到游戏消息: ${message.cmd}`, 'info')
}
// 1. 获取角色信息
const roleInfo = await refreshRoleInfo()
if (!roleInfo?.role) {
throw new Error('获取角色信息失败或数据异常')
}
log(`当前每日任务进度: ${roleInfo.role.dailyTask?.dailyPoint || 0}/100`)
// 2. 执行任务
log('第二步: 开始执行每日任务...')
await executeDailyTasks(roleInfo, log, (tokenId, progress) => {
log(`任务进度: ${progress}%`)
})
// 增加超时时间,因为游戏服务器响应可能较慢
const roleInfo = await tokenStore.sendMessageWithPromise(tokenId, 'role_getroleinfo', {}, 10000)
log('=== 任务执行完成 ===', 'success')
message.success('每日任务补差执行完成')
if (roleInfo) {
log('游戏角色信息获取成功!', 'success')
// 显示角色基本信息(如果存在)
if (roleInfo.role) {
log(`角色等级: ${roleInfo.role.level || '未知'}`)
log(`角色名称: ${roleInfo.role.name || '未知'}`)
log(`每日任务进度: ${roleInfo.role.dailyTask?.dailyPoint || 0}/100`)
// 3. 最终刷新角色信息
setTimeout(async () => {
try {
await refreshRoleInfo()
log('最终角色信息刷新完成', 'success')
} catch (error) {
log(`最终刷新失败: ${error.message}`, 'warning')
}
// 现在尝试执行一个简单的游戏指令
log('尝试执行游戏内签到...')
const signInResult = await tokenStore.sendMessageWithPromise(tokenId, 'system_signinreward', {}, 8000)
if (signInResult) {
log('游戏签到执行成功!', 'success')
log(`签到奖励: ${signInResult.reward ? '有奖励' : '无奖励或已签到'}`)
}
message.success('游戏指令测试成功!')
} else {
log('未收到游戏角色数据', 'error')
message.warning('未收到游戏服务器响应')
}
}, 3000)
} catch (error) {
log(`游戏指令执行失败: ${error.message}`, 'error')
// 分析可能的原因
if (error.message.includes('超时') || error.message.includes('timeout')) {
log('可能原因:游戏服务器响应超时', 'error')
log('建议:检查网络连接或稍后重试', 'error')
} else if (error.message.includes('Unknown cmd')) {
log('可能原因游戏指令未在WebSocket客户端中注册', 'error')
} else {
log('可能原因WebSocket连接异常或游戏服务器拒绝请求', 'error')
}
message.error(`游戏指令执行失败: ${error.message}`)
log(`任务执行失败: ${error.message}`, 'error')
console.error('详细错误信息:', error)
message.error(`任务执行失败: ${error.message}`)
} finally {
busy.value = false
}
}
// 简化的执行器函数 - 直接使用tokenStore方法
const U = async (roleInfoResp, logFn, progress) => {
const tokenId = tokenStore.selectedToken.id
logFn?.('开始执行每日任务')
// 检查已完成的任务
const completedTasks = roleInfoResp.role?.dailyTask?.complete ?? {}
const isTaskCompleted = (taskId) => completedTasks[taskId] === -1
// 执行任务列表
const taskList = []
// 基础任务
if (!isTaskCompleted(2)) { // 分享游戏
taskList.push({ name: '分享一次游戏', cmd: 'system_mysharecallback', params: { isSkipShareCard: true, type: 2 } })
// 刷新任务状态
const handleRefreshTaskStatus = async () => {
if (!isConnected.value) {
message.warning('WebSocket未连接无法刷新任务状态')
return
}
if (!isTaskCompleted(3)) { // 赠送好友
taskList.push({ name: '赠送好友金币', cmd: 'friend_batch' })
try {
log('手动刷新任务状态...')
await refreshRoleInfo()
message.success('任务状态刷新成功')
} catch (error) {
log(`刷新失败: ${error.message}`, 'error')
message.error(`刷新失败: ${error.message}`)
}
if (!isTaskCompleted(4)) { // 招募
taskList.push({ name: '免费招募', cmd: 'hero_recruit' })
}
if (!isTaskCompleted(6)) { // 点金
taskList.push({ name: '免费点金', cmd: 'system_buygold' })
}
if (!isTaskCompleted(5) && settings.claimHangUp) { // 挂机奖励
taskList.push({ name: '领取挂机奖励', cmd: 'system_claimhangupreward' })
}
if (!isTaskCompleted(7) && settings.openBox) { // 开宝箱
taskList.push({ name: '开启宝箱', cmd: 'item_openbox', params: { itemId: 2001, number: 10 } })
}
if (!isTaskCompleted(14) && settings.claimBottle) { // 盐罐
taskList.push({ name: '领取盐罐奖励', cmd: 'bottlehelper_claim' })
}
// 常规奖励
taskList.push(
{ name: '福利签到', cmd: 'system_signinreward' },
{ name: '俱乐部签到', cmd: 'legion_signin' },
{ name: '领取每日礼包', cmd: 'discount_claimreward' },
{ name: '领取免费礼包', cmd: 'card_claimreward' }
)
if (settings.claimEmail) {
taskList.push({ name: '领取邮件奖励', cmd: 'mail_claimallattachment' })
}
// 任务奖励领取
for (let i = 1; i <= 10; i++) {
taskList.push({ name: `领取任务奖励${i}`, cmd: 'task_claimdailypoint', params: { taskId: i } })
}
taskList.push(
{ name: '领取日常任务奖励', cmd: 'task_claimdailyreward' },
{ name: '领取周常任务奖励', cmd: 'task_claimweekreward' }
)
// 竞技场
if (!isTaskCompleted(13) && settings.arenaEnable) {
logFn?.('开始竞技场战斗')
try {
// 获取队伍信息
const teamInfo = await tokenStore.sendMessageWithPromise(tokenId, 'presetteam_getinfo', {}, 5000)
// 切换阵容(如果需要)
if (teamInfo?.presetTeamInfo?.useTeamId !== settings.arenaFormation) {
await tokenStore.sendMessageWithPromise(tokenId, 'presetteam_saveteam', { teamId: settings.arenaFormation }, 5000)
logFn?.(`切换到竞技场阵容 ${settings.arenaFormation}`)
}
// 开始竞技场
await tokenStore.sendMessageWithPromise(tokenId, 'arena_startarea', {}, 5000)
// 进行3场战斗
for (let i = 1; i <= 3; i++) {
logFn?.(`竞技场战斗 ${i}/3`)
const targets = await tokenStore.sendMessageWithPromise(tokenId, 'arena_getareatarget', {}, 5000)
const targetId = targets?.roleList?.[0]?.roleId
if (targetId) {
await tokenStore.sendMessageWithPromise(tokenId, 'fight_startareaarena', { targetId }, 8000)
logFn?.(`完成竞技场战斗 ${i}`, 'success')
}
}
} catch (error) {
logFn?.(`竞技场战斗失败: ${error.message}`, 'error')
}
}
// 执行任务列表
const total = taskList.length
for (let i = 0; i < taskList.length; i++) {
const task = taskList[i]
logFn?.(task.name)
try {
await tokenStore.sendMessageWithPromise(tokenId, task.cmd, task.params || {}, 5000)
logFn?.(`${task.name} 成功`, 'success')
} catch (error) {
logFn?.(`${task.name} 失败: ${error.message}`, 'error')
}
// 更新进度
const progress_pct = Math.floor(((i + 1) / total) * 100)
if (progress && tokenId) progress(tokenId, progress_pct)
// 小延迟避免过快
await new Promise(resolve => setTimeout(resolve, 300))
}
// 确保进度为100%
if (progress && tokenId) progress(tokenId, 100)
logFn?.('所有任务执行完成', 'success')
}
// 辅助函数
@@ -646,7 +910,7 @@ const saveSettings = (roleId, s) => {
}
}
// 监听设置变化 - 基于参考代码的watch逻辑
// 监听设置变化
watch(settings, (cur) => {
const role = getCurrentRole()
if (role) saveSettings(role.roleId, cur)
@@ -655,29 +919,41 @@ watch(settings, (cur) => {
// 监听token选择变化
watch(() => tokenStore.selectedToken, async (newToken, oldToken) => {
if (newToken && newToken !== oldToken) {
log(`切换到Token: ${newToken.name}`)
// 加载新token的设置
const saved = loadSettings(newToken.id)
if (saved) Object.assign(settings, saved)
// 同步任务状态
syncCompleteFromServer(tokenStore.selectedTokenRoleInfo)
// 如果WebSocket已连接尝试获取最新角色信息
if (isConnected.value) {
try {
await refreshRoleInfo()
} catch (error) {
console.warn('切换token后获取角色信息失败:', error.message)
}
}
}
}, { immediate: true })
// 生命周期 - 基于参考代码的onMounted函数
// 监听角色信息变化,自动同步任务状态
watch(() => tokenStore.selectedTokenRoleInfo, (newRoleInfo) => {
if (newRoleInfo?.role?.dailyTask?.complete) {
log('角色信息更新,同步任务状态')
syncCompleteFromServer(newRoleInfo)
}
}, { immediate: true, deep: true })
// 生命周期
onMounted(async () => {
// 首次拉取角色信息如果有选中的token
if (tokenStore.selectedToken) {
log('组件初始化完成')
// 首次拉取角色信息如果有选中的token且已连接
if (tokenStore.selectedToken && isConnected.value) {
try {
await refreshRoleInfo()
} catch (error) {
console.warn('初始化时获取角色信息失败:', error.message)
// 如果获取失败,尝试发送普通消息(不等待响应)
try {
tokenStore.sendMessage(tokenStore.selectedToken.id, 'role_getroleinfo', {})
} catch (sendError) {
console.warn('发送角色信息请求失败:', sendError.message)
}
}
}
@@ -687,13 +963,11 @@ onMounted(async () => {
if (saved) Object.assign(settings, saved)
}
// 同步完成态(使用现有的角色信息)
syncCompleteFromServer(tokenStore.selectedTokenRoleInfo)
// 初始化时的任务状态同步会通过 watch selectedTokenRoleInfo 自动处理
})
// 清理监听 - 基于参考代码的onBeforeUnmount
onBeforeUnmount(() => {
tokenStore.setMessageListener(undefined)
log('组件即将卸载')
})
</script>
@@ -851,7 +1125,34 @@ onBeforeUnmount(() => {
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-sm);
width: 100%;
}
.refresh-button {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-medium);
background: white;
color: var(--text-secondary);
font-size: var(--font-size-sm);
cursor: pointer;
transition: all var(--transition-fast);
&:hover:not(:disabled) {
background: var(--bg-tertiary);
border-color: var(--primary-color);
color: var(--primary-color);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.settings-content {
@@ -941,7 +1242,7 @@ onBeforeUnmount(() => {
background: var(--bg-tertiary);
border-radius: var(--border-radius-medium);
padding: var(--spacing-md);
max-height: 300px;
max-height: 400px;
overflow-y: auto;
}
@@ -968,6 +1269,10 @@ onBeforeUnmount(() => {
&.success {
color: var(--success-color);
}
&.warning {
color: #f59e0b;
}
}
// 响应式设计

View File

@@ -2,30 +2,30 @@
<div class="team-status-card">
<div class="card-header">
<img
src="/icons/Ob7pyorzmHiJcbab2c25af264d0758b527bc1b61cc3b.png"
alt="队伍图标"
class="team-icon"
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"
:class="[
'team-button',
{ active: currentTeam === teamId }
]"
@click="selectTeam(teamId)"
v-for="teamId in availableTeams"
:key="teamId"
:disabled="loading || switching"
:class="['team-button', { active: currentTeam === teamId }]"
@click="selectTeam(teamId)"
>
{{ teamId }}
</button>
<button
class="team-button refresh-button"
title="刷新队伍数据"
@click="refreshTeamData"
class="team-button refresh-button"
:disabled="loading"
title="刷新队伍数据"
@click="refreshTeamData(true)"
>
🔄
</button>
@@ -36,236 +36,227 @@
<div class="team-display">
<div class="current-team-info">
<span class="label">当前阵容</span>
<span class="team-number">阵容 {{ currentTeam }}</span>
<span class="team-number">
<template v-if="!loading">阵容 {{ currentTeam }}</template>
<template v-else>加载中</template>
</span>
</div>
<div class="heroes-container">
<div class="heroes-grid">
<div v-if="!loading" class="heroes-inline">
<div
v-for="hero in currentTeamHeroes"
:key="hero.id || hero.name"
class="hero-card"
v-for="hero in currentTeamHeroes"
:key="hero.id || hero.name"
class="hero-item"
>
<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 class="hero-circle">
<img
v-if="hero.avatar"
:src="hero.avatar"
:alt="hero.name"
class="hero-avatar"
>
<div v-else class="hero-placeholder">
{{ hero.name?.substring(0, 2) || '?' }}
</div>
</div>
<span class="hero-name">{{ hero.name || '未知' }}</span>
</div>
</div>
<div
v-if="!currentTeamHeroes.length"
class="empty-team"
>
<div v-if="!loading && !currentTeamHeroes.length" class="empty-team">
<p>暂无队伍信息</p>
</div>
<div v-if="loading" class="empty-team"><p>正在加载队伍信息</p></div>
</div>
</div>
</div>
</div>
</template>
<script setup>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useTokenStore } from '@/stores/tokenStore'
import { useMessage } from 'naive-ui'
import { useMessage, NTag } from 'naive-ui'
/**
* 集成英雄字典游戏ID -> { name, type }
* 你也可以独立出一个 heroDict.ts 后 import按你的要求这里整合到同一文件。
*/
const HERO_DICT: Record<number, { name: string; type: string }> = {
101: { name: '司马懿', type: '魏国' }, 102: { name: '郭嘉', type: '魏国' }, 103: { name: '关羽', type: '蜀国' },
104: { name: '诸葛亮', type: '蜀国' }, 105: { name: '周瑜', type: '吴国' }, 106: { name: '太史慈', type: '吴国' },
107: { name: '吕布', type: '群雄' }, 108: { name: '华佗', type: '群雄' }, 109: { name: '甄姬', type: '魏国' },
110: { name: '黄月英', type: '蜀国' }, 111: { name: '孙策', type: '吴国' }, 112: { name: '贾诩', type: '群雄' },
113: { name: '曹仁', type: '魏国' }, 114: { name: '姜维', type: '蜀国' }, 115: { name: '孙坚', type: '吴国' },
116: { name: '公孙瓒', type: '群雄' }, 117: { name: '典韦', type: '魏国' }, 118: { name: '赵云', type: '蜀国' },
119: { name: '大乔', type: '吴国' }, 120: { name: '张角', type: '群雄' }, 201: { name: '徐晃', type: '魏国' },
202: { name: '荀彧', type: '魏国' }, 203: { name: '典韦', type: '魏国' }, 204: { name: '张飞', type: '蜀国' },
205: { name: '赵云', type: '蜀国' }, 206: { name: '庞统', type: '蜀国' }, 207: { name: '鲁肃', type: '吴国' },
208: { name: '陆逊', type: '吴国' }, 209: { name: '甘宁', type: '吴国' }, 210: { name: '貂蝉', type: '群雄' },
211: { name: '董卓', type: '群雄' }, 212: { name: '张角', type: '群雄' }, 213: { name: '张辽', type: '魏国' },
214: { name: '夏侯惇', type: '魏国' }, 215: { name: '许褚', type: '魏国' }, 216: { name: '夏侯渊', type: '魏国' },
217: { name: '魏延', type: '蜀国' }, 218: { name: '黄忠', type: '蜀国' }, 219: { name: '马超', type: '蜀国' },
220: { name: '马岱', type: '蜀国' }, 221: { name: '吕蒙', type: '吴国' }, 222: { name: '黄盖', type: '吴国' },
223: { name: '蔡文姬', type: '魏国' }, 224: { name: '小乔', type: '吴国' }, 225: { name: '袁绍', type: '群雄' },
226: { name: '华雄', type: '群雄' }, 227: { name: '颜良', type: '群雄' }, 228: { name: '文丑', type: '群雄' },
301: { name: '周泰', type: '吴国' }, 302: { name: '许攸', type: '魏国' }, 303: { name: '于禁', type: '魏国' },
304: { name: '张星彩', type: '蜀国' }, 305: { name: '关银屏', type: '蜀国' }, 306: { name: '关平', type: '蜀国' },
307: { name: '程普', type: '吴国' }, 308: { name: '张昭', type: '吴国' }, 309: { name: '陆绩', type: '吴国' },
310: { name: '吕玲绮', type: '群雄' }, 311: { name: '潘凤', type: '群雄' }, 312: { name: '邢道荣', type: '群雄' },
313: { name: '祝融夫人', type: '群雄' }, 314: { name: '孟获', type: '群雄' }
}
const tokenStore = useTokenStore()
const message = useMessage()
// 响应式数据
// 状态
const loading = ref(false)
const switching = ref(false)
const currentTeam = ref(1)
const availableTeams = ref([1, 2, 3, 4])
const availableTeams = ref<number[]>([1, 2, 3, 4])
// 计算属性
const presetTeamInfo = computed(() => {
return tokenStore.gameData?.presetTeam || null
})
// —— 缓存优先的 presetTeam 原始数据 ——
const presetTeamRaw = computed(() => tokenStore.gameData?.presetTeam ?? null)
// 统一结构:输出 { useTeamId, teams }
function normalizePresetTeam(raw: any): { useTeamId: number; teams: Record<number, { teamInfo: Record<string, any> }> } {
if (!raw) return { useTeamId: 1, teams: {} }
const root = raw.presetTeamInfo ?? raw
const findUseIdRec = (obj: any): number | null => {
if (!obj || typeof obj !== 'object') return null
if (typeof obj.useTeamId === 'number') return obj.useTeamId
for (const k of Object.keys(obj)) {
const v = findUseIdRec(obj[k])
if (v) return v
}
return null
}
const useTeamId = root.useTeamId ?? root.presetTeamInfo?.useTeamId ?? findUseIdRec(root) ?? 1
const dict = root.presetTeamInfo ?? root
const teams: Record<number, { teamInfo: Record<string, any> }> = {}
const ids = Object.keys(dict || {}).filter(k => /^\d+$/.test(k))
for (const idStr of ids) {
const id = Number(idStr)
const node = dict[idStr]
if (!node) { teams[id] = { teamInfo: {} }; continue }
if (node.teamInfo) {
teams[id] = { teamInfo: node.teamInfo }
} else if (node.heroes) {
const ti: Record<string, any> = {}
node.heroes.forEach((h: any, idx: number) => { ti[String(idx + 1)] = h })
teams[id] = { teamInfo: ti }
} else if (typeof node === 'object') {
const hasHero = Object.values(node).some((v: any) => v && typeof v === 'object' && 'heroId' in v)
teams[id] = { teamInfo: hasHero ? node : {} }
} else {
teams[id] = { teamInfo: {} }
}
}
return { useTeamId: Number(useTeamId) || 1, teams }
}
const presetTeam = computed(() => normalizePresetTeam(presetTeamRaw.value))
// —— 英雄列表 ——
const currentTeamHeroes = computed(() => {
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
})
}
const team = presetTeam.value.teams[currentTeam.value]?.teamInfo
if (!team) return []
const heroes: Array<{ id: number; name: string; type: string; position: number; level?: number; avatar?: string }> = []
for (const [pos, hero] of Object.entries(team)) {
const hid = (hero as any)?.heroId ?? (hero as any)?.id
if (!hid) continue
const meta = HERO_DICT[Number(hid)]
heroes.push({
id: Number(hid),
name: meta?.name ?? `英雄${hid}`,
type: meta?.type ?? '',
position: Number(pos),
level: (hero as any)?.level ?? 1,
avatar: (hero as any)?.avatar
})
} 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)
heroes.sort((a, b) => a.position - b.position)
return heroes
})
// 从presetTeamInfo获取可用队伍数量
// —— 命令封装 ——
const executeGameCommand = async (tokenId: string | number, cmd: string, params: any = {}, description = '', timeout = 8000) => {
try {
return await tokenStore.sendMessageWithPromise(tokenId, cmd, params, timeout)
} catch (error: any) {
const msg = error?.message ?? String(error)
if (description) message.error(`${description}失败:${msg}`)
throw error
}
}
// —— 数据加载:缓存优先,可强制刷新 ——
const getTeamInfoWithCache = async (force = false) => {
if (!tokenStore.selectedToken) {
message.warning('请先选择Token')
return null
}
const tokenId = tokenStore.selectedToken.id
if (!force) {
const cached = tokenStore.gameData?.presetTeam?.presetTeamInfo
if (cached) return cached
}
loading.value = true
try {
const result = await executeGameCommand(tokenId, 'presetteam_getinfo', {}, '获取阵容信息')
tokenStore.$patch((state) => {
state.gameData = { ...(state.gameData ?? {}), presetTeam: result }
})
return result?.presetTeamInfo ?? null
} finally {
loading.value = false
}
}
// —— UI 同步 ——
const updateAvailableTeams = () => {
if (!presetTeamInfo.value?.presetTeamInfo) return
const ids = Object.keys(presetTeam.value.teams).map(Number).filter(n => !Number.isNaN(n)).sort((a, b) => a - b)
availableTeams.value = ids.length ? ids : [1, 2, 3, 4]
}
const updateCurrentTeam = () => { currentTeam.value = presetTeam.value.useTeamId || 1 }
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 selectTeam = async (teamId: number) => {
if (switching.value || loading.value) return
if (!tokenStore.selectedToken) { message.warning('请先选择Token'); return }
const prev = currentTeam.value
switching.value = true
try {
await executeGameCommand(tokenStore.selectedToken.id, 'presetteam_saveteam', { teamId }, `切换到阵容 ${teamId}`)
currentTeam.value = teamId
message.success(`已切换到阵容 ${teamId}`)
await refreshTeamData(true)
} catch (e) {
currentTeam.value = prev
} finally {
switching.value = false
}
}
// 更新当前队伍
const updateCurrentTeam = () => {
if (presetTeamInfo.value?.presetTeamInfo?.useTeamId) {
currentTeam.value = presetTeamInfo.value.presetTeamInfo.useTeamId
const refreshTeamData = async (force = false) => { await getTeamInfoWithCache(force) }
// —— 首次挂载:先查缓存,再兜底拉接口 ——
onMounted(async () => {
await refreshTeamData(false)
updateAvailableTeams(); updateCurrentTeam()
if (!presetTeamRaw.value) {
await refreshTeamData(true)
updateAvailableTeams(); updateCurrentTeam()
}
}
// 获取英雄名称(这里需要英雄数据字典)
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()
})
// —— 监听缓存变化(其他地方写入也能联动) ——
watch(() => presetTeamRaw.value, () => { updateAvailableTeams(); updateCurrentTeam() }, { deep: true })
</script>
<style scoped lang="scss">
@@ -275,187 +266,121 @@ onMounted(() => {
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);
}
&: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);
.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-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-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);
}
}
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); } }
&:disabled { opacity: .6; cursor: not-allowed; }
}
.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);
}
}
.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;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.heroes-grid {
.heroes-inline {
display: flex;
gap: var(--spacing-sm);
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: var(--spacing-md);
justify-content: center;
}
.hero-card {
.hero-item {
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);
}
gap: 4px;
min-width: 50px;
}
.hero-avatar {
.hero-circle {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: 2px solid var(--border-color, #e5e5e5);
background: white;
}
.hero-avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
.hero-placeholder {
width: 40px;
height: 40px;
border-radius: 50%;
width: 100%;
height: 100%;
background: var(--primary-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-sm);
font-size: 12px;
font-weight: var(--font-weight-bold);
}
.hero-name {
font-size: var(--font-size-xs);
font-size: 11px;
color: var(--text-secondary);
text-align: center;
font-weight: var(--font-weight-medium);
max-width: 50px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-team {
text-align: center;
color: var(--text-secondary);
p {
margin: 0;
font-size: var(--font-size-sm);
}
.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);
.heroes-inline {
justify-content: center;
gap: var(--spacing-xs);
}
.hero-item {
min-width: 45px;
}
.hero-circle {
width: 35px;
height: 35px;
}
.hero-name {
font-size: 10px;
max-width: 45px;
}
}
</style>

View File

@@ -57,9 +57,9 @@
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useTokenStore } from '@/stores/tokenStore'
import { useMessage } from 'naive-ui'
import {computed, onMounted, ref, watch} from 'vue'
import {useTokenStore} from '@/stores/tokenStore'
import {useMessage} from 'naive-ui'
const tokenStore = useTokenStore()
const message = useMessage()
@@ -72,66 +72,43 @@ const lastClimbResult = ref(null) // 最后一次爬塔结果
// 计算属性 - 从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
return `${floor} - ${layer}`
})
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(() => {
const hasEnergy = towerEnergy.value > 0
const notClimbing = !isClimbing.value
console.log(`🗼 canClimb 计算: hasEnergy=${hasEnergy}, notClimbing=${notClimbing}, result=${hasEnergy && notClimbing}`)
return hasEnergy && notClimbing
})
// 方法
const startTowerClimb = async () => {
console.log('🗼 开始爬塔按钮被点击')
console.log('🗼 当前状态:', {
canClimb: canClimb.value,
isClimbing: isClimbing.value,
towerEnergy: towerEnergy.value,
hasSelectedToken: !!tokenStore.selectedToken
})
if (!tokenStore.selectedToken) {
message.warning('请先选择Token')
return
@@ -149,36 +126,27 @@ const startTowerClimb = async () => {
}
// 确保在操作开始前设置状态
console.log('🗼 设置爬塔状态为true')
isClimbing.value = true
// 设置超时保护15秒后自动重置状态
climbTimeout.value = setTimeout(() => {
console.log('🗼 超时保护触发,自动重置爬塔状态')
isClimbing.value = false
climbTimeout.value = null
}, 15000)
try {
const tokenId = tokenStore.selectedToken.id
console.log('🗼 使用Token ID:', tokenId)
message.info('开始爬塔挑战...')
// 发送爬塔命令
console.log('🗼 发送爬塔命令...')
await tokenStore.sendMessageWithPromise(tokenId, 'fight_starttower', {}, 10000)
console.log('🗼 爬塔命令发送成功')
message.success('爬塔命令已发送')
// 立即查询塔信息以获取最新状态
console.log('🗼 爬塔完成,立即查询塔信息')
await getTowerInfo()
// 再延迟查询一次确保数据同步
setTimeout(async () => {
console.log('🗼 延迟查询塔信息')
await getTowerInfo()
// 清除超时并重置状态
@@ -186,12 +154,10 @@ const startTowerClimb = async () => {
clearTimeout(climbTimeout.value)
climbTimeout.value = null
}
console.log('🗼 延迟查询完成,重置爬塔状态')
isClimbing.value = false
}, 3000)
}, 2000)
} catch (error) {
console.error('🗼 爬塔失败:', error)
message.error('爬塔失败: ' + (error.message || '未知错误'))
// 发生错误时立即重置状态
@@ -199,7 +165,6 @@ const startTowerClimb = async () => {
clearTimeout(climbTimeout.value)
climbTimeout.value = null
}
console.log('🗼 发生错误,立即重置爬塔状态')
isClimbing.value = false
}
@@ -209,12 +174,11 @@ const startTowerClimb = async () => {
// 重置爬塔状态的方法
const resetClimbingState = () => {
console.log('🗼 用户手动重置爬塔状态')
if (climbTimeout.value) {
clearTimeout(climbTimeout.value)
climbTimeout.value = null
}
isClimbing.value = falsexian1xian
isClimbing.value = false
message.info('爬塔状态已重置')
}
@@ -226,36 +190,19 @@ const getTowerInfo = async () => {
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)
}
@@ -327,24 +274,15 @@ watch(() => tokenStore.gameData.towerResult, (newResult, oldResult) => {
// 生命周期
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无法获取塔信息')

View File

@@ -712,9 +712,23 @@ export const useTokenStore = defineStore('tokens', () => {
return sendMessage(tokenId, 'heart_beat')
}
// 发送获取角色信息请求
const sendGetRoleInfo = (tokenId, params = {}) => {
return sendMessageWithPromise(tokenId, 'role_getroleinfo', params)
// 发送获取角色信息请求(异步处理)
const sendGetRoleInfo = async (tokenId, params = {}) => {
try {
const roleInfo = await sendMessageWithPromise(tokenId, 'role_getroleinfo', params, 10000)
// 手动更新游戏数据(因为响应可能不会自动触发消息处理)
if (roleInfo) {
gameData.value.roleInfo = roleInfo
gameData.value.lastUpdated = new Date().toISOString()
console.log('📊 角色信息已通过 Promise 更新')
}
return roleInfo
} catch (error) {
console.error(`❌ 获取角色信息失败 [${tokenId}]:`, error.message)
throw error
}
}
// 发送获取数据版本请求

View File

@@ -382,27 +382,29 @@ export class XyzwWebSocketClient {
/** Promise 版发送 */
sendWithPromise(cmd, params = {}, timeoutMs = 5000) {
const respKey = `${cmd}_${this.seq + 1}`
return new Promise((resolve, reject) => {
if (!this.connected && !this.socket) {
return reject(new Error("WebSocket 连接已关闭"))
}
// 生成唯一的请求ID
const requestId = `${cmd}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
// 设置 Promise 状态
this.promises[respKey] = { resolve, reject }
this.promises[requestId] = { resolve, reject, originalCmd: cmd }
// 超时处理
const timer = setTimeout(() => {
delete this.promises[respKey]
delete this.promises[requestId]
reject(new Error(`请求超时: ${cmd} (${timeoutMs}ms)`))
}, timeoutMs)
// 发送消息
this.send(cmd, params, {
respKey,
respKey: requestId,
onSent: () => {
clearTimeout(timer)
// 消息发送成功后,不要清除超时器,让它继续等待响应
// 只有在收到响应或超时时才清除
}
})
})
@@ -513,13 +515,11 @@ export class XyzwWebSocketClient {
// 命令到响应的映射 - 处理响应命令与原始命令不匹配的情况
const responseToCommandMap = {
// 1:1 响应映射(优先级高)
'role_getroleinforesp': 'role_getroleinfo',
'system_signinrewardresp': 'system_signinreward',
'hero_recruitresp': 'hero_recruit',
'friend_batchresp': 'friend_batch',
'system_claimhanguprewardresp': 'system_claimhangupreward',
'task_claimdailyrewardresp': 'task_claimdailyreward',
'task_claimweekrewardresp': 'task_claimweekreward',
'item_openboxresp': 'item_openbox',
'bottlehelper_claimresp': 'bottlehelper_claim',
'bottlehelper_startresp': 'bottlehelper_start',
@@ -532,23 +532,46 @@ export class XyzwWebSocketClient {
'arena_getareatargetresp': 'arena_getareatarget',
'presetteam_getinforesp': 'presetteam_getinfo',
'presetteam_saveteamresp': 'presetteam_saveteam',
'presetteam_getteamresp': 'presetteam_getteam',
'mail_claimallattachmentresp': 'mail_claimallattachment',
'store_buyresp': 'store_purchase'
'store_buyresp': 'store_purchase',
'system_getdatabundleverresp': 'system_getdatabundlever',
'tower_claimrewardresp': 'tower_claimreward',
'fight_starttowerresp': 'fight_starttower',
// 特殊响应映射 - 有些命令有独立响应,有些用同步响应
'task_claimdailyrewardresp': 'task_claimdailyreward',
'task_claimweekrewardresp': 'task_claimweekreward',
// 同步响应映射(优先级低)
'syncresp': ['system_mysharecallback', 'task_claimdailypoint'],
'syncrewardresp': ['system_buygold', 'discount_claimreward', 'card_claimreward',
'artifact_lottery', 'genie_sweep', 'genie_buysweep','system_signinreward']
}
// 获取原始命令名
const originalCmd = responseToCommandMap[cmd] || cmd
// 获取原始命令名(支持一对一和一对多映射)
let originalCmds = responseToCommandMap[cmd]
if (!originalCmds) {
originalCmds = [cmd] // 如果没有映射,使用响应命令本身
} else if (typeof originalCmds === 'string') {
originalCmds = [originalCmds] // 转换为数组
}
// 查找对应的 Promise
for (const [key, promise] of Object.entries(this.promises)) {
// 检查是否匹配原始命令或响应命令
if (key.startsWith(originalCmd) || key.startsWith(cmd) || cmd === key) {
delete this.promises[key]
// 查找对应的 Promise - 遍历所有等待中的 Promise
for (const [requestId, promiseData] of Object.entries(this.promises)) {
// 检查 Promise 是否匹配当前响应的任一原始命令
if (originalCmds.includes(promiseData.originalCmd)) {
delete this.promises[requestId]
// 获取响应数据,优先使用 rawDataProtoMsg 自动解码),然后 decodedBody手动解码最后 body
const responseBody = packet.rawData !== undefined ? packet.rawData :
packet.decodedBody !== undefined ? packet.decodedBody :
packet.body
if (packet.code === 0 || packet.code === undefined) {
promise.resolve(packet.body || packet)
promiseData.resolve(responseBody || packet)
} else {
promise.reject(new Error(`服务器错误: ${packet.code} - ${packet.hint || '未知错误'}`))
promiseData.reject(new Error(`服务器错误: ${packet.code} - ${packet.hint || '未知错误'}`))
}
break
}

View File

@@ -267,6 +267,77 @@ const bulkActionOptions = [
}
]
// 等待WebSocket连接并加载阵容数据
const loadTeamDataWithConnection = async (tokenId, maxRetries = 3, retryDelay = 2000) => {
console.log('每日页面进入开始检查WebSocket连接状态...')
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// 检查WebSocket连接状态
const wsStatus = tokenStore.getWebSocketStatus(tokenId)
console.log(`${attempt}次检查WebSocket状态:`, wsStatus)
if (wsStatus !== 'connected') {
console.log('WebSocket未连接尝试建立连接...')
// 尝试建立WebSocket连接
const tokenData = tokenStore.gameTokens.find(t => t.id === tokenId)
if (tokenData && tokenData.token) {
// 触发WebSocket连接
tokenStore.createWebSocketConnection(tokenId, tokenData.token, tokenData.wsUrl)
// 等待连接建立
await new Promise(resolve => setTimeout(resolve, retryDelay))
// 再次检查连接状态
const newStatus = tokenStore.getWebSocketStatus(tokenId)
if (newStatus !== 'connected') {
if (attempt < maxRetries) {
console.log(`连接未建立,${retryDelay / 1000}秒后重试...`)
continue
} else {
throw new Error('WebSocket连接超时')
}
}
} else {
throw new Error('未找到有效的Token数据或WebSocket URL')
}
}
// WebSocket已连接开始加载阵容数据
console.log('WebSocket已连接开始加载阵容数据...')
const result = await tokenStore.sendMessageWithPromise(
tokenId,
'presetteam_getinfo',
{},
8000
)
if (result) {
// 更新到游戏数据缓存中
tokenStore.$patch((state) => {
state.gameData = { ...(state.gameData ?? {}), presetTeam: result }
})
console.log('阵容数据加载成功:', result)
message.success('阵容数据已更新')
return result
}
} catch (error) {
console.error(`${attempt}次尝试失败:`, error)
if (attempt < maxRetries) {
console.log(`${retryDelay / 1000}秒后进行第${attempt + 1}次重试...`)
await new Promise(resolve => setTimeout(resolve, retryDelay))
} else {
console.error('所有重试均失败,阵容数据加载失败')
message.warning(`阵容数据加载失败: ${error.message || '未知错误'}`)
return null
}
}
}
}
// 方法
const refreshTasks = async () => {
if (!selectedRoleId.value) {
@@ -567,6 +638,11 @@ onMounted(async () => {
await gameRolesStore.fetchGameRoles()
}
// 页面进入时手动调用阵容加载接口确保WebSocket连接后再调用
if (tokenStore.selectedToken) {
await loadTeamDataWithConnection(tokenStore.selectedToken.id)
}
// 设置默认选中的角色
if (gameRolesStore.selectedRole) {
selectedRoleId.value = gameRolesStore.selectedRole.id