mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52: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);
|
playerStore.setMode(nextMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
playerStore.loadTheme();
|
playerStore.loadTheme();
|
||||||
connect();
|
connect();
|
||||||
playerStore.fetchBots();
|
|
||||||
syncTimer = setInterval(() => playerStore.syncElapsed(), 3000);
|
syncTimer = setInterval(() => playerStore.syncElapsed(), 3000);
|
||||||
mobileRaf = requestAnimationFrame(updateMobileProgress);
|
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(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -10,8 +10,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-right">
|
<div class="nav-right">
|
||||||
<!-- Bot selector (always shown when at least one bot exists) -->
|
<!-- Scoped (dedicated link): static label locked to the one bot, no switching -->
|
||||||
<div v-if="store.bots.length > 0" class="bot-selector" ref="selectorRef">
|
<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">
|
<button class="bot-selector-btn" @click="dropdownOpen = !dropdownOpen">
|
||||||
<span class="bot-dot" :class="{ online: activeBot?.connected }" />
|
<span class="bot-dot" :class="{ online: activeBot?.connected }" />
|
||||||
<span class="bot-selector-name">{{ activeBot?.name ?? '选择机器人' }}</span>
|
<span class="bot-selector-name">{{ activeBot?.name ?? '选择机器人' }}</span>
|
||||||
@@ -22,7 +34,7 @@
|
|||||||
<div v-if="dropdownOpen" class="bot-dropdown">
|
<div v-if="dropdownOpen" class="bot-dropdown">
|
||||||
<div class="bot-dropdown-header">机器人</div>
|
<div class="bot-dropdown-header">机器人</div>
|
||||||
<div
|
<div
|
||||||
v-for="bot in store.bots"
|
v-for="bot in displayedBots"
|
||||||
:key="bot.id"
|
:key="bot.id"
|
||||||
class="bot-card"
|
class="bot-card"
|
||||||
:class="{ active: bot.id === store.activeBotId }"
|
:class="{ active: bot.id === store.activeBotId }"
|
||||||
@@ -138,6 +150,11 @@ async function onLogout() {
|
|||||||
navRouter.replace({ name: 'login' });
|
navRouter.replace({ name: 'login' });
|
||||||
}
|
}
|
||||||
const activeBot = computed(() => store.activeBot);
|
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 dropdownOpen = ref(false);
|
||||||
const selectorRef = ref<HTMLElement | null>(null);
|
const selectorRef = ref<HTMLElement | null>(null);
|
||||||
const togglingBots = ref<Record<string, boolean>>({});
|
const togglingBots = ref<Record<string, boolean>>({});
|
||||||
@@ -156,6 +173,14 @@ function selectBot(id: string) {
|
|||||||
dropdownOpen.value = false;
|
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 {
|
function resolveBaseUrl(): string {
|
||||||
const base = publicBaseUrl.value;
|
const base = publicBaseUrl.value;
|
||||||
if (base && /^https?:\/\//i.test(base)) return base.replace(/\/+$/, '');
|
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 {
|
.bot-state-mini {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
&.playing { color: var(--color-online); }
|
&.playing { color: var(--color-online); }
|
||||||
|
|||||||
Reference in new issue
Block a user