Files
xyzw_web_helper/src/components/TeamStatus.vue
2025-09-04 01:44:05 +08:00

508 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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"
:disabled="loading || switching"
:class="['team-button', { active: currentTeam === teamId }]"
@click="selectTeam(teamId)"
>
{{ teamId }}
</button>
<button
class="refresh-button"
:disabled="loading"
title="刷新队伍数据"
@click="refreshTeamData(true)"
>
<svg class="refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
<path d="M3 21v-5h5"/>
</svg>
<span class="refresh-text">刷新</span>
</button>
</div>
</div>
<div class="card-content">
<div class="team-display">
<div class="current-team-info">
<span class="label">当前阵容</span>
<span class="team-number">
<template v-if="!loading">阵容 {{ currentTeam }}</template>
<template v-else>加载中</template>
</span>
</div>
<div class="heroes-container">
<div v-if="!loading" class="heroes-inline">
<div
v-for="hero in currentTeamHeroes"
:key="hero.id || hero.name"
class="hero-item"
>
<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="!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 lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useTokenStore } from '@/stores/tokenStore'
import { useMessage, NTag } from 'naive-ui'
/**
* 集成英雄字典游戏ID -> { name, type }
* 你也可以独立出一个 heroDict.ts 后 import按你的要求这里整合到同一文件。
*/
const HERO_DICT = {
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])
// WebSocket连接状态
const wsStatus = computed(() => {
if (!tokenStore.selectedToken) return 'disconnected'
return tokenStore.getWebSocketStatus(tokenStore.selectedToken.id)
})
// —— 缓存优先的 presetTeam 原始数据 ——
const presetTeamRaw = computed(() => tokenStore.gameData?.presetTeam ?? null)
// 统一结构:输出 { useTeamId, teams }
function normalizePresetTeam(raw) {
if (!raw) return { useTeamId: 1, teams: {} }
const root = raw.presetTeamInfo ?? raw
const findUseIdRec = (obj) => {
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 = {}
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, idx) => { ti[String(idx + 1)] = h })
teams[id] = { teamInfo: ti }
} else if (typeof node === 'object') {
const hasHero = Object.values(node).some((v) => 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(() => {
const team = presetTeam.value.teams[currentTeam.value]?.teamInfo
if (!team) return []
const heroes = []
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
})
}
heroes.sort((a, b) => a.position - b.position)
return heroes
})
// —— 命令封装 ——
const executeGameCommand = async (tokenId, cmd, params = {}, description = '', timeout = 8000) => {
try {
return await tokenStore.sendMessageWithPromise(tokenId, cmd, params, timeout)
} catch (error) {
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
} catch (error) {
console.error('获取阵容信息失败:', error)
return null
} finally {
loading.value = false
}
}
// —— UI 同步 ——
const updateAvailableTeams = () => {
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) => {
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 refreshTeamData = async (force = false) => { await getTeamInfoWithCache(force) }
// —— 首次挂载:检查连接状态后获取数据 ——
onMounted(async () => {
// 组件挂载时获取队伍信息
if (tokenStore.selectedToken && wsStatus.value === 'connected') {
await refreshTeamData(false)
updateAvailableTeams(); updateCurrentTeam()
if (!presetTeamRaw.value) {
await refreshTeamData(true)
updateAvailableTeams(); updateCurrentTeam()
}
} else if (!tokenStore.selectedToken) {
console.log('🛡️ 没有选中的Token无法获取队伍信息')
} else {
console.log('🛡️ WebSocket未连接等待连接后自动获取队伍信息')
}
})
// —— 监听WebSocket连接状态变化 ——
watch(wsStatus, (newStatus, oldStatus) => {
console.log(`🛡️ WebSocket状态变化: ${oldStatus} -> ${newStatus}`)
if (newStatus === 'connected' && oldStatus !== 'connected' && tokenStore.selectedToken) {
console.log('🛡️ WebSocket已连接自动获取队伍信息')
// 延迟一点时间让WebSocket完全就绪
setTimeout(async () => {
await refreshTeamData(false)
updateAvailableTeams(); updateCurrentTeam()
if (!presetTeamRaw.value) {
await refreshTeamData(true)
updateAvailableTeams(); updateCurrentTeam()
}
}, 1000)
}
})
// —— 监听Token变化 ——
watch(() => tokenStore.selectedToken, async (newToken, oldToken) => {
if (newToken && newToken.id !== oldToken?.id) {
console.log('🛡️ Token已切换重新获取队伍信息')
// 检查WebSocket是否已连接
const status = tokenStore.getWebSocketStatus(newToken.id)
if (status === 'connected') {
await refreshTeamData(true) // 切换Token时强制刷新
updateAvailableTeams(); updateCurrentTeam()
}
}
})
// —— 监听缓存变化(其他地方写入也能联动) ——
watch(() => presetTeamRaw.value, () => { updateAvailableTeams(); updateCurrentTeam() }, { deep: true })
</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; }
&:disabled { opacity: .6; cursor: not-allowed; }
}
.refresh-button {
display: flex;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 12px;
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 8px;
background: var(--bg-primary, #ffffff);
color: var(--text-secondary, #6b7280);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast, 0.15s ease);
&:hover {
background: var(--bg-secondary, #f9fafb);
border-color: var(--border-hover, #d1d5db);
color: var(--text-primary, #374151);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
&:hover {
background: var(--bg-primary, #ffffff);
border-color: var(--border-color, #e5e7eb);
color: var(--text-secondary, #6b7280);
transform: none;
box-shadow: none;
}
}
.refresh-icon {
width: 14px;
height: 14px;
transition: transform var(--transition-fast, 0.15s ease);
}
&:not(:disabled):hover .refresh-icon {
transform: rotate(180deg);
}
&:disabled .refresh-icon {
animation: spin 1s linear infinite;
}
.refresh-text {
font-size: 13px;
line-height: 1;
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.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: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.heroes-inline {
display: flex;
gap: var(--spacing-sm);
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
}
.hero-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
min-width: 50px;
}
.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: 100%;
height: 100%;
background: var(--primary-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: var(--font-weight-bold);
}
.hero-name {
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); }
}
@media (max-width: 768px) {
.card-header {
flex-direction: column;
gap: var(--spacing-sm);
text-align: center;
}
.team-selector {
justify-content: center;
}
.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>