mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
feat(scope): lock Navbar selector to scoped bot + apply scope on load
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
88ac7d2a68
commit
463a8e2f8a
2 files changed
+89
-5
No files matched your search
+7
-2
@@ -140,12 +140,17 @@ function cycleMobileMode() {
|
||||
playerStore.setMode(nextMode);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
playerStore.loadTheme();
|
||||
connect();
|
||||
playerStore.fetchBots();
|
||||
syncTimer = setInterval(() => playerStore.syncElapsed(), 3000);
|
||||
mobileRaf = requestAnimationFrame(updateMobileProgress);
|
||||
// Reconcile the dedicated-link scope only after the bot list is known: the
|
||||
// router guard sets scopedBotId tentatively from ?bot, but applyScopeFromQuery
|
||||
// validates it against the loaded bots (locks if it exists, clears if stale).
|
||||
await playerStore.fetchBots();
|
||||
const qBot = typeof route.query.bot === 'string' ? route.query.bot : null;
|
||||
playerStore.applyScopeFromQuery(qBot);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -10,8 +10,20 @@
|
||||
</div>
|
||||
|
||||
<div class="nav-right">
|
||||
<!-- Bot selector (always shown when at least one bot exists) -->
|
||||
<div v-if="store.bots.length > 0" class="bot-selector" ref="selectorRef">
|
||||
<!-- Scoped (dedicated link): static label locked to the one bot, no switching -->
|
||||
<div v-if="store.isScoped" class="bot-selector scoped" ref="selectorRef">
|
||||
<div class="bot-selector-btn static">
|
||||
<span class="bot-dot" :class="{ online: activeBot?.connected }" />
|
||||
<span class="bot-selector-name">{{ activeBot?.name ?? '专属机器人' }}</span>
|
||||
<span v-if="activeBot?.playing && !activeBot?.paused" class="bot-state-mini playing">▶</span>
|
||||
<span v-else-if="activeBot?.paused" class="bot-state-mini paused">⏸</span>
|
||||
<span class="scope-badge">专属模式</span>
|
||||
</div>
|
||||
<button class="scope-exit-btn" @click="exitScope" title="退出专属模式">退出</button>
|
||||
</div>
|
||||
|
||||
<!-- Normal: full selector with switching (shown when at least one bot exists) -->
|
||||
<div v-else-if="store.bots.length > 0" class="bot-selector" ref="selectorRef">
|
||||
<button class="bot-selector-btn" @click="dropdownOpen = !dropdownOpen">
|
||||
<span class="bot-dot" :class="{ online: activeBot?.connected }" />
|
||||
<span class="bot-selector-name">{{ activeBot?.name ?? '选择机器人' }}</span>
|
||||
@@ -22,7 +34,7 @@
|
||||
<div v-if="dropdownOpen" class="bot-dropdown">
|
||||
<div class="bot-dropdown-header">机器人</div>
|
||||
<div
|
||||
v-for="bot in store.bots"
|
||||
v-for="bot in displayedBots"
|
||||
:key="bot.id"
|
||||
class="bot-card"
|
||||
:class="{ active: bot.id === store.activeBotId }"
|
||||
@@ -138,6 +150,11 @@ async function onLogout() {
|
||||
navRouter.replace({ name: 'login' });
|
||||
}
|
||||
const activeBot = computed(() => store.activeBot);
|
||||
// While scoped (dedicated link), the selector is locked to the single scoped
|
||||
// bot; otherwise the full list is shown and switching is allowed.
|
||||
const displayedBots = computed(() =>
|
||||
store.isScoped ? store.bots.filter((b) => b.id === store.scopedBotId) : store.bots,
|
||||
);
|
||||
const dropdownOpen = ref(false);
|
||||
const selectorRef = ref<HTMLElement | null>(null);
|
||||
const togglingBots = ref<Record<string, boolean>>({});
|
||||
@@ -156,6 +173,14 @@ function selectBot(id: string) {
|
||||
dropdownOpen.value = false;
|
||||
}
|
||||
|
||||
// Leave dedicated-link mode. Clear scope BEFORE navigating so the router guard
|
||||
// (which re-attaches ?bot from scopedBotId) sees a null scope and lets us out.
|
||||
function exitScope() {
|
||||
store.clearScope();
|
||||
dropdownOpen.value = false;
|
||||
navRouter.push('/');
|
||||
}
|
||||
|
||||
function resolveBaseUrl(): string {
|
||||
const base = publicBaseUrl.value;
|
||||
if (base && /^https?:\/\//i.test(base)) return base.replace(/\/+$/, '');
|
||||
@@ -370,6 +395,60 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* Scoped (dedicated-link) selector: locked, non-interactive label + exit */
|
||||
.bot-selector.scoped {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bot-selector-btn.static {
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: var(--hover-bg);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.scope-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--color-primary-15);
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scope-exit-btn {
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--hover-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.bot-state-mini {
|
||||
font-size: 14px;
|
||||
&.playing { color: var(--color-online); }
|
||||
|
||||
Reference in new issue
Block a user