mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 13:02:49 +08:00
feat: implement server password login, YouTube source, and improved bot selector UI
- **TS server password** (#7/#9): add serverPassword field across database, manager, bot API, and Settings UI — allows joining password-protected servers - **YouTube audio source** (#1/#10): new YouTubeProvider using yt-dlp binary; adds -y flag in chat commands, /api/music supports platform=youtube, YouTube badge in SongCard, yt-dlp-wrap npm dependency - **Bot selector UI** (#14): selector always visible (not just when >1 bot), bigger button with border and play-state indicator; per-bot URL routing at /bot/:id with BotRedirect view; copy-link button in dropdown Closes #1, #7, #9, #10, #14 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
b20ddf2603
commit
d5d6abeb1e
16 files changed
+391
-40
No files matched your search
@@ -9,36 +9,42 @@
|
||||
</div>
|
||||
|
||||
<div class="nav-right">
|
||||
<!-- Multi-bot selector -->
|
||||
<div v-if="store.bots.length > 1" class="bot-selector" ref="selectorRef">
|
||||
<!-- Bot selector (always shown when at least one bot exists) -->
|
||||
<div v-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>
|
||||
<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>
|
||||
<Icon icon="mdi:chevron-down" class="bot-chevron" :class="{ rotated: dropdownOpen }" />
|
||||
</button>
|
||||
<div v-if="dropdownOpen" class="bot-dropdown">
|
||||
<button
|
||||
<div
|
||||
v-for="bot in store.bots"
|
||||
:key="bot.id"
|
||||
class="bot-dropdown-item"
|
||||
:class="{ active: bot.id === store.activeBotId }"
|
||||
@click="selectBot(bot.id)"
|
||||
class="bot-dropdown-row"
|
||||
>
|
||||
<span class="bot-dot" :class="{ online: bot.connected }" />
|
||||
<span class="bot-dropdown-name">{{ bot.name }}</span>
|
||||
<span v-if="bot.playing && !bot.paused" class="bot-playing-badge">播放中</span>
|
||||
<span v-else-if="bot.paused" class="bot-paused-badge">已暂停</span>
|
||||
<span v-else-if="bot.connected" class="bot-idle-badge">空闲</span>
|
||||
<span v-else class="bot-offline-badge">离线</span>
|
||||
</button>
|
||||
<button
|
||||
class="bot-dropdown-item"
|
||||
:class="{ active: bot.id === store.activeBotId }"
|
||||
@click="selectBot(bot.id)"
|
||||
>
|
||||
<span class="bot-dot" :class="{ online: bot.connected }" />
|
||||
<span class="bot-dropdown-name">{{ bot.name }}</span>
|
||||
<span v-if="bot.playing && !bot.paused" class="bot-playing-badge">播放中</span>
|
||||
<span v-else-if="bot.paused" class="bot-paused-badge">已暂停</span>
|
||||
<span v-else-if="bot.connected" class="bot-idle-badge">空闲</span>
|
||||
<span v-else class="bot-offline-badge">离线</span>
|
||||
</button>
|
||||
<button class="bot-link-btn" :title="`复制 ${bot.name} 的专属链接`" @click.stop="copyBotLink(bot.id)">
|
||||
<Icon icon="mdi:link-variant" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="bot-dropdown-divider" />
|
||||
<div class="bot-dropdown-hint">点击切换 · 🔗 复制专属链接</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Single bot status (original behavior) -->
|
||||
<div v-else-if="activeBot" class="bot-status" :class="{ online: activeBot.connected }">
|
||||
{{ activeBot.name }} {{ activeBot.connected ? '在线' : '离线' }}
|
||||
</div>
|
||||
|
||||
<RouterLink to="/settings" class="settings-btn">
|
||||
<Icon icon="mdi:cog" />
|
||||
</RouterLink>
|
||||
@@ -61,6 +67,13 @@ function selectBot(id: string) {
|
||||
dropdownOpen.value = false;
|
||||
}
|
||||
|
||||
function copyBotLink(id: string) {
|
||||
const url = `${window.location.origin}/bot/${id}`;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
dropdownOpen.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onClickOutside(e: MouseEvent) {
|
||||
if (selectorRef.value && !selectorRef.value.contains(e.target as Node)) {
|
||||
dropdownOpen.value = false;
|
||||
@@ -146,19 +159,28 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 14px;
|
||||
padding: 8px 16px;
|
||||
background: var(--hover-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background var(--transition-fast);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.bot-state-mini {
|
||||
font-size: 11px;
|
||||
&.playing { color: #22c55e; }
|
||||
&.paused { color: #eab308; }
|
||||
}
|
||||
|
||||
.bot-chevron {
|
||||
font-size: 16px;
|
||||
opacity: 0.5;
|
||||
@@ -205,7 +227,8 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
@@ -222,6 +245,12 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.bot-dropdown-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bot-dropdown-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -230,6 +259,21 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bot-link-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 15px;
|
||||
opacity: 0.4;
|
||||
transition: opacity var(--transition-fast), background var(--transition-fast);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.bot-playing-badge,
|
||||
.bot-paused-badge,
|
||||
.bot-idle-badge,
|
||||
@@ -261,8 +305,21 @@ onUnmounted(() => {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.bot-dropdown-divider {
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.bot-dropdown-hint {
|
||||
padding: 4px 12px 6px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
opacity: 0.6;
|
||||
transition: opacity var(--transition-fast);
|
||||
&:hover { opacity: 1; }
|
||||
|
||||
Reference in new issue
Block a user