From 1552fa1a394ea6830d116e58ec2a04084a68d36e Mon Sep 17 00:00:00 2001 From: saopig1 Date: Tue, 21 Apr 2026 01:12:58 +0800 Subject: [PATCH] feat(web): mobile-responsive redesign with Library view and design tokens Add mobile layout (mini player, bottom tab bar, responsive navbar), new Library view, redesigned bot dropdown with action buttons, and centralize colors/typography/shadows into CSS variables in variables.scss. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/settings.local.json | 5 +- web/src/App.vue | 177 ++++++++++++++++++- web/src/components/Navbar.vue | 302 +++++++++++++++++++++----------- web/src/components/Player.vue | 12 +- web/src/components/Queue.vue | 2 +- web/src/components/SongCard.vue | 24 +-- web/src/router/index.ts | 5 + web/src/styles/variables.scss | 67 ++++++- web/src/views/Home.vue | 13 +- web/src/views/Library.vue | 198 +++++++++++++++++++++ web/src/views/Settings.vue | 18 +- 11 files changed, 684 insertions(+), 139 deletions(-) create mode 100644 web/src/views/Library.vue diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7e003df..28d65be 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,10 @@ "mcp__Claude_Preview__*", "mcp__Claude_in_Chrome__*", "mcp__scheduled-tasks__*", - "Bash(npx vitest:*)" + "Bash(npx vitest:*)", + "Bash(cp \"C:\\\\Users\\\\saopig1\\\\.claude\\\\projects\\\\C--Users-saopig1-Music-teamspeak-music-bot\\\\b5a64d6f-051e-4b87-966c-ece97d2b879b\\\\tool-results\\\\webfetch-1776569008470-exv7qe.bin\" /tmp/design.gz)", + "Bash(gunzip -f /tmp/design.gz)", + "Read(//tmp/**)" ], "deny": [ "Bash(git push * main)", diff --git a/web/src/App.vue b/web/src/App.vue index 16f8d86..a76eefa 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -5,32 +5,87 @@ + + +
+
+
+
+ +
+
{{ currentSong.name }}
+
{{ currentSong.artist }}
+
+ + +
+ + +
@@ -47,5 +102,123 @@ onUnmounted(() => { @media (max-width: 1336px) { padding: 80px 5vw 80px; } + + @media (max-width: 768px) { + padding: 72px 16px 200px; + } +} + +// Mobile mini player +.m-player { + position: fixed; + left: 8px; + right: 8px; + bottom: 68px; + height: 58px; + padding: 8px 10px; + display: flex; + align-items: center; + gap: 10px; + background: var(--bg-secondary); + border-radius: var(--radius-md); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35); + z-index: 95; + cursor: pointer; + + @media (min-width: 769px) { + display: none; + } +} + +.m-player-progress { + position: absolute; + top: 0; + left: 10px; + right: 10px; + height: 2px; +} + +.m-player-progress-fill { + height: 2px; + background: var(--color-primary); + border-radius: 1px; +} + +.m-player-info { + flex: 1; + min-width: 0; +} + +.m-player-name { + font-size: 13px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.m-player-artist { + font-size: 11px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.m-player-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + opacity: 0.85; + flex-shrink: 0; +} + +// Mobile bottom tab bar +.m-tabbar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + height: 60px; + display: flex; + align-items: center; + justify-content: space-around; + padding-bottom: env(safe-area-inset-bottom, 0); + background: var(--bg-navbar); + backdrop-filter: saturate(180%) blur(20px); + -webkit-backdrop-filter: saturate(180%) blur(20px); + border-top: 1px solid var(--border-color); + z-index: 100; + + @media (min-width: 769px) { + display: none; + } +} + +.m-tab { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 6px 14px; + color: var(--text-tertiary); + text-decoration: none; + font-family: inherit; + + &.active { + color: var(--color-primary); + } + + .tab-icon { + font-size: 22px; + } + + .tab-label { + font-size: 10px; + font-weight: 500; + } } diff --git a/web/src/components/Navbar.vue b/web/src/components/Navbar.vue index c095d5d..1776d59 100644 --- a/web/src/components/Navbar.vue +++ b/web/src/components/Navbar.vue @@ -5,6 +5,7 @@ @@ -19,38 +20,68 @@
+
机器人
- - - +
+
+ + + + + + +
-
-
点击切换 · 🔗 复制专属链接
@@ -227,6 +258,11 @@ onUnmounted(() => { @media (max-width: 1336px) { padding: 0 5vw; } + + @media (max-width: 768px) { + padding: 0 16px; + height: 52px; + } } .logo { @@ -234,11 +270,20 @@ onUnmounted(() => { font-weight: 700; color: var(--color-primary); margin-right: 40px; + + @media (max-width: 768px) { + font-size: 17px; + margin-right: 0; + } } .nav-links { display: flex; gap: 24px; + + @media (max-width: 768px) { + display: none; + } } .nav-link { @@ -266,7 +311,7 @@ onUnmounted(() => { opacity: 0.6; &.online { - background: rgba(51, 94, 234, 0.15); + background: var(--color-primary-15); color: var(--color-primary); opacity: 1; } @@ -295,12 +340,25 @@ onUnmounted(() => { background: var(--bg-card); border-color: var(--color-primary); } + + @media (max-width: 768px) { + padding: 6px 10px; + font-size: 12px; + font-weight: 600; + min-height: 32px; + gap: 6px; + border-radius: var(--radius-full); + } } .bot-state-mini { font-size: 14px; - &.playing { color: #22c55e; } - &.paused { color: #eab308; } + &.playing { color: var(--color-online); } + &.paused { color: var(--color-paused); } + + @media (max-width: 768px) { + display: none; + } } .bot-chevron { @@ -311,6 +369,10 @@ onUnmounted(() => { &.rotated { transform: rotate(180deg); } + + @media (max-width: 768px) { + display: none; + } } .bot-dot { @@ -321,7 +383,12 @@ onUnmounted(() => { flex-shrink: 0; &.online { - background: #22c55e; + background: var(--color-online); + } + + @media (max-width: 768px) { + width: 8px; + height: 8px; } } @@ -330,96 +397,83 @@ onUnmounted(() => { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + + @media (max-width: 768px) { + max-width: 80px; + } } .bot-dropdown { position: absolute; top: calc(100% + 6px); right: 0; - min-width: 200px; + min-width: 320px; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-md); - padding: 4px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); + padding: 6px; + box-shadow: var(--shadow-dropdown); z-index: 200; + + @media (max-width: 768px) { + position: fixed; + top: 52px; + left: 8px; + right: 8px; + min-width: auto; + } } -.bot-dropdown-item { +.bot-dropdown-header { + font-size: 11px; + font-weight: 600; + color: var(--text-tertiary); + padding: 6px 10px 4px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.bot-card { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px; + border-radius: var(--radius-sm); + margin-bottom: 4px; + border: 1px solid transparent; + transition: background var(--transition-fast); + + &.active { + background: var(--color-primary-12); + border-color: rgba(99, 102, 241, 0.25); + } +} + +.bot-card-head { display: flex; align-items: center; gap: 8px; - flex: 1; - min-width: 0; - padding: 8px 12px; - border-radius: var(--radius-sm); - font-size: 13px; cursor: pointer; - transition: background var(--transition-fast); - - &:hover { - background: var(--hover-bg); - } - - &.active { - background: rgba(51, 94, 234, 0.12); - color: var(--color-primary); - } } -.bot-dropdown-row { - display: flex; - align-items: center; - gap: 2px; -} - -.bot-dropdown-name { +.bot-card-name { flex: 1; + font-size: 13px; + font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.bot-link-btn { +.bot-current-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; - 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-power-btn { - flex-shrink: 0; - padding: 6px 8px; - border-radius: var(--radius-sm); - font-size: 16px; - opacity: 0.5; - color: var(--text-tertiary); - transition: opacity var(--transition-fast), background var(--transition-fast), color var(--transition-fast); - cursor: pointer; - - &:hover:not(:disabled) { - opacity: 1; - background: var(--hover-bg); - } - - &:disabled { - opacity: 0.25; - cursor: wait; - } - - &.online { - color: #22c55e; - opacity: 0.9; - } } .bot-playing-badge, @@ -427,25 +481,25 @@ onUnmounted(() => { .bot-idle-badge, .bot-offline-badge { font-size: 11px; - padding: 1px 6px; + padding: 2px 6px; border-radius: 4px; font-weight: 500; flex-shrink: 0; } .bot-playing-badge { - background: rgba(34, 197, 94, 0.15); - color: #22c55e; + background: var(--color-online-15); + color: var(--color-online); } .bot-paused-badge { - background: rgba(234, 179, 8, 0.15); - color: #eab308; + background: var(--color-paused-15); + color: var(--color-paused); } .bot-idle-badge { - background: rgba(51, 94, 234, 0.12); - color: var(--color-primary); + background: var(--hover-bg); + color: var(--text-secondary); } .bot-offline-badge { @@ -453,17 +507,47 @@ onUnmounted(() => { color: var(--text-tertiary); } -.bot-dropdown-divider { - height: 1px; - background: var(--border-color); - margin: 4px 0; +.bot-card-controls { + display: flex; + gap: 6px; } -.bot-dropdown-hint { - padding: 4px 12px 6px; +.bot-ctrl-btn { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 6px 8px; + border-radius: var(--radius-sm); + background: var(--hover-bg); + border: 1px solid var(--border-color); + color: var(--text-primary); font-size: 11px; - color: var(--text-tertiary); - text-align: center; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + transition: all var(--transition-fast); + + &:hover:not(:disabled) { + background: var(--bg-card); + border-color: var(--color-primary); + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + + &.primary { + background: var(--color-primary); + color: #fff; + border-color: var(--color-primary); + } + + &.danger { + color: #ef4444; + } } .settings-btn { @@ -471,12 +555,16 @@ onUnmounted(() => { opacity: 0.6; transition: opacity var(--transition-fast); &:hover { opacity: 1; } + + @media (max-width: 768px) { + display: none; + } } .link-dialog-backdrop { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.55); + background: var(--bg-modal-scrim); display: flex; align-items: center; justify-content: center; @@ -490,7 +578,7 @@ onUnmounted(() => { padding: 20px; min-width: 360px; max-width: 90vw; - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); + box-shadow: var(--shadow-modal); } .link-dialog-title { diff --git a/web/src/components/Player.vue b/web/src/components/Player.vue index e77b56c..7f4b836 100644 --- a/web/src/components/Player.vue +++ b/web/src/components/Player.vue @@ -206,6 +206,10 @@ function cycleMode() { left: 0; right: 0; z-index: 100; + + @media (max-width: 768px) { + display: none; + } } .player-bar { @@ -326,12 +330,12 @@ function cycleMode() { .bot-badge { display: inline-block; - font-size: 10px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semi); padding: 0 5px; - background: rgba(51, 94, 234, 0.15); + background: var(--color-primary-15); color: var(--color-primary); - border-radius: 3px; + border-radius: var(--radius-xs); line-height: 16px; white-space: nowrap; flex-shrink: 0; diff --git a/web/src/components/Queue.vue b/web/src/components/Queue.vue index 83356b1..5a654c9 100644 --- a/web/src/components/Queue.vue +++ b/web/src/components/Queue.vue @@ -146,7 +146,7 @@ async function removeSong(index: number) { } &.active { - background: rgba(51, 94, 234, 0.1); + background: var(--color-primary-10); } } diff --git a/web/src/components/SongCard.vue b/web/src/components/SongCard.vue index 35e48fc..9c8d21d 100644 --- a/web/src/components/SongCard.vue +++ b/web/src/components/SongCard.vue @@ -63,7 +63,7 @@ function formatDuration(seconds: number): string { } &.active { - background: rgba(51, 94, 234, 0.1); + background: var(--color-primary-10); } } @@ -96,31 +96,31 @@ function formatDuration(seconds: number): string { .platform-badge { flex-shrink: 0; - font-size: 10px; - font-weight: 600; + font-size: var(--fs-micro); + font-weight: var(--fw-semi); padding: 1px 5px; - border-radius: 3px; + border-radius: var(--radius-xs); line-height: 1.4; } .badge-netease { - background: rgba(232, 17, 35, 0.15); - color: #e81123; + background: var(--brand-netease-15); + color: var(--brand-netease); } .badge-qq { - background: rgba(18, 183, 106, 0.15); - color: #12b76a; + background: var(--brand-qq-15); + color: var(--brand-qq); } .badge-bilibili { - background: rgba(0, 161, 214, 0.15); - color: #00a1d6; + background: var(--brand-bilibili-15); + color: var(--brand-bilibili); } .badge-youtube { - background: rgba(255, 0, 0, 0.12); - color: #ff0000; + background: var(--brand-youtube-12); + color: var(--brand-youtube); } .song-artist { diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 356d57f..cc060f5 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -13,6 +13,11 @@ const router = createRouter({ name: 'search', component: () => import('../views/Search.vue'), }, + { + path: '/library', + name: 'library', + component: () => import('../views/Library.vue'), + }, { path: '/playlist/:id', name: 'playlist', diff --git a/web/src/styles/variables.scss b/web/src/styles/variables.scss index c407688..e65ab4c 100644 --- a/web/src/styles/variables.scss +++ b/web/src/styles/variables.scss @@ -1,33 +1,95 @@ -// YesPlayMusic-inspired design tokens +// TSMusicBot Design System — canonical tokens :root { - // Fonts + // Typography --font-primary: 'Barlow', -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, 'Cascadia Code', monospace; + + --fs-hero: 22px; + --fs-h1: 20px; + --fs-h2: 17px; + --fs-h3: 16px; + --fs-body: 14px; + --fs-sm: 13px; + --fs-xs: 12px; + --fs-2xs: 11px; + --fs-micro: 10px; + + --fw-regular: 400; + --fw-medium: 500; + --fw-semi: 600; + --fw-bold: 700; + --fw-black: 800; + + --lh-tight: 1.2; + --lh-normal: 1.4; + --lh-body: 1.5; // Spacing + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 36px; + --space-10: 80px; + + // Layout --navbar-height: 56px; --player-height: 56px; + --page-gutter: 10vw; // Radius + --radius-xs: 3px; --radius-sm: 6px; --radius-md: 10px; --radius-lg: 14px; --radius-xl: 20px; + --radius-full: 9999px; // Transitions --transition-fast: 0.2s ease; --transition-normal: 0.3s ease; + --ease-cinematic: cubic-bezier(0.25, 0.1, 0.25, 1); + + // Shadows + --shadow-dropdown: 0 8px 24px rgba(0, 0, 0, 0.2); + --shadow-modal: 0 12px 40px rgba(0, 0, 0, 0.35); // Accent --color-primary: #335eea; --color-primary-bg: #eaeffd; + --color-primary-15: rgba(51, 94, 234, 0.15); + --color-primary-10: rgba(51, 94, 234, 0.10); + --color-primary-12: rgba(51, 94, 234, 0.12); + --gradient-fm: linear-gradient(135deg, #335eea, #6366f1); + + // Semantic (theme-invariant) + --color-online: #22c55e; + --color-online-15: rgba(34, 197, 94, 0.15); + --color-paused: #eab308; + --color-paused-15: rgba(234, 179, 8, 0.15); + + // Platform brand + --brand-netease: #e81123; + --brand-netease-15: rgba(232, 17, 35, 0.15); + --brand-qq: #12b76a; + --brand-qq-15: rgba(18, 183, 106, 0.15); + --brand-bilibili: #00a1d6; + --brand-bilibili-15: rgba(0, 161, 214, 0.15); + --brand-youtube: #ff0000; + --brand-youtube-12: rgba(255, 0, 0, 0.12); } +// Dark theme (default) +:root, [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); + --bg-modal-scrim: rgba(0, 0, 0, 0.55); --text-primary: #ffffff; --text-secondary: rgba(255, 255, 255, 0.58); --text-tertiary: rgba(255, 255, 255, 0.38); @@ -40,6 +102,7 @@ --bg-secondary: #f5f5f7; --bg-card: rgba(0, 0, 0, 0.02); --bg-navbar: rgba(255, 255, 255, 0.86); + --bg-modal-scrim: rgba(0, 0, 0, 0.35); --text-primary: #000000; --text-secondary: rgba(0, 0, 0, 0.58); --text-tertiary: rgba(0, 0, 0, 0.38); diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index 1f8aaf8..1564c1f 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -179,6 +179,10 @@ onMounted(() => { .section { margin-bottom: 36px; + + @media (max-width: 768px) { + margin-bottom: 28px; + } } .section-title { @@ -188,6 +192,11 @@ onMounted(() => { display: flex; align-items: center; gap: 8px; + + @media (max-width: 768px) { + font-size: 18px; + margin-bottom: 12px; + } } .section-count { @@ -223,7 +232,7 @@ onMounted(() => { justify-content: center; width: 24px; height: 24px; - background: #00a1d6; + background: var(--brand-bilibili); color: white; border-radius: 4px; font-size: 14px; @@ -317,6 +326,7 @@ onMounted(() => { @media (max-width: 1200px) { grid-template-columns: repeat(4, 1fr); } @media (max-width: 900px) { grid-template-columns: repeat(3, 1fr); } + @media (max-width: 768px) { grid-template-columns: repeat(3, 1fr); gap: 14px; } } .daily-card { @@ -348,6 +358,7 @@ onMounted(() => { @media (max-width: 1200px) { grid-template-columns: repeat(4, 1fr); } @media (max-width: 900px) { grid-template-columns: repeat(3, 1fr); } + @media (max-width: 768px) { grid-template-columns: repeat(3, 1fr); gap: 14px; } } .playlist-card { diff --git a/web/src/views/Library.vue b/web/src/views/Library.vue new file mode 100644 index 0000000..f90a655 --- /dev/null +++ b/web/src/views/Library.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/web/src/views/Settings.vue b/web/src/views/Settings.vue index ca90e21..cacfed3 100755 --- a/web/src/views/Settings.vue +++ b/web/src/views/Settings.vue @@ -820,16 +820,16 @@ onUnmounted(() => { background: var(--border-color); color: var(--text-tertiary); &.online { - background: rgba(51, 94, 234, 0.15); + background: var(--color-primary-15); color: var(--color-primary); } &.playing { - background: rgba(76, 175, 80, 0.15); - color: #4caf50; + background: var(--color-online-15); + color: var(--color-online); } &.paused { - background: rgba(255, 152, 0, 0.15); - color: #ff9800; + background: var(--color-paused-15); + color: var(--color-paused); } } @@ -853,7 +853,7 @@ onUnmounted(() => { color: var(--color-primary); &.bilibili-icon { - color: #00a1d6; + color: var(--brand-bilibili); } } @@ -865,7 +865,7 @@ onUnmounted(() => { .account-status { font-size: 12px; color: var(--text-tertiary); - &.logged { color: #4caf50; } + &.logged { color: var(--color-online); } } .login-methods { @@ -888,7 +888,7 @@ onUnmounted(() => { &:hover { border-color: var(--color-primary); color: var(--color-primary); } &.active { - background: rgba(51, 94, 234, 0.1); + background: var(--color-primary-10); border-color: var(--color-primary); color: var(--color-primary); } @@ -1012,7 +1012,7 @@ onUnmounted(() => { &:hover { border-color: var(--border-color); } &.active { border-color: var(--color-primary); - background: rgba(51, 94, 234, 0.1); + background: var(--color-primary-10); } }