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

文件管理器

云存储文件管理界面,支持网格/列表视图、文件搜索和存储空间展示。

独立打开

完整模板源代码

查看 488 行代码
"use client";

export const dynamic = "force-static";

import { useState } from "react";
import {
  ChevronRight,
  Cloud,
  Download,
  File,
  FileImage,
  FileText,
  Film,
  Folder,
  Grid3X3,
  HardDrive,
  LayoutList,
  Menu,
  MoreVertical,
  Music,
  Plus,
  Search,
  Share2,
  Star,
  Trash2,
  Upload,
  Users,
  X,
} from "@/components/IconShim";
import { TemplateBackLink } from "@/components/templates/TemplateBackLink";
type ViewMode = "grid" | "list";

interface FileItem {
  id: number;
  name: string;
  type: "folder" | "image" | "document" | "video" | "audio" | "file";
  size?: string;
  modified: string;
  shared?: boolean;
  starred: boolean;
}

const files: FileItem[] = [
  {
    id: 1,
    name: "Design Assets",
    type: "folder",
    modified: "Feb 15, 2026",
    starred: true,
  },
  {
    id: 2,
    name: "Project Documents",
    type: "folder",
    modified: "Feb 12, 2026",
    starred: false,
  },
  {
    id: 3,
    name: "Photos 2026",
    type: "folder",
    modified: "Feb 10, 2026",
    starred: false,
  },
  {
    id: 4,
    name: "hero-banner.png",
    type: "image",
    size: "2.4 MB",
    modified: "Feb 14, 2026",
    shared: true,
    starred: false,
  },
  {
    id: 5,
    name: "Q4-Report.pdf",
    type: "document",
    size: "1.8 MB",
    modified: "Feb 13, 2026",
    starred: true,
  },
  {
    id: 6,
    name: "product-demo.mp4",
    type: "video",
    size: "128 MB",
    modified: "Feb 11, 2026",
    shared: true,
    starred: false,
  },
  {
    id: 7,
    name: "podcast-ep12.mp3",
    type: "audio",
    size: "45 MB",
    modified: "Feb 9, 2026",
    starred: false,
  },
  {
    id: 8,
    name: "brand-guidelines.pdf",
    type: "document",
    size: "5.2 MB",
    modified: "Feb 8, 2026",
    starred: false,
  },
  {
    id: 9,
    name: "screenshot-app.png",
    type: "image",
    size: "890 KB",
    modified: "Feb 7, 2026",
    shared: true,
    starred: false,
  },
  {
    id: 10,
    name: "meeting-notes.docx",
    type: "document",
    size: "124 KB",
    modified: "Feb 6, 2026",
    starred: false,
  },
  {
    id: 11,
    name: "archive-2025.zip",
    type: "file",
    size: "2.1 GB",
    modified: "Jan 30, 2026",
    starred: false,
  },
];

const iconForType = (type: FileItem["type"]) => {
  switch (type) {
    case "folder":
      return Folder;
    case "image":
      return FileImage;
    case "document":
      return FileText;
    case "video":
      return Film;
    case "audio":
      return Music;
    default:
      return File;
  }
};

const colorForType = (type: FileItem["type"]) => {
  switch (type) {
    case "folder":
      return "text-blue-500 bg-blue-50";
    case "image":
      return "text-pink-500 bg-pink-50";
    case "document":
      return "text-orange-500 bg-orange-50";
    case "video":
      return "text-purple-500 bg-purple-50";
    case "audio":
      return "text-green-500 bg-green-50";
    default:
      return "text-gray-500 bg-gray-100";
  }
};

const storageUsed = 42.8;
const storageTotal = 100;

