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:
saopig1andClaude Opus 4.8 committed 2026-05-30 15:17:01 +08:00
1 parent 88ac7d2a68
commit 463a8e2f8a
2 files changed
+89 -5

No files matched your search

+7 -2
View File
@@ -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(() => {
+82 -3
View File
@@ -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); }