本页出处
设计风格资料库
内容来自 luckymaomi 的 UI 风格库(复刻版),以 MIT 许可 提供。本站镜像收录,仅作学习参考;这一页的原始地址是 原站页面。
← 回到导览访问原站仓库
返回模板目录
mediadark-mode

音乐播放器

暗色主题音乐播放器,包含播放队列、播放控制栏、音量调节和播放列表。

独立打开

完整模板源代码

查看 530 行代码
"use client";

export const dynamic = "force-static";

import { useState } from "react";
import {
  Heart,
  Home,
  Library,
  ListMusic,
  Menu,
  Mic2,
  MoreHorizontal,
  Pause,
  Play,
  Plus,
  Radio,
  Repeat,
  Search,
  Shuffle,
  SkipBack,
  SkipForward,
  Volume2,
  X,
} from "@/components/IconShim";
import { TemplateBackLink } from "@/components/templates/TemplateBackLink";
interface Track {
  id: number;
  title: string;
  artist: string;
  album: string;
  duration: string;
  liked: boolean;
  color: string;
}

const playlist: Track[] = [
  {
    id: 1,
    title: "Neon Meridian",
    artist: "Velvet Arcade",
    album: "Night Circuit",
    duration: "4:03",
    liked: true,
    color: "from-indigo-600 to-purple-700",
  },
  {
    id: 2,
    title: "Electric Bloom",
    artist: "Glass Fauna",
    album: "Chlorophyll",
    duration: "3:49",
    liked: false,
    color: "from-green-500 to-teal-600",
  },
  {
    id: 3,
    title: "First Light",
    artist: "Monochrome",
    album: "Grey Rooms",
    duration: "2:07",
    liked: true,
    color: "from-gray-700 to-gray-900",
  },
  {
    id: 4,
    title: "Let It Unfold",
    artist: "Paper Tides",
    album: "Currents of June",
    duration: "7:46",
    liked: false,
    color: "from-pink-500 to-rose-600",
  },
  {
    id: 5,
    title: "Retrograde Motion",
    artist: "Cobalt Sons",
    album: "Overgrowth",
    duration: "3:42",
    liked: false,
    color: "from-blue-600 to-cyan-700",
  },
  {
    id: 6,
    title: "Crystalline",
    artist: "Monochrome",
    album: "Grey Rooms",
    duration: "3:22",
    liked: true,
    color: "from-gray-600 to-gray-800",
  },
  {
    id: 7,
    title: "Analog Love",
    artist: "Circuit Royale",
    album: "Rediscovery",
    duration: "4:58",
    liked: false,
    color: "from-yellow-500 to-orange-600",
  },
  {
    id: 8,
    title: "Eventually, Everything",
    artist: "Paper Tides",
    album: "Currents of June",
    duration: "5:18",
    liked: false,
    color: "from-pink-400 to-rose-500",
  },
  {
    id: 9,
    title: "Something About Midnight",
    artist: "Circuit Royale",
    album: "Rediscovery",
    duration: "3:52",
    liked: true,
    color: "from-amber-500 to-orange-600",
  },
  {
    id: 10,
    title: "Youth in Amber",
    artist: "Low Meadow",
    album: "If Fields Could Speak",
    duration: "4:24",
    liked: false,
    color: "from-slate-600 to-slate-800",
  },
];

