Browse Source

修改风格和功能

sequoia00 3 weeks ago
parent
commit
91c0081782
11 changed files with 1165 additions and 292 deletions
  1. 0 0
      AGENTS.md
  2. 7 1
      README.md
  3. 0 0
      app/AGENTS.md
  4. 34 0
      app/main.py
  5. BIN
      img/01.jpg
  6. BIN
      img/02.jpg
  7. BIN
      img/chenglong.jpg
  8. BIN
      img/mowenwei.jpg
  9. 485 130
      static/css/style.css
  10. 466 93
      static/js/app.js
  11. 173 68
      templates/index.html

File diff suppressed because it is too large
+ 0 - 0
AGENTS.md


+ 7 - 1
README.md

@@ -15,7 +15,13 @@ FastAPI 在线音乐播放器,音乐文件默认存储在 `mp3file/` 目录。
 
 
 ```bash
 ```bash
 conda activate py311
 conda activate py311
-cd /home/myprojector/musicweb
+cd musicwebplay
 pip install -r requirements.txt
 pip install -r requirements.txt
 uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
 uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
 ```
 ```
+
+也可以在项目根目录直接执行:
+
+```bash
+./start.sh
+```

File diff suppressed because it is too large
+ 0 - 0
app/AGENTS.md


+ 34 - 0
app/main.py

@@ -31,6 +31,7 @@ SUPPORTED_EXTENSIONS = {
     ".m4a",
     ".m4a",
     ".webm",
     ".webm",
 }
 }
+IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
 
 
 app = FastAPI(title="MusicWeb", version="1.0.0")
 app = FastAPI(title="MusicWeb", version="1.0.0")
 app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
 app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
@@ -98,11 +99,35 @@ def build_track(relative_path: str) -> dict[str, str]:
     }
     }
 
 
 
 
+def find_cover_for_directory(root: Path) -> str | None:
+    candidates = (
+        "cover.jpg",
+        "cover.jpeg",
+        "cover.png",
+        "cover.webp",
+        "folder.jpg",
+        "folder.jpeg",
+        "folder.png",
+        "folder.webp",
+    )
+    for candidate in candidates:
+        path = root / candidate
+        if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS:
+            return path.relative_to(MUSIC_DIR).as_posix()
+
+    for child in sorted(root.iterdir(), key=lambda item: item.name.lower()):
+        if child.is_file() and child.suffix.lower() in IMAGE_EXTENSIONS:
+            return child.relative_to(MUSIC_DIR).as_posix()
+    return None
+
+
 def build_tree(root: Path) -> dict[str, Any]:
 def build_tree(root: Path) -> dict[str, Any]:
     rel_root = root.relative_to(MUSIC_DIR) if root != MUSIC_DIR else Path(".")
     rel_root = root.relative_to(MUSIC_DIR) if root != MUSIC_DIR else Path(".")
+    cover_path = find_cover_for_directory(root)
     node = {
     node = {
         "name": "音乐库" if root == MUSIC_DIR else root.name,
         "name": "音乐库" if root == MUSIC_DIR else root.name,
         "path": "" if rel_root == Path(".") else rel_root.as_posix(),
         "path": "" if rel_root == Path(".") else rel_root.as_posix(),
+        "cover": f"/api/cover/{cover_path}" if cover_path else None,
         "folders": [],
         "folders": [],
         "tracks": [],
         "tracks": [],
     }
     }
@@ -153,6 +178,15 @@ def stream_file(file_path: str) -> FileResponse:
     return FileResponse(file, media_type=media_type, filename=file.name)
     return FileResponse(file, media_type=media_type, filename=file.name)
 
 
 
 
