mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Surface failures + tighten edges:
1. Toast for /play-song & /play-playlist failures. Backend now returns
{ok, message} (localized in Chinese to match the rest of the UI).
Store stashes a notification on ok=false; new Toast.vue mounted in
App.vue shows it for 3-5s then fades. Clicking the X dismisses
immediately. Sits above the player on desktop and above the mobile
tabbar on phones.
2. QQ collected playlists pagination. fcg_get_profile_order_asset.fcg
returns max 30 per call; we now loop using has_more / short-page
detection up to a 300-playlist hard cap. Single-call users (typical)
exit the loop on the first iteration so no extra requests.
3. getPlayableSongIds chunking. 100 mids per request keeps URL well
under 8KB; chunk-level errors are isolated so a transient blip on
one chunk doesn't poison the whole batch. Returns null only when
every chunk failed (caller falls back to sequential retry).
4. SourceTabs single-source mode now renders a small subdued "网易云"
or "QQ" label instead of vanishing entirely, so the user always
knows which platform's data they're looking at.
5. Hide the "我的歌单 N" count badge when N=0 — Home and Library no
longer show "我的歌单 0" with an empty grid.
6. Auth state change invalidates fetchHomeData cache. Previously, a
user who logged out as account A and into account B within 5
minutes would see A's playlists. Now we always re-check auth at
the top of fetchHomeData and bypass the TTL cache when authStatus
has changed.
Out of scope:
- NetEase analogous batch precheck (per request).
- 60s TS3 UDP idle disconnect — that's the bundled @honeybbq/teamspeak-
client UDP layer kicking when no server packet arrives in 60s. It's
baked in (constant `v=6e4`) and not exposed as an option, and root
cause is server-side or network-layer behavior we can't reach from
here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
2.0 KiB
Vue
95 lines
2.0 KiB
Vue
<template>
|
|
<div v-if="sources.length > 0" class="source-tabs">
|
|
<template v-if="sources.length >= 2">
|
|
<button
|
|
v-for="src in sources"
|
|
:key="src"
|
|
type="button"
|
|
class="source-tab"
|
|
:class="{ active: src === modelValue }"
|
|
@click="$emit('update:modelValue', src)"
|
|
>
|
|
{{ LABELS[src] }}
|
|
</button>
|
|
</template>
|
|
<span v-else class="source-tab-label" :title="`数据来自${LABELS[sources[0]]}`">
|
|
{{ LABELS[sources[0]] }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Source } from '../stores/player.js';
|
|
|
|
const LABELS: Record<Source, string> = {
|
|
netease: '网易云',
|
|
qq: 'QQ',
|
|
};
|
|
|
|
defineProps<{
|
|
modelValue: Source;
|
|
sources: Source[];
|
|
}>();
|
|
|
|
defineEmits<{
|
|
'update:modelValue': [value: Source];
|
|
}>();
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.source-tabs {
|
|
display: inline-flex;
|
|
gap: 4px;
|
|
margin-left: 12px;
|
|
align-items: center;
|
|
}
|
|
|
|
.source-tab {
|
|
padding: 4px 10px;
|
|
min-height: 28px;
|
|
font-size: var(--fs-sm);
|
|
font-weight: var(--fw-medium);
|
|
color: var(--text-secondary);
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
transition: color var(--transition-fast), background var(--transition-fast);
|
|
|
|
&:hover {
|
|
color: var(--text-primary);
|
|
background: var(--hover-bg);
|
|
}
|
|
|
|
&.active {
|
|
color: var(--color-primary);
|
|
background: var(--color-primary-12);
|
|
font-weight: var(--fw-semi);
|
|
}
|
|
}
|
|
|
|
// Single-source mode: not interactive, but tells the user which platform
|
|
// they're looking at instead of leaving them guessing.
|
|
.source-tab-label {
|
|
padding: 4px 10px;
|
|
font-size: var(--fs-xs);
|
|
font-weight: var(--fw-medium);
|
|
color: var(--text-tertiary);
|
|
background: var(--hover-bg);
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.source-tabs {
|
|
margin-left: 8px;
|
|
gap: 2px;
|
|
}
|
|
|
|
.source-tab {
|
|
padding: 6px 10px;
|
|
min-height: 36px; // larger touch target on mobile
|
|
font-size: var(--fs-xs);
|
|
}
|
|
}
|
|
</style>
|