# TSMusicBot Phase 7: WebUI Frontend (Vue.js SPA) > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build the Vue.js SPA frontend with YesPlayMusic-inspired design. Covers all 6 pages (Home, Search, Playlist, Lyrics, History, Settings), the player bar, navbar, and WebSocket real-time updates. **Architecture:** Vue 3 + Vite SPA in `web/`. Pinia stores manage player state and WebSocket connection. SCSS variables handle theming (dark/light). The frontend communicates with the backend via REST API (`/api/*`) and WebSocket (`/ws`). **Tech Stack:** Vue 3, Vite 5, Pinia, Vue Router 4, SCSS, axios, node-vibrant, Iconify --- ### Task 1: Scaffold Vue.js project **Files:** - Create: `web/package.json` - Create: `web/vite.config.ts` - Create: `web/index.html` - Create: `web/src/main.ts` - Create: `web/src/App.vue` - Create: `web/tsconfig.json` - [ ] **Step 1: Create Vue project in web/ directory** ```bash cd "C:/Users/saopig1/Music/teamspeak music bot" mkdir -p web cd web npm init -y npm install vue vue-router@4 pinia axios node-vibrant @iconify/vue npm install -D vite @vitejs/plugin-vue typescript sass vue-tsc ``` - [ ] **Step 2: Create web/vite.config.ts** ```typescript import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': 'http://localhost:3000', '/ws': { target: 'ws://localhost:3000', ws: true, }, }, }, build: { outDir: 'dist', }, }); ``` - [ ] **Step 3: Create web/tsconfig.json** ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], "skipLibCheck": true, "noEmit": true, "paths": { "@/*": ["./src/*"] }, "baseUrl": "." }, "include": ["src/**/*.ts", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ``` - [ ] **Step 4: Create web/index.html** ```html TSMusicBot
``` - [ ] **Step 5: Create web/src/main.ts** ```typescript import { createApp } from 'vue'; import { createPinia } from 'pinia'; import App from './App.vue'; import router from './router/index.js'; import './styles/global.scss'; const app = createApp(App); app.use(createPinia()); app.use(router); app.mount('#app'); ``` - [ ] **Step 6: Create web/src/App.vue** ```vue ``` - [ ] **Step 7: Commit** ```bash cd "C:/Users/saopig1/Music/teamspeak music bot" git add web/ git commit -m "feat: scaffold Vue.js frontend project with Vite, Pinia, and Vue Router" ``` --- ### Task 2: Global styles and theming **Files:** - Create: `web/src/styles/variables.scss` - Create: `web/src/styles/global.scss` - [ ] **Step 1: Create variables.scss** ```scss // YesPlayMusic-inspired design tokens :root { // Fonts --font-primary: 'Barlow', -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; // Spacing --navbar-height: 56px; --player-height: 56px; // Radius --radius-sm: 6px; --radius-md: 10px; --radius-lg: 14px; --radius-xl: 20px; // Transitions --transition-fast: 0.2s ease; --transition-normal: 0.3s ease; // Accent --color-primary: #335eea; --color-primary-bg: #eaeffd; } [data-theme='dark'] { --bg-primary: #222222; --bg-secondary: #323232; --bg-card: rgba(255, 255, 255, 0.04); --bg-navbar: rgba(34, 34, 34, 0.86); --text-primary: #ffffff; --text-secondary: rgba(255, 255, 255, 0.58); --text-tertiary: rgba(255, 255, 255, 0.38); --border-color: rgba(255, 255, 255, 0.06); --hover-bg: rgba(255, 255, 255, 0.08); } [data-theme='light'] { --bg-primary: #ffffff; --bg-secondary: #f5f5f7; --bg-card: rgba(0, 0, 0, 0.02); --bg-navbar: rgba(255, 255, 255, 0.86); --text-primary: #000000; --text-secondary: rgba(0, 0, 0, 0.58); --text-tertiary: rgba(0, 0, 0, 0.38); --border-color: rgba(0, 0, 0, 0.06); --hover-bg: rgba(0, 0, 0, 0.04); } ``` - [ ] **Step 2: Create global.scss** ```scss @use 'variables'; * { margin: 0; padding: 0; box-sizing: border-box; } html { font-size: 16px; } body { font-family: var(--font-primary); background: var(--bg-primary); color: var(--text-primary); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { text-decoration: none; color: inherit; } button { border: none; background: none; cursor: pointer; font-family: inherit; color: inherit; } // Scrollbar ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--text-tertiary); border-radius: 4px; } // Shared utility classes .frosted-glass { background: var(--bg-navbar); backdrop-filter: saturate(180%) blur(20px); -webkit-backdrop-filter: saturate(180%) blur(20px); } .hover-scale { transition: transform var(--transition-fast); &:hover { transform: scale(1.04); } &:active { transform: scale(0.96); } } ``` - [ ] **Step 3: Commit** ```bash git add web/src/styles/ git commit -m "feat: add YesPlayMusic-inspired SCSS theme with dark/light mode" ``` --- ### Task 3: Pinia stores (player + WebSocket) **Files:** - Create: `web/src/stores/player.ts` - Create: `web/src/composables/useWebSocket.ts` - [ ] **Step 1: Create player store** Create `web/src/stores/player.ts`: ```typescript import { defineStore } from 'pinia'; import axios from 'axios'; export interface Song { id: string; name: string; artist: string; album: string; duration: number; coverUrl: string; platform: 'netease' | 'qq'; } export interface BotStatus { id: string; name: string; connected: boolean; playing: boolean; paused: boolean; currentSong: Song | null; queueSize: number; volume: number; playMode: string; } export const usePlayerStore = defineStore('player', { state: () => ({ bots: [] as BotStatus[], activeBotId: null as string | null, queue: [] as Song[], theme: 'dark' as 'dark' | 'light', }), getters: { activeBot(): BotStatus | null { return this.bots.find((b) => b.id === this.activeBotId) ?? this.bots[0] ?? null; }, currentSong(): Song | null { return this.activeBot?.currentSong ?? null; }, isPlaying(): boolean { return this.activeBot?.playing ?? false; }, isPaused(): boolean { return this.activeBot?.paused ?? false; }, }, actions: { setActiveBotId(id: string) { this.activeBotId = id; }, updateBotStatus(botId: string, status: BotStatus) { const index = this.bots.findIndex((b) => b.id === botId); if (index >= 0) { this.bots[index] = status; } else { this.bots.push(status); } }, removeBotStatus(botId: string) { this.bots = this.bots.filter((b) => b.id !== botId); }, setQueue(queue: Song[]) { this.queue = queue; }, toggleTheme() { this.theme = this.theme === 'dark' ? 'light' : 'dark'; localStorage.setItem('theme', this.theme); }, loadTheme() { const saved = localStorage.getItem('theme') as 'dark' | 'light' | null; if (saved) this.theme = saved; }, async fetchBots() { const res = await axios.get('/api/bot'); this.bots = res.data.bots; if (!this.activeBotId && this.bots.length > 0) { this.activeBotId = this.bots[0].id; } }, async play(query: string, platform = 'netease') { if (!this.activeBotId) return; await axios.post(`/api/player/${this.activeBotId}/play`, { query, platform }); }, async addToQueue(query: string, platform = 'netease') { if (!this.activeBotId) return; await axios.post(`/api/player/${this.activeBotId}/add`, { query, platform }); }, async pause() { if (!this.activeBotId) return; await axios.post(`/api/player/${this.activeBotId}/pause`); }, async resume() { if (!this.activeBotId) return; await axios.post(`/api/player/${this.activeBotId}/resume`); }, async next() { if (!this.activeBotId) return; await axios.post(`/api/player/${this.activeBotId}/next`); }, async prev() { if (!this.activeBotId) return; await axios.post(`/api/player/${this.activeBotId}/prev`); }, async stop() { if (!this.activeBotId) return; await axios.post(`/api/player/${this.activeBotId}/stop`); }, async setVolume(volume: number) { if (!this.activeBotId) return; await axios.post(`/api/player/${this.activeBotId}/volume`, { volume }); }, async setMode(mode: string) { if (!this.activeBotId) return; await axios.post(`/api/player/${this.activeBotId}/mode`, { mode }); }, }, }); ``` - [ ] **Step 2: Create WebSocket composable** Create `web/src/composables/useWebSocket.ts`: ```typescript import { ref, onUnmounted } from 'vue'; import { usePlayerStore } from '../stores/player.js'; export function useWebSocket() { const connected = ref(false); let ws: WebSocket | null = null; let reconnectTimer: ReturnType | null = null; function connect() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const url = `${protocol}//${window.location.host}/ws`; ws = new WebSocket(url); ws.onopen = () => { connected.value = true; console.log('WebSocket connected'); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); const store = usePlayerStore(); switch (data.type) { case 'init': for (const bot of data.bots) { store.updateBotStatus(bot.id, bot); } break; case 'stateChange': store.updateBotStatus(data.botId, data.status); if (data.queue) store.setQueue(data.queue); break; case 'botConnected': store.updateBotStatus(data.botId, data.status); break; case 'botDisconnected': store.removeBotStatus(data.botId); break; } }; ws.onclose = () => { connected.value = false; // Reconnect after 3 seconds reconnectTimer = setTimeout(connect, 3000); }; ws.onerror = () => { ws?.close(); }; } function disconnect() { if (reconnectTimer) clearTimeout(reconnectTimer); ws?.close(); } onUnmounted(disconnect); return { connected, connect, disconnect }; } ``` - [ ] **Step 3: Commit** ```bash git add web/src/stores/ web/src/composables/ git commit -m "feat: add Pinia player store and WebSocket composable" ``` --- ### Task 4: Router setup **Files:** - Create: `web/src/router/index.ts` - [ ] **Step 1: Create router** Create `web/src/router/index.ts`: ```typescript import { createRouter, createWebHistory } from 'vue-router'; const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', name: 'home', component: () => import('../views/Home.vue'), }, { path: '/search', name: 'search', component: () => import('../views/Search.vue'), }, { path: '/playlist/:id', name: 'playlist', component: () => import('../views/Playlist.vue'), }, { path: '/lyrics', name: 'lyrics', component: () => import('../views/Lyrics.vue'), }, { path: '/history', name: 'history', component: () => import('../views/History.vue'), }, { path: '/settings', name: 'settings', component: () => import('../views/Settings.vue'), }, ], }); export default router; ``` - [ ] **Step 2: Commit** ```bash git add web/src/router/ git commit -m "feat: add Vue Router with all page routes" ``` --- ### Task 5: Navbar component **Files:** - Create: `web/src/components/Navbar.vue` - [ ] **Step 1: Create Navbar** Create `web/src/components/Navbar.vue`: ```vue ``` - [ ] **Step 2: Commit** ```bash git add web/src/components/Navbar.vue git commit -m "feat: add frosted-glass Navbar component" ``` --- ### Task 6: Player bar component **Files:** - Create: `web/src/components/Player.vue` - [ ] **Step 1: Create Player** Create `web/src/components/Player.vue`: ```vue ``` - [ ] **Step 2: Commit** ```bash git add web/src/components/Player.vue git commit -m "feat: add frosted-glass Player bar with playback controls and volume" ``` --- ### Task 7: CoverArt component (with colored shadow) **Files:** - Create: `web/src/components/CoverArt.vue` - [ ] **Step 1: Create CoverArt** Create `web/src/components/CoverArt.vue`: ```vue ``` - [ ] **Step 2: Commit** ```bash git add web/src/components/CoverArt.vue git commit -m "feat: add CoverArt component with YesPlayMusic-style colored shadow" ``` --- ### Task 8: Home page **Files:** - Create: `web/src/views/Home.vue` - [ ] **Step 1: Create Home view** Create `web/src/views/Home.vue`: ```vue ``` - [ ] **Step 2: Commit** ```bash git add web/src/views/Home.vue git commit -m "feat: add Home page with bot selector, search bar, now playing, and playlists" ``` --- ### Task 9: Search, Playlist, Lyrics, History, Settings pages **Files:** - Create: `web/src/views/Search.vue` - Create: `web/src/views/Playlist.vue` - Create: `web/src/views/Lyrics.vue` - Create: `web/src/views/History.vue` - Create: `web/src/views/Settings.vue` - Create: `web/src/components/SongCard.vue` - Create: `web/src/components/Queue.vue` These pages follow the same patterns established in Task 8. Each page: - Uses the Pinia store for state - Calls REST API endpoints for data - Uses shared components (CoverArt, SongCard) - Follows the YesPlayMusic design tokens (SCSS variables, frosted glass, hover-scale, etc.) Due to the size, each view file should be implemented following these specs: - [ ] **Step 1: Create SongCard component** Create `web/src/components/SongCard.vue` — a reusable song row with cover, name, artist, duration, and play/add buttons. Used in search results, playlist detail, and queue. ```vue ``` - [ ] **Step 2: Create Search view** Create `web/src/views/Search.vue` — search input, platform toggle (netease/qq), results as SongCard list. - [ ] **Step 3: Create Playlist view** Create `web/src/views/Playlist.vue` — hero header with cover art, playlist name, play all button, song list. - [ ] **Step 4: Create Lyrics view** Create `web/src/views/Lyrics.vue` — full-screen with dynamic blurred album art background, scrolling lyrics with active line highlighting. Uses `node-vibrant` for color extraction. - [ ] **Step 5: Create History view** Create `web/src/views/History.vue` — play history list from `/api/player/:botId/history`. - [ ] **Step 6: Create Settings view** Create `web/src/views/Settings.vue` — bot instance management (create/delete/start/stop), TS server connection config, music account login (QR code display, SMS form, cookie paste), command prefix, theme toggle. - [ ] **Step 7: Create Queue component** Create `web/src/components/Queue.vue` — sidebar queue panel showing current queue with drag-to-reorder and remove buttons. - [ ] **Step 8: Commit** ```bash git add web/src/views/ web/src/components/ git commit -m "feat: add all page views (Search, Playlist, Lyrics, History, Settings) and shared components" ``` --- ### Task 10: Build and verify - [ ] **Step 1: Build frontend** ```bash cd web && npm run build ``` Expected: `web/dist/` is created with `index.html` and JS/CSS assets. - [ ] **Step 2: Add frontend build script to root package.json** Add to root `package.json` scripts: ```json { "scripts": { "build:web": "cd web && npm run build", "build": "tsc && npm run build:web" } } ``` - [ ] **Step 3: Commit** ```bash cd "C:/Users/saopig1/Music/teamspeak music bot" git add -A git commit -m "chore: Phase 7 complete — Vue.js WebUI with YesPlayMusic-inspired design" ```