/** Generative album art: motif keyed by track id, tinted by the track gradient. */
function AlbumArt({
  id,
  color,
  className = "",
}: {
  id: number;
  color: string;
  className?: string;
}) {
  const motif = id % 5;
  return (
    <div
      aria-hidden="true"
      className={`relative overflow-hidden bg-gradient-to-br ${color} ${className}`}
    >
      {motif === 0 && (
        <div className="absolute inset-0 flex items-center justify-center">
          <span className="absolute h-3/4 w-3/4 rounded-full border-2 border-white/25" />
          <span className="absolute h-1/2 w-1/2 rounded-full border border-white/35" />
          <span className="absolute h-[12%] w-[12%] rounded-full bg-white/80" />
        </div>
      )}
      {motif === 1 && (
        <div className="absolute inset-x-[16%] inset-y-[26%] flex items-center gap-[6%]">
          {[42, 72, 55, 92, 64, 80, 46].map((height, i) => (
            <span
              key={i}
              className="flex-1 rounded-full bg-white/60"
              style={{ height: `${height}%` }}
            />
          ))}
        </div>
      )}
      {motif === 2 && (
        <>
          <span className="absolute left-1/2 top-[24%] h-[46%] w-[46%] -translate-x-1/2 rounded-full bg-white/70" />
          <span className="absolute inset-x-0 bottom-0 h-[34%] bg-black/30" />
        </>
      )}
      {motif === 3 && (
        <>
          <span className="absolute left-1/2 top-1/2 h-1/2 w-1/2 -translate-x-1/2 -translate-y-1/2 rotate-45 border-2 border-white/50" />
          <span className="absolute left-1/2 top-1/2 h-1/4 w-1/4 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-white/30" />
        </>
      )}
      {motif === 4 && (
        <>
          <span className="absolute -right-1/4 -top-1/4 h-3/4 w-3/4 rounded-full border-4 border-white/25" />
          <span className="absolute -bottom-1/4 -left-1/4 h-3/4 w-3/4 rounded-full border-4 border-white/25" />
          <span className="absolute left-[45%] top-[45%] h-[10%] w-[10%] rounded-full bg-white/70" />
        </>
      )}
    </div>
  );
}

const recentPlaylists = [
  { name: "Chill Vibes", tracks: 42, color: "from-violet-500 to-purple-600" },
  { name: "Focus Mode", tracks: 28, color: "from-blue-500 to-cyan-600" },
  { name: "Late Night", tracks: 35, color: "from-indigo-600 to-violet-700" },
  { name: "Workout Mix", tracks: 55, color: "from-red-500 to-orange-600" },
];

