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

@@ -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-for="hero in currentTeamHeroes"
:key="hero.id || hero.name"
class="hero-card"
<div v-if="!loading" class="heroes-inline">
<div
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 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 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 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) }
// 获取英雄名称(这里需要英雄数据字典)
const getHeroName = (heroId) => {
// 暂时返回英雄ID后续可以添加英雄名称映射
const heroNames = {
1: '剑士',
2: '法师',
3: '弓手',
4: '盗贼',
5: '牧师'
// 更多英雄映射...
// —— 首次挂载:先查缓存,再兜底拉接口 ——
onMounted(async () => {
await refreshTeamData(false)
updateAvailableTeams(); updateCurrentTeam()
if (!presetTeamRaw.value) {
await refreshTeamData(true)
updateAvailableTeams(); updateCurrentTeam()
}
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;
.card-header {
flex-direction: column;
gap: var(--spacing-sm);
text-align: center;
}
.team-selector {
justify-content: 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>
</style>