export default function FileManagerTemplate() {
  const [viewMode, setViewMode] = useState<ViewMode>("grid");
  const [searchQuery, setSearchQuery] = useState("");
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [fileList, setFileList] = useState<FileItem[]>(files);

  const toggleStar = (id: number) => {
    setFileList((prev) =>
      prev.map((f) => (f.id === id ? { ...f, starred: !f.starred } : f)),
    );
  };

  const filtered = fileList.filter((f) =>
    f.name.toLowerCase().includes(searchQuery.toLowerCase()),
  );

  return (
    <div className="min-h-screen bg-gray-50 flex">
      {/* Mobile Sidebar Overlay */}
      {sidebarOpen && (
        <div className="fixed inset-0 z-50 lg:hidden">
          <div
            className="absolute inset-0 bg-black/40"
            role="presentation"
            onClick={() => setSidebarOpen(false)}
          />
          <aside className="absolute left-0 top-0 bottom-0 w-72 bg-white shadow-xl flex flex-col">
            <div className="flex items-center justify-between p-4 border border-gray-100">
              <span className="font-bold text-lg">CloudDrive</span>
              <button
                onClick={() => setSidebarOpen(false)}
                aria-label="Close sidebar"
              >
                <X className="w-5 h-5 text-gray-400" />
              </button>
            </div>
            <SidebarContent />
          </aside>
        </div>
      )}

      {/* Desktop Sidebar */}
      <aside className="hidden lg:flex w-64 xl:w-72 shrink-0 bg-white border border-gray-200 flex-col sticky top-0 h-screen">
        <div className="p-5 border border-gray-100">
          <span className="text-xl font-bold text-gray-900">
            <Cloud className="inline w-5 h-5 text-blue-500 mr-2" />
            CloudDrive
          </span>
        </div>
        <SidebarContent />
      </aside>

      {/* Main Content */}
      <main className="flex-1 min-w-0 flex flex-col">
        {/* Header */}
        <header className="bg-white border border-gray-200 px-4 md:px-6 py-3 flex items-center gap-3 sticky top-0 z-30">
          <button
            className="lg:hidden p-2 hover:bg-gray-100 rounded-lg"
            onClick={() => setSidebarOpen(true)}
            aria-label="Open sidebar"
          >
            <Menu className="w-5 h-5" />
          </button>

          {/* Breadcrumb */}
          <div className="hidden sm:flex items-center gap-1 text-sm text-gray-500">
            <span className="hover:text-gray-700 cursor-pointer">My Drive</span>
            <ChevronRight className="w-3.5 h-3.5" />
            <span className="text-gray-900 font-medium">All Files</span>
          </div>

          <div className="flex-1" />

          {/* Search */}
          <div className="relative max-w-xs w-full">
            <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
            <input
              type="text"
              placeholder="Search files..."
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
              className="w-full pl-9 pr-4 py-2 text-sm bg-gray-100 border border-transparent rounded-lg focus:bg-white focus:border-blue-300 focus:outline-none transition-all"
            />
          </div>

          {/* View Toggle */}
          <div className="hidden sm:flex items-center bg-gray-100 rounded-lg p-0.5">
            <button
              onClick={() => setViewMode("grid")}
              className={`p-1.5 rounded-md transition-colors ${viewMode === "grid" ? "bg-white shadow-sm text-gray-900" : "text-gray-500 hover:text-gray-700"}`}
              aria-label="Grid view"
            >
              <Grid3X3 className="w-4 h-4" />
            </button>
            <button
              onClick={() => setViewMode("list")}
              className={`p-1.5 rounded-md transition-colors ${viewMode === "list" ? "bg-white shadow-sm text-gray-900" : "text-gray-500 hover:text-gray-700"}`}
              aria-label="List view"
            >
              <LayoutList className="w-4 h-4" />
            </button>
          </div>

          <button className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white text-sm font-medium rounded-lg hover:bg-blue-600 transition-colors">
            <Upload className="w-4 h-4" />
            <span className="hidden sm:inline">Upload</span>
          </button>
        </header>

        {/* Quick Actions */}
        <div className="px-4 md:px-6 py-4 flex gap-3 overflow-x-auto">
          {[
            {
              icon: Plus,
              label: "New Folder",
              color: "text-blue-600 bg-blue-50 border-blue-200",
            },
            {
              icon: Upload,
              label: "Upload File",
              color: "text-green-600 bg-green-50 border-green-200",
            },
            {
              icon: Share2,
              label: "Share",
              color: "text-purple-600 bg-purple-50 border-purple-200",
            },
          ].map((action) => (
            <button
              key={action.label}
              className={`flex items-center gap-2 px-4 py-2 text-sm border rounded-lg shrink-0 hover:shadow-sm transition-all ${action.color}`}
            >
              <action.icon className="w-4 h-4" />
              {action.label}
            </button>
          ))}
        </div>

        {/* File List */}
        <div className="flex-1 px-4 md:px-6 pb-8">
          {viewMode === "grid" ? (
            <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 gap-3">
              {filtered.map((file) => {
                const Icon = iconForType(file.type);
                const colors = colorForType(file.type);
                return (
                  <div
                    key={file.id}
                    className="group bg-white border border-gray-200 rounded-xl p-4 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer"
                  >
                    <div className="flex items-start justify-between mb-3">
                      <div
                        className={`w-10 h-10 rounded-lg ${colors} flex items-center justify-center`}
                      >
                        <Icon className="w-5 h-5" />
                      </div>
                      <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
                        <button
                          onClick={(e) => {
                            e.stopPropagation();
                            toggleStar(file.id);
                          }}
                          className="p-1 hover:bg-gray-100 rounded"
                        >
                          <Star
                            className={`w-3.5 h-3.5 ${file.starred ? "text-yellow-500 fill-yellow-500" : "text-gray-400"}`}
                          />
                        </button>
                        <button className="p-1 hover:bg-gray-100 rounded">
                          <MoreVertical className="w-3.5 h-3.5 text-gray-400" />
                        </button>
                      </div>
                    </div>
                    <p className="text-sm font-medium text-gray-900 truncate mb-1">
                      {file.name}
                    </p>
                    <p className="text-xs text-gray-400">{file.size || "--"}</p>
                    {file.shared && (
                      <div className="mt-2 flex items-center gap-1 text-xs text-blue-500">
                        <Users className="w-3 h-3" />
                        Shared
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          ) : (
            <div className="bg-white border border-gray-200 rounded-xl overflow-hidden">
              <table className="w-full">
                <thead>
                  <tr className="text-left text-xs text-gray-500 uppercase tracking-wider border border-gray-100">
                    <th className="px-4 py-3 font-medium">Name</th>
                    <th className="px-4 py-3 font-medium hidden sm:table-cell">
                      Size
                    </th>
                    <th className="px-4 py-3 font-medium hidden md:table-cell">
                      Modified
                    </th>
                    <th className="px-4 py-3 font-medium w-20"></th>
                  </tr>
                </thead>
                <tbody>
                  {filtered.map((file) => {
                    const Icon = iconForType(file.type);
                    const colors = colorForType(file.type);
                    return (
                      <tr
                        key={file.id}
                        className="border border-gray-50 hover:bg-gray-50 cursor-pointer group transition-colors"
                      >
                        <td className="px-4 py-3">
                          <div className="flex items-center gap-3">
                            <div
                              className={`w-8 h-8 rounded-lg ${colors} flex items-center justify-center shrink-0`}
                            >
                              <Icon className="w-4 h-4" />
                            </div>
                            <div className="min-w-0">
                              <span className="text-sm font-medium text-gray-900 truncate block">
                                {file.name}
                              </span>
                              {file.shared && (
                                <span className="text-xs text-blue-500 flex items-center gap-1 mt-0.5">
                                  <Users className="w-3 h-3" />
                                  Shared
                                </span>
                              )}
                            </div>
                          </div>
                        </td>
                        <td className="px-4 py-3 text-sm text-gray-500 hidden sm:table-cell">
                          {file.size || "--"}
                        </td>
                        <td className="px-4 py-3 text-sm text-gray-500 hidden md:table-cell">
                          {file.modified}
                        </td>
                        <td className="px-4 py-3">
                          <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
                            <button
                              onClick={(e) => {
                                e.stopPropagation();
                                toggleStar(file.id);
                              }}
                              className="p-1 hover:bg-gray-200 rounded"
                            >
                              <Star
                                className={`w-3.5 h-3.5 ${file.starred ? "text-yellow-500 fill-yellow-500" : "text-gray-400"}`}
                              />
                            </button>
                            <button className="p-1 hover:bg-gray-200 rounded">
                              <Download className="w-3.5 h-3.5 text-gray-400" />
                            </button>
                            <button className="p-1 hover:bg-gray-200 rounded">
                              <MoreVertical className="w-3.5 h-3.5 text-gray-400" />
                            </button>
                          </div>
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          )}
        </div>
      </main>
      <TemplateBackLink />
    </div>
  );
}

function SidebarContent() {
  return (
    <div className="flex-1 flex flex-col p-4 overflow-y-auto">
      <nav className="space-y-1 mb-6">
        {[
          { icon: HardDrive, label: "My Drive", active: true },
          { icon: Users, label: "Shared with me", active: false },
          { icon: Star, label: "Starred", active: false },
          { icon: Trash2, label: "Trash", 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-blue-50 text-blue-700 font-medium"
                : "text-gray-600 hover:bg-gray-100"
            }`}
          >
            <item.icon className="w-4 h-4" />
            {item.label}
          </button>
        ))}
      </nav>

      {/* Storage */}
      <div className="mt-auto pt-4 border border-gray-100">
        <div className="flex items-center justify-between text-xs text-gray-500 mb-2">
          <span>Storage</span>
          <span>
            {storageUsed} GB / {storageTotal} GB
          </span>
        </div>
        <div className="h-2 bg-gray-100 rounded-full overflow-hidden">
          <div
            className="h-full bg-blue-500 rounded-full transition-all"
            style={{ width: `${(storageUsed / storageTotal) * 100}%` }}
          />
        </div>
        <button className="mt-3 w-full py-2 text-xs text-blue-600 font-medium border border-blue-200 rounded-lg hover:bg-blue-50 transition-colors">
          Upgrade Storage
        </button>
      </div>
    </div>
  );
}

查看关联风格