export default function MusicPlayerTemplate() {
  const [currentTrack, setCurrentTrack] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);
  const [tracks, setTracks] = useState<Track[]>(playlist);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [progress, setProgress] = useState(35);

  const track = tracks[currentTrack];

  const toggleLike = (id: number) => {
    setTracks((prev) =>
      prev.map((t) => (t.id === id ? { ...t, liked: !t.liked } : t)),
    );
  };

  const playTrack = (index: number) => {
    setCurrentTrack(index);
    setIsPlaying(true);
    setProgress(0);
  };

  return (
    <div className="h-screen flex flex-col bg-gray-950 text-white overflow-hidden">
      <div className="flex-1 flex overflow-hidden">
        {/* Mobile Sidebar */}
        {sidebarOpen && (
          <div className="fixed inset-0 z-50 lg:hidden">
            <div
              className="absolute inset-0 bg-black/60"
              role="presentation"
              onClick={() => setSidebarOpen(false)}
            />
            <aside className="absolute left-0 top-0 bottom-0 w-72 bg-gray-900 flex flex-col overflow-y-auto">
              <div className="flex items-center justify-between p-4">
                <span className="font-bold text-lg">Library</span>
                <button
                  onClick={() => setSidebarOpen(false)}
                  aria-label="Close"
                >
                  <X className="w-5 h-5 text-gray-400" />
                </button>
              </div>
              <SidebarContent recentPlaylists={recentPlaylists} />
            </aside>
          </div>
        )}

        {/* Desktop Sidebar */}
        <aside className="hidden lg:flex w-60 xl:w-72 shrink-0 bg-gray-900 flex-col sticky top-0 h-full border border-gray-800">
          <div className="p-5">
            <span className="text-xl font-bold bg-gradient-to-r from-green-400 to-emerald-500 bg-clip-text text-transparent">
              MusicKit
            </span>
          </div>
          <SidebarContent recentPlaylists={recentPlaylists} />
        </aside>

        {/* Main Content */}
        <main className="flex-1 min-w-0 overflow-y-auto">
          {/* Header */}
          <div className="sticky top-0 z-20 bg-gray-950/80 backdrop-blur-xl px-4 md:px-6 py-3 flex items-center gap-3">
            <button
              className="lg:hidden p-2 hover:bg-gray-800 rounded-lg"
              onClick={() => setSidebarOpen(true)}
              aria-label="Open sidebar"
            >
              <Menu className="w-5 h-5" />
            </button>
            <div className="relative flex-1 max-w-md">
              <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
              <input
                type="text"
                placeholder="Search songs, artists, albums..."
                className="w-full pl-9 pr-4 py-2 bg-gray-800 rounded-full text-sm text-white outline-none focus:ring-2 focus:ring-green-500/50 placeholder:text-gray-500 transition-all"
              />
            </div>
          </div>

          {/* Now Playing Hero */}
          <div
            className={`mx-4 md:mx-6 mt-2 p-6 md:p-8 rounded-2xl bg-gradient-to-br ${track.color} relative overflow-hidden`}
          >
            <div className="absolute inset-0 bg-black/20" />
            <div className="relative z-10 flex items-center gap-5 md:gap-7">
              <AlbumArt
                id={track.id}
                color={track.color}
                className="h-20 w-20 shrink-0 rounded-xl ring-1 ring-white/25 md:h-28 md:w-28"
              />
              <div className="min-w-0">
                <p className="text-xs uppercase tracking-widest text-white/60 mb-2">
                  Now Playing
                </p>
                <h1 className="text-2xl md:text-3xl font-bold mb-1 truncate">
                  {track.title}
                </h1>
                <p className="text-white/70 truncate">
                  {track.artist} &middot; {track.album}
                </p>
              </div>
            </div>
          </div>

          {/* Playlist */}
          <div className="px-4 md:px-6 mt-6 pb-32">
            <div className="flex items-center justify-between mb-4">
              <h2 className="text-lg font-bold">Queue</h2>
              <button className="text-sm text-gray-400 hover:text-white transition-colors">
                See All
              </button>
            </div>
            <div className="space-y-1">
              {tracks.map((t, i) => (
                <button
                  key={t.id}
                  onClick={() => playTrack(i)}
                  className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors text-left group ${
                    i === currentTrack
                      ? "bg-gray-800/80 text-green-400"
                      : "hover:bg-gray-800/50 text-gray-300"
                  }`}
                >
                  <span className="w-6 text-center text-xs text-gray-500 group-hover:hidden">
                    {i + 1}
                  </span>
                  <span className="w-6 text-center hidden group-hover:block">
                    {i === currentTrack && isPlaying ? (
                      <Pause className="w-3.5 h-3.5 mx-auto" />
                    ) : (
                      <Play className="w-3.5 h-3.5 mx-auto" />
                    )}
                  </span>
                  <AlbumArt
                    id={t.id}
                    color={t.color}
                    className="w-10 h-10 rounded shrink-0"
                  />
                  <div className="flex-1 min-w-0">
                    <p
                      className={`text-sm font-medium truncate ${i === currentTrack ? "text-green-400" : "text-white"}`}
                    >
                      {t.title}
                    </p>
                    <p className="text-xs text-gray-500 truncate">{t.artist}</p>
                  </div>
                  <button
                    onClick={(e) => {
                      e.stopPropagation();
                      toggleLike(t.id);
                    }}
                    className="p-1 shrink-0"
                  >
                    <Heart
                      className={`w-4 h-4 ${t.liked ? "text-green-400 fill-green-400" : "text-gray-600 hover:text-gray-400"}`}
                    />
                  </button>
                  <span className="text-xs text-gray-500 shrink-0 w-10 text-right">
                    {t.duration}
                  </span>
                  <button className="p-1 text-gray-600 hover:text-gray-400 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
                    <MoreHorizontal className="w-4 h-4" />
                  </button>
                </button>
              ))}
            </div>
          </div>
        </main>
      </div>

      {/* Player Bar */}
      <div className="bg-gray-900 border border-gray-800 px-4 py-3 flex items-center gap-4 z-30">
        {/* Track Info */}
        <div className="flex items-center gap-3 w-48 lg:w-64 shrink-0">
          <AlbumArt
            id={track.id}
            color={track.color}
            className="w-12 h-12 rounded-lg shrink-0"
          />
          <div className="min-w-0 hidden sm:block">
            <p className="text-sm font-medium text-white truncate">
              {track.title}
            </p>
            <p className="text-xs text-gray-400 truncate">{track.artist}</p>
          </div>
          <button
            onClick={() => toggleLike(track.id)}
            className="hidden sm:block shrink-0"
          >
            <Heart
              className={`w-4 h-4 ${track.liked ? "text-green-400 fill-green-400" : "text-gray-500"}`}
            />
          </button>
        </div>

        {/* Controls */}
        <div className="flex-1 flex flex-col items-center max-w-xl mx-auto">
          <div className="flex items-center gap-4 mb-2">
            <button className="text-gray-400 hover:text-white transition-colors hidden sm:block">
              <Shuffle className="w-4 h-4" />
            </button>
            <button
              onClick={() => {
                setCurrentTrack((prev) =>
                  prev === 0 ? tracks.length - 1 : prev - 1,
                );
                setProgress(0);
              }}
              className="text-gray-300 hover:text-white transition-colors"
            >
              <SkipBack className="w-5 h-5" />
            </button>
            <button
              onClick={() => setIsPlaying(!isPlaying)}
              className="w-9 h-9 bg-white rounded-full flex items-center justify-center hover:scale-105 transition-transform"
            >
              {isPlaying ? (
                <Pause className="w-4 h-4 text-gray-900" />
              ) : (
                <Play className="w-4 h-4 text-gray-900 ml-0.5" />
              )}
            </button>
            <button
              onClick={() => {
                setCurrentTrack((prev) =>
                  prev === tracks.length - 1 ? 0 : prev + 1,
                );
                setProgress(0);
              }}
              className="text-gray-300 hover:text-white transition-colors"
            >
              <SkipForward className="w-5 h-5" />
            </button>
            <button className="text-gray-400 hover:text-white transition-colors hidden sm:block">
              <Repeat className="w-4 h-4" />
            </button>
          </div>
          <div className="w-full flex items-center gap-2">
            <span className="text-[10px] text-gray-500 w-8 text-right">
              1:24
            </span>
            <div className="flex-1 h-1 bg-gray-700 rounded-full group cursor-pointer">
              <div
                className="h-full bg-white rounded-full relative group-hover:bg-green-400 transition-colors"
                style={{ width: `${progress}%` }}
              >
                <span className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 bg-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
              </div>
            </div>
            <span className="text-[10px] text-gray-500 w-8">
              {track.duration}
            </span>
          </div>
        </div>

        {/* Volume */}
        <div className="hidden lg:flex items-center gap-2 w-48 shrink-0 justify-end">
          <button className="text-gray-400 hover:text-white">
            <Mic2 className="w-4 h-4" />
          </button>
          <button className="text-gray-400 hover:text-white">
            <ListMusic className="w-4 h-4" />
          </button>
          <Volume2 className="w-4 h-4 text-gray-400" />
          <div className="w-24 h-1 bg-gray-700 rounded-full group cursor-pointer">
            <div
              className="h-full bg-gray-400 rounded-full group-hover:bg-green-400 transition-colors"
              style={{ width: "70%" }}
            />
          </div>
        </div>
      </div>
      <TemplateBackLink />
    </div>
  );
}

function SidebarContent({
  recentPlaylists,
}: {
  recentPlaylists: { name: string; tracks: number; color: string }[];
}) {
  return (
    <div className="flex-1 flex flex-col p-4 overflow-y-auto">
      <nav className="space-y-1 mb-6">
        {[
          { icon: Home, label: "Home", active: true },
          { icon: Search, label: "Search", active: false },
          { icon: Library, label: "Your Library", active: false },
          { icon: Radio, label: "Radio", active: false },
        ].map((item) => (
          <button
            key={item.label}
            className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors ${
              item.active
                ? "bg-gray-800 text-white font-medium"
                : "text-gray-400 hover:text-white hover:bg-gray-800/50"
            }`}
          >
            <item.icon className="w-5 h-5" />
            {item.label}
          </button>
        ))}
      </nav>

      <div className="mb-4 flex items-center justify-between">
        <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
          Playlists
        </h3>
        <button className="p-1 text-gray-500 hover:text-white rounded">
          <Plus className="w-4 h-4" />
        </button>
      </div>

      <div className="space-y-2">
        {recentPlaylists.map((pl) => (
          <button
            key={pl.name}
            className="w-full flex items-center gap-3 p-2 rounded-lg hover:bg-gray-800/50 transition-colors text-left"
          >
            <AlbumArt
              id={pl.name.length}
              color={pl.color}
              className="w-10 h-10 rounded shrink-0"
            />
            <div className="min-w-0">
              <p className="text-sm font-medium text-gray-200 truncate">
                {pl.name}
              </p>
              <p className="text-xs text-gray-500">{pl.tracks} tracks</p>
            </div>
          </button>
        ))}
      </div>
    </div>
  );
}

查看关联风格