+@app.get("/api/cover/{file_path:path}")
+def cover_file(file_path: str) -> FileResponse:
+    file = safe_music_path(file_path)
+    if not file.exists() or not file.is_file() or file.suffix.lower() not in IMAGE_EXTENSIONS:
+        raise HTTPException(status_code=404, detail="Cover not found")
+    media_type = mimetypes.guess_type(file.name)[0] or "application/octet-stream"
+    return FileResponse(file, media_type=media_type, filename=file.name)
+
+
 @app.post("/api/upload")
 @app.post("/api/upload")
 async def upload_files(
 async def upload_files(
     files: list[UploadFile] = File(...),
     files: list[UploadFile] = File(...),

BIN
img/01.jpg


BIN
img/02.jpg


BIN
img/chenglong.jpg


BIN
img/mowenwei.jpg


+ 485 - 130
static/css/style.css

@@ -1,14 +1,13 @@
 :root {
 :root {
-  --bg: #09111f;
-  --bg-soft: rgba(12, 23, 44, 0.72);
-  --panel: rgba(20, 33, 58, 0.88);
-  --line: rgba(255, 255, 255, 0.1);
-  --text: #eff6ff;
-  --muted: #9fb3c8;
-  --accent: #61d7b6;
-  --accent-2: #f6b15f;
-  --danger: #ff7b7b;
-  --shadow: 0 24px 70px rgba(0, 0, 0, 0.35);
+  --bg: #f6efe6;
+  --panel: rgba(255, 249, 242, 0.94);
+  --panel-strong: #fffaf5;
+  --line: rgba(95, 73, 45, 0.12);
+  --text: #2c2218;
+  --muted: #7b6854;
+  --accent: #d76434;
+  --accent-deep: #b6481c;
+  --shadow: 0 18px 50px rgba(88, 55, 26, 0.14);
 }
 }
 
 
 * {
 * {
@@ -18,219 +17,575 @@
 body {
 body {
   margin: 0;
   margin: 0;
   min-height: 100vh;
   min-height: 100vh;
-  font-family: "Segoe UI", "PingFang SC", sans-serif;
+  font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
   color: var(--text);
   color: var(--text);
   background:
   background:
-    radial-gradient(circle at top left, rgba(97, 215, 182, 0.22), transparent 30%),
-    radial-gradient(circle at top right, rgba(246, 177, 95, 0.18), transparent 28%),
-    linear-gradient(135deg, #08101d 0%, #0d1d35 54%, #10284c 100%);
+    radial-gradient(circle at top left, rgba(215, 100, 52, 0.18), transparent 28%),
+    radial-gradient(circle at top right, rgba(255, 200, 127, 0.18), transparent 22%),
+    linear-gradient(180deg, #faf3e8 0%, #f4ebde 45%, #efe4d5 100%);
 }
 }
 
 
-.page-shell {
-  display: grid;
-  grid-template-columns: 340px 1fr;
-  gap: 24px;
-  padding: 24px;
+button,
+input {
+  font: inherit;
+}
+
+button {
+  border: 0;
+  cursor: pointer;
 }
 }
 
 
-.glass {
-  backdrop-filter: blur(18px);
-  background: var(--bg-soft);
+input[type="text"],
+input[type="file"] {
+  width: 100%;
+  padding: 12px 14px;
+  border: 1px solid var(--line);
+  border-radius: 14px;
+  background: rgba(255, 255, 255, 0.8);
+  color: var(--text);
+}
+
+.app-shell {
+  max-width: 1180px;
+  margin: 0 auto;
+  padding: 10px 10px 188px;
+}
+
+.app-header,
+.content-section,
+.player-dock {
+  background: var(--panel);
   border: 1px solid var(--line);
   border: 1px solid var(--line);
-  border-radius: 24px;
   box-shadow: var(--shadow);
   box-shadow: var(--shadow);
+  backdrop-filter: blur(16px);
 }
 }
 
 
-.sidebar,
-.content {
+.app-header {
   display: flex;
   display: flex;
-  flex-direction: column;
-  gap: 20px;
+  justify-content: space-between;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 14px;
+  border-radius: 18px;
 }
 }
 
 
-.sidebar {
-  padding: 24px;
+.header-copy h1,
+.content-section h2 {
+  margin: 0;
 }
 }
 
 
-.brand h1,
-.hero h2 {
-  margin: 0;
-  font-size: 2rem;
+.header-copy h1 {
+  font-size: 1rem;
 }
 }
 
 
 .eyebrow {
 .eyebrow {
-  margin: 0 0 10px;
-  text-transform: uppercase;
-  letter-spacing: 0.18em;
-  font-size: 0.72rem;
+  margin: 0 0 4px;
   color: var(--accent);
   color: var(--accent);
+  font-size: 0.64rem;
+  letter-spacing: 0.16em;
+  text-transform: uppercase;
 }
 }
 
 
 .subtext {
 .subtext {
+  margin: 0;
   color: var(--muted);
   color: var(--muted);
-  line-height: 1.6;
+  line-height: 1.45;
+  font-size: 0.86rem;
 }
 }
 
 
-.panel,
-.hero,
-.library-panel,
-.queue-panel {
-  padding: 20px;
+.header-pill {
+  flex: 0 0 auto;
+  padding: 7px 10px;
+  border-radius: 999px;
+  background: rgba(215, 100, 52, 0.1);
+  color: var(--accent-deep);
+  font-size: 0.82rem;
 }
 }
 
 
-.panel-head {
+.page-content {
+  margin-top: 12px;
   display: flex;
   display: flex;
-  justify-content: space-between;
-  align-items: center;
-  gap: 16px;
-  margin-bottom: 14px;
+  flex-direction: column;
+  gap: 14px;
 }
 }
 
 
-.panel-head h2 {
-  margin: 0;
-  font-size: 1.05rem;
+.view-stack {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
 }
 }
 
 
-.field {
+.is-hidden {
+  display: none !important;
+}
+
+.content-section {
+  border-radius: 24px;
+  padding: 16px;
+}
+
+.slim-hero {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  gap: 8px;
-  margin-bottom: 12px;
-  color: var(--muted);
-  font-size: 0.92rem;
+  gap: 12px;
+  padding: 14px 16px;
 }
 }
 
 
-input {
-  width: 100%;
-  border: 1px solid var(--line);
-  background: rgba(255, 255, 255, 0.04);
-  color: var(--text);
-  border-radius: 14px;
-  padding: 12px 14px;
+.hero-metrics {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 10px;
 }
 }
 
 
-button {
-  border: 0;
-  border-radius: 999px;
-  padding: 11px 16px;
-  cursor: pointer;
-  transition: transform 0.18s ease, opacity 0.18s ease, background 0.18s ease;
+.metric-card {
+  padding: 12px;
+  border-radius: 18px;
+  background: rgba(255, 255, 255, 0.8);
 }
 }
 
 
-button:hover {
-  transform: translateY(-1px);
+.metric-card strong {
+  display: block;
+  font-size: 1.25rem;
+  margin-bottom: 2px;
 }
 }
 
 
-.primary-btn {
-  background: linear-gradient(135deg, var(--accent), #2eb6f0);
-  color: #072035;
-  font-weight: 700;
+.metric-card span {
+  color: var(--muted);
+  font-size: 0.84rem;
 }
 }
 
 
-.secondary-btn,
-.ghost-btn {
-  background: rgba(255, 255, 255, 0.07);
-  color: var(--text);
+.section-head {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 12px;
 }
 }
 
 
-.secondary-btn.active,
-.ghost-btn.active {
-  background: rgba(97, 215, 182, 0.18);
-  color: var(--accent);
+.section-head h2 {
+  font-size: 1rem;
 }
 }
 
 
-.content {
-  min-width: 0;
+.section-head span {
+  color: var(--muted);
+  font-size: 0.8rem;
 }
 }
 
 
-.hero {
+.featured-list,
+.song-list,
+.ranking-list,
+.queue-list,
+.playlist-list {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  gap: 18px;
+  gap: 10px;
 }
 }
 
 
-.hero-actions {
+.album-grid,
+.stack-section {
+  display: grid;
+  gap: 10px;
+}
+
+.album-grid {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.stack-section {
+  grid-template-columns: 1fr;
+}
+
+.feature-card,
+.album-card,
+.song-row,
+.ranking-card,
+.playlist-item,
+.empty-state,
+.detail-header {
+  border: 1px solid rgba(123, 104, 84, 0.12);
+  background: var(--panel-strong);
+  border-radius: 20px;
+}
+
+.feature-card {
+  overflow: hidden;
+}
+
+.feature-card-inner {
+  display: grid;
+  grid-template-columns: 108px 1fr;
+  min-height: 176px;
+}
+
+.album-cover,
+.feature-cover,
+.detail-cover-wrap {
+  position: relative;
+  overflow: hidden;
+  background: linear-gradient(145deg, rgba(215, 100, 52, 0.94), rgba(169, 65, 29, 0.92));
+  color: #fff7f0;
   display: flex;
   display: flex;
-  flex-wrap: wrap;
-  gap: 12px;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+  font-weight: 700;
 }
 }
 
 
-audio {
+.feature-cover {
+  padding: 14px;
+}
+
+.album-cover {
+  min-height: 118px;
+  padding: 12px;
+  border-bottom: 1px solid rgba(123, 104, 84, 0.12);
+}
+
+.cover-image,
+.detail-cover-image {
+  position: absolute;
+  inset: 0;
   width: 100%;
   width: 100%;
+  height: 100%;
+  object-fit: cover;
 }
 }
 
 
-.library-grid {
-  display: grid;
-  grid-template-columns: 1.1fr 1fr;
-  gap: 24px;
+.cover-name,
+.detail-cover-fallback {
+  position: relative;
+  z-index: 1;
+  line-height: 1.3;
 }
 }
 
 
-.folder-tree,
-.queue-list,
-.playlist-list {
+.feature-body,
+.album-body {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  gap: 12px;
-  max-height: 62vh;
-  overflow: auto;
-  padding-right: 4px;
+  gap: 10px;
+  padding: 12px;
 }
 }
 
 
-.folder-node {
-  border: 1px solid var(--line);
-  border-radius: 18px;
-  padding: 14px;
-  background: rgba(255, 255, 255, 0.03);
+.feature-body h3,
+.album-body h3,
+.ranking-card h3,
+.track-name,
+.detail-copy h2 {
+  margin: 0;
 }
 }
 
 
-.folder-node h3,
-.folder-node h4 {
-  margin: 0 0 8px;
+.meta-row,
+.track-path,
+.playlist-item p,
+.ranking-note {
+  margin: 0;
+  color: var(--muted);
+  font-size: 0.84rem;
+  word-break: break-word;
 }
 }
 
 
-.folder-actions,
+.card-actions,
 .track-actions {
 .track-actions {
   display: flex;
   display: flex;
   flex-wrap: wrap;
   flex-wrap: wrap;
   gap: 8px;
   gap: 8px;
 }
 }
 
 
-.track-item,
+.compact-actions {
+  flex-wrap: nowrap;
+}
+
+.primary-btn,
+.secondary-btn,
+.text-btn,
+.back-btn {
+  padding: 9px 13px;
+  border-radius: 999px;
+}
+
+.primary-btn {
+  background: linear-gradient(135deg, var(--accent), #ea9660);
+  color: #fff7f1;
+}
+
+.secondary-btn {
+  background: rgba(215, 100, 52, 0.1);
+  color: var(--accent-deep);
+}
+
+.text-btn,
+.back-btn {
+  background: rgba(123, 104, 84, 0.08);
+  color: var(--text);
+}
+
+.song-row,
+.ranking-card,
 .playlist-item {
 .playlist-item {
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
-  gap: 12px;
   align-items: center;
   align-items: center;
-  border: 1px solid var(--line);
-  border-radius: 18px;
+  gap: 10px;
+  padding: 12px;
+}
+
+.compact-list .song-row {
+  padding: 10px 12px;
+}
+
+.compact-list .track-name {
+  font-size: 0.92rem;
+}
+
+.compact-list .track-path {
+  font-size: 0.76rem;
+}
+
+.song-copy,
+.ranking-main {
+  min-width: 0;
+  flex: 1;
+}
+
+.queue-list .song-row.is-active {
+  border-color: rgba(215, 100, 52, 0.5);
+  box-shadow: inset 0 0 0 1px rgba(215, 100, 52, 0.15);
+}
+
+.rank-badge {
+  width: 34px;
+  height: 34px;
+  flex: 0 0 34px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 14px;
+  background: rgba(215, 100, 52, 0.12);
+  color: var(--accent-deep);
+  font-weight: 700;
+}
+
+.detail-header {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 14px;
   padding: 14px;
   padding: 14px;
-  background: rgba(255, 255, 255, 0.03);
 }
 }
 
 
-.track-name {
+.detail-cover-wrap {
+  width: 100%;
+  aspect-ratio: 1 / 1;
+  border-radius: 22px;
+}
+
+.detail-copy {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.field {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  margin-bottom: 12px;
+  color: var(--muted);
+  font-size: 0.9rem;
+}
+
+.empty-state {
+  padding: 16px;
+  text-align: center;
+  color: var(--muted);
+}
+
+.player-dock {
+  position: fixed;
+  left: 8px;
+  right: 8px;
+  bottom: max(8px, env(safe-area-inset-bottom));
+  z-index: 999;
+  padding: 12px 14px;
+  border-radius: 24px;
+  background:
+    linear-gradient(180deg, rgba(255, 250, 244, 0.97), rgba(246, 237, 226, 0.99)),
+    var(--panel);
+}
+
+.player-top {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.dock-cover-wrap {
+  position: relative;
+  width: 50px;
+  height: 50px;
+  flex: 0 0 50px;
+}
+
+.dock-cover-image,
+.dock-cover-fallback {
+  width: 100%;
+  height: 100%;
+  border-radius: 16px;
+}
+
+.dock-cover-image {
+  object-fit: cover;
+}
+
+.dock-cover-fallback {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, #de8552, #b54c22);
+  color: #fff8f2;
+  font-size: 1rem;
+  font-weight: 700;
+}
+
+.dock-copy {
+  min-width: 0;
+}
+
+.dock-copy strong,
+.dock-copy p {
   display: block;
   display: block;
-  margin-bottom: 5px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 }
 
 
-.track-path {
-  margin: 0;
+.dock-copy p {
+  margin: 3px 0 0;
   color: var(--muted);
   color: var(--muted);
-  font-size: 0.88rem;
-  word-break: break-all;
+  font-size: 0.82rem;
 }
 }
 
 
-.playlist-item p {
-  margin: 4px 0 0;
+.player-progress {
+  display: grid;
+  grid-template-columns: 40px 1fr 40px;
+  gap: 8px;
+  align-items: center;
+  margin-top: 10px;
   color: var(--muted);
   color: var(--muted);
+  font-size: 0.76rem;
+}
+
+input[type="range"] {
+  width: 100%;
+  accent-color: var(--accent);
+}
+
+.icon-actions {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 12px;
+}
+
+.icon-btn {
+  width: 42px;
+  height: 42px;
+  border-radius: 50%;
+  background: rgba(123, 104, 84, 0.08);
+  color: var(--text);
+  position: relative;
+  flex: 0 0 42px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
 }
 }
 
 
-@media (max-width: 1080px) {
-  .page-shell,
-  .library-grid {
-    grid-template-columns: 1fr;
+.icon-btn.active {
+  background: rgba(215, 100, 52, 0.12);
+  color: var(--accent-deep);
+}
+
+.play-toggle {
+  width: 54px;
+  height: 54px;
+  flex-basis: 54px;
+  background: linear-gradient(135deg, var(--accent), #e98f58);
+  color: #fff8f2;
+}
+
+.small-icon-btn {
+  width: 30px;
+  height: 30px;
+  flex-basis: 30px;
+}
+
+.icon-btn svg {
+  width: 22px;
+  height: 22px;
+  stroke: currentColor;
+  fill: none;
+  stroke-width: 1.9;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.small-icon-btn svg {
+  width: 16px;
+  height: 16px;
+  stroke-width: 2.1;
+}
+
+.play-toggle svg {
+  width: 26px;
+  height: 26px;
+}
+
+.play-toggle .icon-play {
+  margin-left: 2px;
+}
+
+.play-toggle svg path {
+  fill: currentColor;
+  stroke: none;
+}
+
+.icon-toast {
+  position: fixed;
+  left: 50%;
+  bottom: 152px;
+  transform: translateX(-50%);
+  z-index: 1200;
+  padding: 8px 12px;
+  border-radius: 999px;
+  background: rgba(44, 34, 24, 0.88);
+  color: #fffaf5;
+  font-size: 0.78rem;
+  white-space: nowrap;
+}
+
+audio {
+  display: none;
+}
+
+@media (min-width: 760px) {
+  .app-shell {
+    padding: 12px 14px 184px;
+  }
+
+  .stack-section {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .featured-list {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 10px;
+  }
+
+  .detail-header {
+    grid-template-columns: 120px 1fr;
+    align-items: start;
   }
   }
 
 
-  .sidebar {
-    order: 2;
+  .detail-cover-wrap {
+    width: 120px;
   }
   }
 }
 }

+ 466 - 93
static/js/app.js

@@ -5,86 +5,339 @@ const state = {
   currentTrackIndex: -1,
   currentTrackIndex: -1,
   shuffle: false,
   shuffle: false,
   loopMode: "list",
   loopMode: "list",
+  albums: [],
+  currentView: "home",
+  activeAlbumPath: "",
 };
 };
 
 
 const audio = document.getElementById("audioPlayer");
 const audio = document.getElementById("audioPlayer");
+const progressBar = document.getElementById("progressBar");
+let toastTimer = null;
+
+function savePlaybackState() {
+  const currentTrack = state.currentQueue[state.currentTrackIndex] || null;
+  const payload = {
+    currentFolder: state.currentFolder,
+    currentQueue: state.currentQueue.map((track) => track.path),
+    currentTrackPath: currentTrack ? currentTrack.path : "",
+    currentTime: audio.currentTime || 0,
+    shuffle: state.shuffle,
+    loopMode: state.loopMode,
+    activeAlbumPath: state.activeAlbumPath,
+  };
+  localStorage.setItem("musicwebplayback", JSON.stringify(payload));
+}
+
+function showIconToast(text) {
+  const toast = document.getElementById("iconToast");
+  toast.textContent = text;
+  toast.classList.remove("is-hidden");
+  if (toastTimer) clearTimeout(toastTimer);
+  toastTimer = setTimeout(() => toast.classList.add("is-hidden"), 1200);
+}
+
+function restorePlaybackState() {
+  const raw = localStorage.getItem("musicwebplayback");
+  if (!raw || !state.library) return;
+
+  try {
+    const saved = JSON.parse(raw);
+    state.shuffle = Boolean(saved.shuffle);
+    state.loopMode = saved.loopMode || "list";
+    state.currentFolder = saved.currentFolder || "";
+    state.activeAlbumPath = saved.activeAlbumPath || "";
+    updateToggleStates();
+    setCurrentFolder(state.currentFolder);
+
+    if (Array.isArray(saved.currentQueue) && saved.currentQueue.length) {
+      state.currentQueue = saved.currentQueue
+        .map((path) => state.library.all_tracks.find((track) => track.path === path))
+        .filter(Boolean);
+    }
+
+    const trackPath = saved.currentTrackPath || "";
+    if (trackPath) {
+      const queueIndex = state.currentQueue.findIndex((track) => track.path === trackPath);
+      if (queueIndex >= 0) {
+        state.currentTrackIndex = queueIndex;
+      } else {
+        const single = state.library.all_tracks.find((track) => track.path === trackPath);
+        if (single) {
+          state.currentQueue = [single];
+          state.currentTrackIndex = 0;
+        }
+      }
+    }
+
+    if (state.currentQueue.length && state.currentTrackIndex >= 0) {
+      const track = state.currentQueue[state.currentTrackIndex];
+      audio.src = track.url;
+      updateDock(track);
+      renderQueue();
+      syncPlayButton();
+      audio.addEventListener(
+        "loadedmetadata",
+        () => {
+          audio.currentTime = Number(saved.currentTime || 0);
+        },
+        { once: true }
+      );
+    }
+
+    if (state.activeAlbumPath) {
+      renderAlbumDetail(state.activeAlbumPath);
+    }
+  } catch {
+    localStorage.removeItem("musicwebplayback");
+  }
+}
 
 
 async function fetchLibrary() {
 async function fetchLibrary() {
   const response = await fetch("/api/library");
   const response = await fetch("/api/library");
   state.library = await response.json();
   state.library = await response.json();
-  renderTree();
+  state.albums = buildAlbums(state.library.tree);
+
+  document.getElementById("albumCount").textContent = state.albums.length;
+  document.getElementById("trackCount").textContent = state.library.all_tracks.length;
+
+  renderHome();
   renderPlaylists();
   renderPlaylists();
+  restorePlaybackState();
+
+  if (state.activeAlbumPath) {
+    renderAlbumDetail(state.activeAlbumPath);
+  }
+
   if (!state.currentQueue.length) {
   if (!state.currentQueue.length) {
     state.currentQueue = [...state.library.all_tracks];
     state.currentQueue = [...state.library.all_tracks];
     renderQueue();
     renderQueue();
   }
   }
 }
 }
 
 
-function toggleButton(id, active, label) {
-  const button = document.getElementById(id);
-  button.classList.toggle("active", active);
-  button.textContent = label;
-}
+function buildAlbums(tree) {
+  const albums = [];
 
 
-function folderTracks(path, node = state.library.tree) {
-  if (node.path === path) return flattenNodeTracks(node);
-  for (const child of node.folders) {
-    const result = folderTracks(path, child);
-    if (result) return result;
+  function visit(node) {
+    const tracks = flattenNodeTracks(node);
+    if (node.path && tracks.length) {
+      albums.push({
+        name: node.name || "未命名专辑",
+        path: node.path,
+        cover: node.cover || null,
+        tracks,
+      });
+    }
+    node.folders.forEach(visit);
   }
   }
-  return null;
+
+  visit(tree);
+  return albums;
 }
 }
 
 
 function flattenNodeTracks(node) {
 function flattenNodeTracks(node) {
   const tracks = [...node.tracks];
   const tracks = [...node.tracks];
-  for (const child of node.folders) {
-    tracks.push(...flattenNodeTracks(child));
-  }
+  node.folders.forEach((child) => tracks.push(...flattenNodeTracks(child)));
   return tracks;
   return tracks;
 }
 }
 
 
-function renderTree() {
-  const container = document.getElementById("folderTree");
-  container.innerHTML = "";
-  container.appendChild(renderFolderNode(state.library.tree));
+function folderTracks(path) {
+  const album = state.albums.find((item) => item.path === path);
+  return album ? [...album.tracks] : [];
 }
 }
 
 
-function renderFolderNode(node) {
-  const wrapper = document.createElement("div");
-  wrapper.className = "folder-node";
+function setCurrentFolder(path) {
+  state.currentFolder = path;
+  document.getElementById("folderIndicator").textContent = path || "根目录";
+}
 
 
-  const title = document.createElement(node.path ? "h4" : "h3");
-  title.textContent = node.name;
-  wrapper.appendChild(title);
+function showView(name) {
+  state.currentView = name;
+  document.getElementById("homeView").classList.toggle("is-hidden", name !== "home");
+  document.getElementById("detailView").classList.toggle("is-hidden", name !== "detail");
+  savePlaybackState();
+}
+
+function coverLabel(name) {
+  return name.length > 12 ? `${name.slice(0, 12)}...` : name;
+}
 
 
-  const actions = document.createElement("div");
-  actions.className = "folder-actions";
+function buildCoverMarkup(album, className) {
+  const imageMarkup = album.cover
+    ? `<img class="cover-image" src="${album.cover}" alt="${album.name}" />`
+    : "";
+  return `
+    <div class="${className}">
+      ${imageMarkup}
+      <span class="cover-name">${coverLabel(album.name)}</span>
+    </div>
+  `;
+}
+
+function renderHome() {
+  const featured = [...state.albums].sort((a, b) => b.tracks.length - a.tracks.length).slice(0, 2);
+  const latestTracks = [...state.library.all_tracks].slice(-8).reverse();
+  const rankingAlbums = [...state.albums].sort((a, b) => b.tracks.length - a.tracks.length).slice(0, 6);
+
+  renderFeaturedAlbums(featured);
+  renderAlbumGrid(state.albums);
+  renderLatestList(latestTracks);
+  renderRankingList(rankingAlbums);
+  renderQueue();
+}
 
 
-  const useBtn = document.createElement("button");
-  useBtn.className = "ghost-btn";
-  useBtn.textContent = "设为当前目录";
-  useBtn.onclick = () => {
-    state.currentFolder = node.path;
-    document.getElementById("folderIndicator").textContent = `当前目录:${node.path || "根目录"}`;
+function buildAlbumCardActions(card, album) {
+  const detailBtn = card.querySelector("[data-action='detail']");
+  const playBtn = card.querySelector("[data-action='play']");
+  detailBtn.onclick = () => renderAlbumDetail(album.path);
+  playBtn.onclick = (event) => {
+    event.stopPropagation();
+    setCurrentFolder(album.path);
+    startQueue(album.tracks, 0);
   };
   };
+  card.onclick = () => renderAlbumDetail(album.path);
+}
 
 
-  const playBtn = document.createElement("button");
-  playBtn.className = "ghost-btn";
-  playBtn.textContent = "播放此目录";
-  playBtn.onclick = () => {
-    const tracks = flattenNodeTracks(node);
-    if (!tracks.length) return alert("该目录下没有可播放文件");
-    startQueue(tracks, 0);
-    state.currentFolder = node.path;
-    document.getElementById("folderIndicator").textContent = `当前目录:${node.path || "根目录"}`;
+function renderFeaturedAlbums(items) {
+  const container = document.getElementById("featuredAlbums");
+  container.innerHTML = "";
+
+  if (!items.length) {
+    container.innerHTML = '<div class="empty-state">暂无可展示的专辑目录</div>';
+    return;
+  }
+
+  items.forEach((album) => {
+    const card = document.createElement("article");
+    card.className = "feature-card";
+    card.innerHTML = `
+      <div class="feature-card-inner">
+        ${buildCoverMarkup(album, "feature-cover")}
+        <div class="feature-body">
+          <div>
+            <h3>${album.name}</h3>
+            <p class="meta-row">${album.tracks.length} 首歌曲 · ${album.path}</p>
+          </div>
+          <p class="subtext">点击进入详细目录页,查看歌曲列表并播放。</p>
+          <div class="card-actions">
+            <button class="primary-btn" data-action="detail">进入目录</button>
+            <button class="secondary-btn" data-action="play">播放</button>
+          </div>
+        </div>
+      </div>
+    `;
+    buildAlbumCardActions(card, album);
+    container.appendChild(card);
+  });
+}
+
+function renderAlbumGrid(items) {
+  const container = document.getElementById("albumGrid");
+  container.innerHTML = "";
+
+  if (!items.length) {
+    container.innerHTML = '<div class="empty-state">当前音乐库还没有专辑目录</div>';
+    return;
+  }
+
+  items.forEach((album) => {
+    const card = document.createElement("article");
+    card.className = "album-card";
+    card.innerHTML = `
+      ${buildCoverMarkup(album, "album-cover")}
+      <div class="album-body">
+        <div>
+          <h3>${album.name}</h3>
+          <p class="meta-row">${album.tracks.length} 首歌曲</p>
+        </div>
+        <div class="card-actions">
+          <button class="secondary-btn" data-action="detail">进入</button>
+          <button class="text-btn" data-action="play">播放</button>
+        </div>
+      </div>
+    `;
+    buildAlbumCardActions(card, album);
+    container.appendChild(card);
+  });
+}
+
+function renderAlbumDetail(path) {
+  const album = state.albums.find((item) => item.path === path);
+  if (!album) return;
+
+  state.activeAlbumPath = path;
+  setCurrentFolder(path);
+  showView("detail");
+
+  document.getElementById("detailTitle").textContent = album.name;
+  document.getElementById("detailMeta").textContent = `${album.path} · ${album.tracks.length} 首歌曲`;
+  document.getElementById("detailTrackCount").textContent = `${album.tracks.length} 首`;
+
+  const coverImage = document.getElementById("detailCoverImage");
+  const coverFallback = document.getElementById("detailCoverFallback");
+  if (album.cover) {
+    coverImage.src = album.cover;
+    coverImage.classList.remove("is-hidden");
+    coverFallback.classList.add("is-hidden");
+  } else {
+    coverImage.removeAttribute("src");
+    coverImage.classList.add("is-hidden");
+    coverFallback.classList.remove("is-hidden");
+    coverFallback.textContent = coverLabel(album.name);
+  }
+
+  document.getElementById("detailPlayBtn").onclick = () => startQueue(album.tracks, 0);
+  document.getElementById("detailPlayAllBtn").onclick = () => {
+    state.currentQueue = [...album.tracks, ...state.currentQueue];
+    state.currentTrackIndex = 0;
+    renderQueue();
+    playTrack(0);
   };
   };
 
 
-  actions.append(useBtn, playBtn);
-  wrapper.appendChild(actions);
+  const container = document.getElementById("detailTrackList");
+  container.innerHTML = "";
+  album.tracks.forEach((track) => container.appendChild(renderTrack(track)));
+}
 
 
-  node.tracks.forEach((track) => wrapper.appendChild(renderTrack(track)));
-  node.folders.forEach((folder) => wrapper.appendChild(renderFolderNode(folder)));
-  return wrapper;
+function renderLatestList(tracks) {
+  const container = document.getElementById("latestList");
+  container.innerHTML = "";
+  if (!tracks.length) {
+    container.innerHTML = '<div class="empty-state">暂无最新更新</div>';
+    return;
+  }
+  tracks.forEach((track) => container.appendChild(renderTrack(track)));
+}
+
+function renderRankingList(items) {
+  const container = document.getElementById("rankingList");
+  container.innerHTML = "";
+  if (!items.length) {
+    container.innerHTML = '<div class="empty-state">暂无排名数据</div>';
+    return;
+  }
+
+  items.forEach((album, index) => {
+    const card = document.createElement("div");
+    card.className = "ranking-card";
+    card.innerHTML = `
+      <span class="rank-badge">${index + 1}</span>
+      <div class="ranking-main">
+        <h3>${album.name}</h3>
+        <p class="ranking-note">${album.tracks.length} 首歌曲</p>
+      </div>
+      <div class="track-actions compact-actions">
+        <button class="icon-btn small-icon-btn play-btn" aria-label="播放"></button>
+      </div>
+    `;
+    card.onclick = () => renderAlbumDetail(album.path);
+    card.querySelector("button").onclick = (event) => {
+      event.stopPropagation();
+      showIconToast("播放");
+      setCurrentFolder(album.path);
+      startQueue(album.tracks, 0);
+    };
+    container.appendChild(card);
+  });
 }
 }
 
 
 function renderTrack(track) {
 function renderTrack(track) {
@@ -92,12 +345,18 @@ function renderTrack(track) {
   const fragment = template.content.cloneNode(true);
   const fragment = template.content.cloneNode(true);
   fragment.querySelector(".track-name").textContent = track.name;
   fragment.querySelector(".track-name").textContent = track.name;
   fragment.querySelector(".track-path").textContent = track.path;
   fragment.querySelector(".track-path").textContent = track.path;
-  fragment.querySelector(".play-btn").onclick = () => startQueue([track], 0);
+  fragment.querySelector(".play-btn").onclick = () => {
+    showIconToast("播放");
+    startQueue([track], 0);
+  };
   fragment.querySelector(".queue-btn").onclick = () => {
   fragment.querySelector(".queue-btn").onclick = () => {
+    showIconToast("加入队列");
     state.currentQueue.push(track);
     state.currentQueue.push(track);
     renderQueue();
     renderQueue();
+    savePlaybackState();
   };
   };
   fragment.querySelector(".move-btn").onclick = async () => {
   fragment.querySelector(".move-btn").onclick = async () => {
+    showIconToast("移动歌曲");
     const destination = prompt("移动到哪个目录?输入相对 mp3file 的目录路径", track.folder || "");
     const destination = prompt("移动到哪个目录?输入相对 mp3file 的目录路径", track.folder || "");
     if (destination === null) return;
     if (destination === null) return;
     await fetch("/api/move", {
     await fetch("/api/move", {
@@ -113,36 +372,50 @@ function renderTrack(track) {
 function renderQueue() {
 function renderQueue() {
   const container = document.getElementById("queueList");
   const container = document.getElementById("queueList");
   container.innerHTML = "";
   container.innerHTML = "";
+  if (!state.currentQueue.length) {
+    container.innerHTML = '<div class="empty-state">当前播放队列为空</div>';
+    return;
+  }
+
   state.currentQueue.forEach((track, index) => {
   state.currentQueue.forEach((track, index) => {
-    const item = document.createElement("div");
-    item.className = "track-item";
-    if (index === state.currentTrackIndex) item.style.borderColor = "var(--accent)";
-    item.innerHTML = `
-      <div>
+    const row = document.createElement("div");
+    row.className = `song-row${index === state.currentTrackIndex ? " is-active" : ""}`;
+    row.innerHTML = `
+      <div class="song-copy">
         <strong class="track-name">${track.name}</strong>
         <strong class="track-name">${track.name}</strong>
         <p class="track-path">${track.path}</p>
         <p class="track-path">${track.path}</p>
       </div>
       </div>
-      <div class="track-actions">
-        <button class="ghost-btn">播放</button>
-        <button class="ghost-btn">移除</button>
+      <div class="track-actions compact-actions">
+        <button class="icon-btn small-icon-btn play-btn" aria-label="播放"></button>
+        <button class="icon-btn small-icon-btn move-btn" aria-label="移除"></button>
       </div>
       </div>
     `;
     `;
-    const [playBtn, removeBtn] = item.querySelectorAll("button");
-    playBtn.onclick = () => playTrack(index);
+    const [playBtn, removeBtn] = row.querySelectorAll("button");
+    playBtn.onclick = () => {
+      showIconToast("播放");
+      playTrack(index);
+    };
     removeBtn.onclick = () => {
     removeBtn.onclick = () => {
+      showIconToast("移除");
       state.currentQueue.splice(index, 1);
       state.currentQueue.splice(index, 1);
       if (state.currentTrackIndex >= state.currentQueue.length) {
       if (state.currentTrackIndex >= state.currentQueue.length) {
         state.currentTrackIndex = state.currentQueue.length - 1;
         state.currentTrackIndex = state.currentQueue.length - 1;
       }
       }
       renderQueue();
       renderQueue();
+      savePlaybackState();
     };
     };
-    container.appendChild(item);
+    container.appendChild(row);
   });
   });
 }
 }
 
 
 function renderPlaylists() {
 function renderPlaylists() {
   const container = document.getElementById("playlistList");
   const container = document.getElementById("playlistList");
   container.innerHTML = "";
   container.innerHTML = "";
+  if (!state.library.playlists.length) {
+    container.innerHTML = '<div class="empty-state">还没有播放列表</div>';
+    return;
+  }
+
   state.library.playlists.forEach((playlist) => {
   state.library.playlists.forEach((playlist) => {
     const item = document.createElement("div");
     const item = document.createElement("div");
     item.className = "playlist-item";
     item.className = "playlist-item";
@@ -151,20 +424,23 @@ function renderPlaylists() {
         <strong>${playlist.name}</strong>
         <strong>${playlist.name}</strong>
         <p>${playlist.tracks.length} 首歌曲</p>
         <p>${playlist.tracks.length} 首歌曲</p>
       </div>
       </div>
-      <div class="track-actions">
-        <button class="ghost-btn">播放</button>
-        <button class="ghost-btn">覆盖</button>
-        <button class="ghost-btn">删除</button>
+      <div class="track-actions compact-actions">
+        <button class="icon-btn small-icon-btn play-btn" aria-label="播放"></button>
+        <button class="icon-btn small-icon-btn queue-btn" aria-label="覆盖"></button>
+        <button class="icon-btn small-icon-btn move-btn" aria-label="删除"></button>
       </div>
       </div>
     `;
     `;
+
     const [playBtn, replaceBtn, deleteBtn] = item.querySelectorAll("button");
     const [playBtn, replaceBtn, deleteBtn] = item.querySelectorAll("button");
     playBtn.onclick = () => {
     playBtn.onclick = () => {
+      showIconToast("播放列表");
       const tracks = playlist.tracks
       const tracks = playlist.tracks
         .map((path) => state.library.all_tracks.find((track) => track.path === path))
         .map((path) => state.library.all_tracks.find((track) => track.path === path))
         .filter(Boolean);
         .filter(Boolean);
       startQueue(tracks, 0);
       startQueue(tracks, 0);
     };
     };
     replaceBtn.onclick = async () => {
     replaceBtn.onclick = async () => {
+      showIconToast("覆盖列表");
       await fetch(`/api/playlists/${playlist.id}`, {
       await fetch(`/api/playlists/${playlist.id}`, {
         method: "PUT",
         method: "PUT",
         headers: { "Content-Type": "application/json" },
         headers: { "Content-Type": "application/json" },
@@ -173,6 +449,7 @@ function renderPlaylists() {
       await fetchLibrary();
       await fetchLibrary();
     };
     };
     deleteBtn.onclick = async () => {
     deleteBtn.onclick = async () => {
+      showIconToast("删除列表");
       await fetch(`/api/playlists/${playlist.id}`, { method: "DELETE" });
       await fetch(`/api/playlists/${playlist.id}`, { method: "DELETE" });
       await fetchLibrary();
       await fetchLibrary();
     };
     };
@@ -180,11 +457,50 @@ function renderPlaylists() {
   });
   });
 }
 }
 
 
+function updateToggleStates() {
+  document.getElementById("shuffleBtn").classList.toggle("active", state.shuffle);
+  document.getElementById("loopBtn").classList.toggle("active", state.loopMode !== "off");
+  document.getElementById("loopBtn").title = `循环: ${state.loopMode === "list" ? "列表" : state.loopMode === "one" ? "单曲" : "关"}`;
+  document.getElementById("shuffleBtn").title = `随机: ${state.shuffle ? "开" : "关"}`;
+  document.getElementById("loopBtn").dataset.tip = state.loopMode === "list" ? "列表循环" : state.loopMode === "one" ? "单曲循环" : "循环关闭";
+  document.getElementById("shuffleBtn").dataset.tip = state.shuffle ? "随机播放: 开" : "随机播放: 关";
+}
+
 function startQueue(tracks, index) {
 function startQueue(tracks, index) {
+  if (!tracks.length) return;
   state.currentQueue = [...tracks];
   state.currentQueue = [...tracks];
   state.currentTrackIndex = index;
   state.currentTrackIndex = index;
   renderQueue();
   renderQueue();
   playTrack(index);
   playTrack(index);
+  savePlaybackState();
+}
+
+function updateDock(track) {
+  document.getElementById("nowTitle").textContent = track.name;
+  document.getElementById("nowMeta").textContent = track.path;
+  document.getElementById("dockTitle").textContent = track.name;
+  document.getElementById("dockPath").textContent = track.path;
+
+  const coverImg = document.getElementById("dockCoverImage");
+  const fallback = document.getElementById("dockCoverFallback");
+  const album = state.albums.find((item) => item.path === track.folder) || null;
+  if (album && album.cover) {
+    coverImg.src = album.cover;
+    coverImg.classList.remove("is-hidden");
+    fallback.classList.add("is-hidden");
+  } else {
+    coverImg.removeAttribute("src");
+    coverImg.classList.add("is-hidden");
+    fallback.classList.remove("is-hidden");
+    fallback.textContent = (track.name || "乐").charAt(0).toUpperCase();
+  }
+}
+
+function syncPlayButton() {
+  document.getElementById("playToggleBtn").classList.toggle("is-playing", !audio.paused && !audio.ended);
+  document.querySelector(".icon-play").classList.toggle("is-hidden", !audio.paused && !audio.ended);
+  document.querySelector(".icon-pause").classList.toggle("is-hidden", audio.paused || audio.ended);
+  document.getElementById("playToggleBtn").dataset.tip = audio.paused || audio.ended ? "播放" : "暂停";
 }
 }
 
 
 function playTrack(index) {
 function playTrack(index) {
@@ -193,58 +509,110 @@ function playTrack(index) {
   const track = state.currentQueue[index];
   const track = state.currentQueue[index];
   audio.src = track.url;
   audio.src = track.url;
   audio.play();
   audio.play();
-  document.getElementById("nowTitle").textContent = track.name;
-  document.getElementById("nowMeta").textContent = track.path;
+  updateDock(track);
   renderQueue();
   renderQueue();
+  savePlaybackState();
 }
 }
 
 
-function nextTrack() {
+function playPrevious() {
   if (!state.currentQueue.length) return;
   if (!state.currentQueue.length) return;
-  if (state.loopMode === "one") {
-    playTrack(state.currentTrackIndex);
-    return;
-  }
+  const prevIndex = state.currentTrackIndex > 0 ? state.currentTrackIndex - 1 : state.currentQueue.length - 1;
+  playTrack(prevIndex);
+}
 
 
+function playNext() {
+  if (!state.currentQueue.length) return;
   if (state.shuffle) {
   if (state.shuffle) {
-    const randomIndex = Math.floor(Math.random() * state.currentQueue.length);
-    playTrack(randomIndex);
+    playTrack(Math.floor(Math.random() * state.currentQueue.length));
     return;
     return;
   }
   }
+  const nextIndex = state.currentTrackIndex + 1 < state.currentQueue.length ? state.currentTrackIndex + 1 : 0;
+  playTrack(nextIndex);
+}
 
 
-  const nextIndex = state.currentTrackIndex + 1;
-  if (nextIndex < state.currentQueue.length) {
-    playTrack(nextIndex);
+function nextTrack() {
+  if (!state.currentQueue.length) return;
+  if (state.loopMode === "one") {
+    playTrack(state.currentTrackIndex);
     return;
     return;
   }
   }
-
-  if (state.loopMode === "list") {
-    playTrack(0);
+  if (state.loopMode === "off" && state.currentTrackIndex === state.currentQueue.length - 1) {
+    audio.pause();
+    syncPlayButton();
+    return;
   }
   }
+  playNext();
+}
+
+function formatTime(seconds) {
+  if (!Number.isFinite(seconds)) return "00:00";
+  const minutes = Math.floor(seconds / 60);
+  const secs = Math.floor(seconds % 60);
+  return `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
 }
 }
 
 
 audio.addEventListener("ended", nextTrack);
 audio.addEventListener("ended", nextTrack);
+audio.addEventListener("play", syncPlayButton);
+audio.addEventListener("pause", syncPlayButton);
+audio.addEventListener("timeupdate", () => {
+  const progress = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
+  progressBar.value = progress;
+  document.getElementById("currentTimeLabel").textContent = formatTime(audio.currentTime);
+  document.getElementById("durationLabel").textContent = formatTime(audio.duration);
+  savePlaybackState();
+});
+audio.addEventListener("loadedmetadata", () => {
+  document.getElementById("durationLabel").textContent = formatTime(audio.duration);
+});
 
 
-document.getElementById("playAllBtn").onclick = () => startQueue(state.library.all_tracks, 0);
-document.getElementById("playFolderBtn").onclick = () => {
-  const tracks = folderTracks(state.currentFolder || "") || [];
-  if (!tracks.length) return alert("当前目录没有歌曲");
-  startQueue(tracks, 0);
-};
+progressBar.addEventListener("input", () => {
+  if (!audio.duration) return;
+  audio.currentTime = (Number(progressBar.value) / 100) * audio.duration;
+});
 
 
+document.getElementById("backHomeBtn").onclick = () => showView("home");
+document.getElementById("saveQueueBtn").onclick = () => renderQueue();
 document.getElementById("shuffleBtn").onclick = () => {
 document.getElementById("shuffleBtn").onclick = () => {
   state.shuffle = !state.shuffle;
   state.shuffle = !state.shuffle;
-  toggleButton("shuffleBtn", state.shuffle, `随机: ${state.shuffle ? "开" : "关"}`);
+  updateToggleStates();
+  showIconToast(document.getElementById("shuffleBtn").dataset.tip);
+  savePlaybackState();
 };
 };
-
 document.getElementById("loopBtn").onclick = () => {
 document.getElementById("loopBtn").onclick = () => {
   state.loopMode = state.loopMode === "list" ? "one" : state.loopMode === "one" ? "off" : "list";
   state.loopMode = state.loopMode === "list" ? "one" : state.loopMode === "one" ? "off" : "list";
-  const labels = { list: "循环: 列表", one: "循环: 单曲", off: "循环: 关" };
-  toggleButton("loopBtn", state.loopMode !== "off", labels[state.loopMode]);
+  updateToggleStates();
+  showIconToast(document.getElementById("loopBtn").dataset.tip);
+  savePlaybackState();
+};
+document.getElementById("prevBtn").onclick = () => {
+  showIconToast("上一曲");
+  playPrevious();
+};
+document.getElementById("nextBtn").onclick = () => {
+  showIconToast("下一曲");
+  playNext();
+};
+document.getElementById("playToggleBtn").onclick = () => {
+  if (!state.currentQueue.length && state.library?.all_tracks?.length) {
+    startQueue(state.library.all_tracks, 0);
+    showIconToast("开始播放");
+    return;
+  }
+  if (audio.paused) {
+    audio.play();
+    showIconToast("播放");
+  } else {
+    audio.pause();
+    showIconToast("暂停");
+  }
 };
 };
 
 
 document.getElementById("uploadBtn").onclick = async () => {
 document.getElementById("uploadBtn").onclick = async () => {
   const files = document.getElementById("uploadInput").files;
   const files = document.getElementById("uploadInput").files;
-  if (!files.length) return alert("请选择文件");
+  if (!files.length) {
+    alert("请选择文件");
+    return;
+  }
   const formData = new FormData();
   const formData = new FormData();
   Array.from(files).forEach((file) => formData.append("files", file));
   Array.from(files).forEach((file) => formData.append("files", file));
   formData.append("target_dir", document.getElementById("targetDir").value.trim());
   formData.append("target_dir", document.getElementById("targetDir").value.trim());
@@ -255,7 +623,10 @@ document.getElementById("uploadBtn").onclick = async () => {
 
 
 document.getElementById("createFolderBtn").onclick = async () => {
 document.getElementById("createFolderBtn").onclick = async () => {
   const path = document.getElementById("newFolderInput").value.trim();
   const path = document.getElementById("newFolderInput").value.trim();
-  if (!path) return alert("请输入目录名");
+  if (!path) {
+    alert("请输入目录名");
+    return;
+  }
   await fetch("/api/folder", {
   await fetch("/api/folder", {
     method: "POST",
     method: "POST",
     headers: { "Content-Type": "application/json" },
     headers: { "Content-Type": "application/json" },
@@ -267,7 +638,10 @@ document.getElementById("createFolderBtn").onclick = async () => {
 
 
 document.getElementById("createPlaylistBtn").onclick = async () => {
 document.getElementById("createPlaylistBtn").onclick = async () => {
   const name = document.getElementById("playlistNameInput").value.trim();
   const name = document.getElementById("playlistNameInput").value.trim();
-  if (!name) return alert("请输入播放列表名称");
+  if (!name) {
+    alert("请输入播放列表名称");
+    return;
+  }
   await fetch("/api/playlists", {
   await fetch("/api/playlists", {
     method: "POST",
     method: "POST",
     headers: { "Content-Type": "application/json" },
     headers: { "Content-Type": "application/json" },
@@ -277,6 +651,5 @@ document.getElementById("createPlaylistBtn").onclick = async () => {
   await fetchLibrary();
   await fetchLibrary();
 };
 };
 
 
-document.getElementById("saveQueueBtn").onclick = () => renderQueue();
-
+updateToggleStates();
 fetchLibrary();
 fetchLibrary();

+ 173 - 68
templates/index.html

@@ -7,97 +7,202 @@
     <link rel="stylesheet" href="/static/css/style.css" />
     <link rel="stylesheet" href="/static/css/style.css" />
   </head>
   </head>
   <body>
   <body>
-    <div class="page-shell">
-      <aside class="sidebar glass">
-        <div class="brand">
-          <p class="eyebrow">FastAPI MusicWeb</p>
+    <div class="app-shell">
+      <header class="app-header">
+        <div class="header-copy">
+          <p class="eyebrow">MusicWeb Player</p>
           <h1>在线音乐播放器</h1>
           <h1>在线音乐播放器</h1>
-          <p class="subtext">支持目录管理、随机播放、循环模式与自定义播放列表。</p>
+          <p class="subtext">移动端专辑浏览与播放</p>
         </div>
         </div>
+        <div class="header-pill">当前目录:<span id="folderIndicator">根目录</span></div>
+      </header>
 
 
-        <section class="panel">
-          <div class="panel-head">
-            <h2>文件操作</h2>
-          </div>
-          <label class="field">
-            <span>目标目录</span>
-            <input id="targetDir" type="text" placeholder="如:流行/周杰伦" />
-          </label>
-          <label class="field">
-            <span>上传音乐</span>
-            <input id="uploadInput" type="file" multiple />
-          </label>
-          <button id="uploadBtn" class="primary-btn">上传文件</button>
-          <label class="field">
-            <span>新建文件夹</span>
-            <input id="newFolderInput" type="text" placeholder="如:轻音乐/夜晚" />
-          </label>
-          <button id="createFolderBtn" class="secondary-btn">创建文件夹</button>
+      <main class="page-content">
+        <section id="homeView" class="view-stack">
+          <section class="content-section slim-hero">
+            <div>
+              <h2 id="nowTitle">选择一个专辑目录开始播放</h2>
+              <p id="nowMeta" class="subtext">点击专辑进入详情页,或直接在卡片上快速播放。</p>
+            </div>
+            <div class="hero-metrics">
+              <div class="metric-card">
+                <strong id="albumCount">0</strong>
+                <span>专辑目录</span>
+              </div>
+              <div class="metric-card">
+                <strong id="trackCount">0</strong>
+                <span>歌曲总数</span>
+              </div>
+            </div>
+          </section>
+
+          <section class="content-section">
+            <div class="section-head">
+              <h2>精选专辑</h2>
+              <span>通栏展示</span>
+            </div>
+            <div id="featuredAlbums" class="featured-list"></div>
+          </section>
+
+          <section class="content-section">
+            <div class="section-head">
+              <h2>全部专辑目录</h2>
+              <span>两栏浏览</span>
+            </div>
+            <div id="albumGrid" class="album-grid"></div>
+          </section>
+
+          <section class="stack-section">
+            <section class="content-section">
+              <div class="section-head">
+                <h2>最新更新</h2>
+                <span>紧凑列表</span>
+              </div>
+              <div id="latestList" class="song-list compact-list"></div>
+            </section>
+
+            <section class="content-section">
+              <div class="section-head">
+                <h2>每日排名</h2>
+                <span>目录热度</span>
+              </div>
+              <div id="rankingList" class="ranking-list"></div>
+            </section>
+          </section>
         </section>
         </section>
 
 
-        <section class="panel">
-          <div class="panel-head">
-            <h2>播放列表</h2>
-          </div>
-          <label class="field">
-            <span>新列表名称</span>
-            <input id="playlistNameInput" type="text" placeholder="我的收藏" />
-          </label>
-          <button id="createPlaylistBtn" class="secondary-btn">用当前队列创建</button>
-          <div id="playlistList" class="playlist-list"></div>
+        <section id="detailView" class="view-stack is-hidden">
+          <section class="content-section detail-header">
+            <button id="backHomeBtn" class="back-btn">返回首页</button>
+            <div class="detail-cover-wrap">
+              <img id="detailCoverImage" class="detail-cover-image is-hidden" alt="cover" />
+              <div id="detailCoverFallback" class="detail-cover-fallback">专辑</div>
+            </div>
+            <div class="detail-copy">
+              <p class="eyebrow">Album Detail</p>
+              <h2 id="detailTitle">专辑详情</h2>
+              <p id="detailMeta" class="subtext"></p>
+              <div class="card-actions">
+                <button id="detailPlayBtn" class="primary-btn">播放当前目录</button>
+                <button id="detailPlayAllBtn" class="secondary-btn">加入并播放</button>
+              </div>
+            </div>
+          </section>
+
+          <section class="content-section">
+            <div class="section-head">
+              <h2>歌曲列表</h2>
+              <span id="detailTrackCount">0 首</span>
+            </div>
+            <div id="detailTrackList" class="song-list"></div>
+          </section>
         </section>
         </section>
-      </aside>
-
-      <main class="content">
-        <section class="hero glass">
-          <div>
-            <p class="eyebrow">Now Playing</p>
-            <h2 id="nowTitle">请选择一首歌</h2>
-            <p id="nowMeta" class="subtext">从目录、全部歌曲或播放列表开始。</p>
-          </div>
-          <div class="hero-actions">
-            <button id="playAllBtn" class="secondary-btn">播放全部</button>
-            <button id="playFolderBtn" class="secondary-btn">播放当前目录</button>
-            <button id="shuffleBtn" class="secondary-btn">随机: 关</button>
-            <button id="loopBtn" class="secondary-btn">循环: 列表</button>
+
+        <section class="content-section">
+          <div class="section-head">
+            <h2>当前播放队列</h2>
+            <button id="saveQueueBtn" class="text-btn">刷新</button>
           </div>
           </div>
-          <audio id="audioPlayer" controls preload="metadata"></audio>
+          <div id="queueList" class="queue-list"></div>
         </section>
         </section>
 
 
-        <section class="library-grid">
-          <div class="library-panel glass">
-            <div class="panel-head">
-              <h2>音乐目录</h2>
-              <span id="folderIndicator">当前目录:根目录</span>
+        <section class="stack-section">
+          <section class="content-section">
+            <div class="section-head">
+              <h2>目录与上传</h2>
             </div>
             </div>
-            <div id="folderTree" class="folder-tree"></div>
-          </div>
+            <label class="field">
+              <span>目标目录</span>
+              <input id="targetDir" type="text" placeholder="如:流行/周杰伦" />
+            </label>
+            <label class="field">
+              <span>上传音乐</span>
+              <input id="uploadInput" type="file" multiple />
+            </label>
+            <button id="uploadBtn" class="primary-btn">上传文件</button>
+            <label class="field">
+              <span>新建文件夹</span>
+              <input id="newFolderInput" type="text" placeholder="如:轻音乐/夜晚" />
+            </label>
+            <button id="createFolderBtn" class="secondary-btn">创建文件夹</button>
+          </section>
 
 
-          <div class="queue-panel glass">
-            <div class="panel-head">
-              <h2>播放队列</h2>
-              <button id="saveQueueBtn" class="ghost-btn">刷新队列</button>
+          <section class="content-section">
+            <div class="section-head">
+              <h2>播放列表</h2>
             </div>
             </div>
-            <div id="queueList" class="queue-list"></div>
-          </div>
+            <label class="field">
+              <span>新列表名称</span>
+              <input id="playlistNameInput" type="text" placeholder="我的收藏" />
+            </label>
+            <button id="createPlaylistBtn" class="secondary-btn">用当前队列创建</button>
+            <div id="playlistList" class="playlist-list"></div>
+          </section>
         </section>
         </section>
       </main>
       </main>
+
+      <div class="player-dock">
+        <div class="player-top">
+          <div class="dock-cover-wrap">
+            <img id="dockCoverImage" class="dock-cover-image is-hidden" alt="cover" />
+            <div class="dock-cover-fallback" id="dockCoverFallback">乐</div>
+          </div>
+          <div class="dock-copy">
+            <strong id="dockTitle">未开始播放</strong>
+            <p id="dockPath">点击专辑或歌曲开始播放</p>
+          </div>
+        </div>
+
+        <div class="player-progress">
+          <span id="currentTimeLabel">00:00</span>
+          <input id="progressBar" type="range" min="0" max="100" value="0" />
+          <span id="durationLabel">00:00</span>
+        </div>
+
+        <div class="player-actions icon-actions">
+          <button id="shuffleBtn" class="icon-btn" aria-label="随机" data-tip="随机播放">
+            <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 4h4v4M20 4l-5 5M4 7h5l6 6M4 17h5l2-2M15 15l5 5M20 20v-4"/></svg>
+          </button>
+          <button id="prevBtn" class="icon-btn" aria-label="上一曲" data-tip="上一曲">
+            <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5v14M18 6l-8 6 8 6V6z"/></svg>
+          </button>
+          <button id="playToggleBtn" class="icon-btn play-toggle" aria-label="播放或暂停" data-tip="播放">
+            <svg class="icon-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7-11-7z"/></svg>
+            <svg class="icon-pause is-hidden" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5h3v14H8zM13 5h3v14h-3z"/></svg>
+          </button>
+          <button id="nextBtn" class="icon-btn" aria-label="下一曲" data-tip="下一曲">
+            <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 5v14M6 6l8 6-8 6V6z"/></svg>
+          </button>
+          <button id="loopBtn" class="icon-btn" aria-label="循环" data-tip="列表循环">
+            <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 2l4 4-4 4M3 11V9a3 3 0 0 1 3-3h15M7 22l-4-4 4-4M21 13v2a3 3 0 0 1-3 3H3"/></svg>
+          </button>
+        </div>
+
+        <audio id="audioPlayer" preload="metadata"></audio>
+      </div>
+      <div id="iconToast" class="icon-toast is-hidden"></div>
     </div>
     </div>
 
 
     <template id="trackItemTemplate">
     <template id="trackItemTemplate">
-      <div class="track-item">
-        <div>
+      <div class="song-row">
+        <div class="song-copy">
           <strong class="track-name"></strong>
           <strong class="track-name"></strong>
           <p class="track-path"></p>
           <p class="track-path"></p>
         </div>
         </div>
-        <div class="track-actions">
-          <button class="play-btn ghost-btn">播放</button>
-          <button class="queue-btn ghost-btn">加入队列</button>
-          <button class="move-btn ghost-btn">移动</button>
+        <div class="track-actions compact-actions">
+          <button class="play-btn icon-btn small-icon-btn" aria-label="播放" data-tip="播放">
+            <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7-11-7z"/></svg>
+          </button>
+          <button class="queue-btn icon-btn small-icon-btn" aria-label="加入队列" data-tip="加入队列">
+            <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M4 12h10M4 17h7M18 9v8M14 13h8"/></svg>
+          </button>
+          <button class="move-btn icon-btn small-icon-btn" aria-label="移动" data-tip="移动歌曲">
+            <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12h10M11 5l7 7-7 7"/></svg>
+          </button>
         </div>
         </div>
       </div>
       </div>
     </template>
     </template>
 
 
     <script src="/static/js/app.js"></script>
     <script src="/static/js/app.js"></script>
   </body>
   </body>
-  </html>
+</html>

Some files were not shown because too many files changed in this diff