+ {/* 面包屑导航 / 页面标题 */}
+
+
+ 📺 媒体库
+
+ }
+ >
+
+
+
+
+ {/* 右侧工具区 */}
+
+ {/* 侧边栏收起/展开按钮 */}
+
+
+ {/* 搜索按钮和布局切换(仅文件浏览时显示) */}
+
+ {/* 搜索按钮 */}
+
+
+
+
+ {/* 工具操作按钮 */}
+
+
+ {/* 布局切换 */}
+
+
+
+
+
+
+ )
+}
+
+// ─── 根布局 ──────────────────────────────────────────────────
+export const RootLayout = (props: RootLayoutProps) => {
+ const [isMobile, setIsMobile] = createSignal(
+ typeof window !== "undefined" ? window.innerWidth < 768 : false,
+ )
+
+ onMount(() => {
+ const handler = () => setIsMobile(window.innerWidth < 768)
+ window.addEventListener("resize", handler)
+ onCleanup(() => window.removeEventListener("resize", handler))
+ })
+
+ // 与 GlobalSidebar 中的 sidebarWidth 保持一致:180px / 56px
+ const marginLeft = createMemo(() => {
+ if (isMobile()) return "0px"
+ return sidebarCollapsed() ? "48px" : "120px"
+ })
+
+ return (
+
+ setSidebarVisible(false)}
+ style={{
+ position: "fixed",
+ inset: "0",
+ background: "rgba(0,0,0,0.5)",
+ "z-index": "99",
+ "backdrop-filter": "blur(3px)",
+ }}
+ />
+
+
+ {/* ══════════════ 侧边栏主体 ══════════════ */}
+
+ {/* ── Logo / 标题区 ── */}
+
+
+
+ {/* Logo 图片(从设置读取,与 Header 保持一致) */}
+
+ }
+ />
+ {/* 站点标题(从数据库设置读取) */}
+
+
+ {siteTitle}
+
+
+
+
+
+
+ {/* ── 导航菜单 ── */}
+
+
+ {/* ── 底部工具栏 ── */}
+
+ {/* 亮/暗模式切换 */}
+
+
+ {/* 系统设置 */}
+
+
+ {/* 透明模式切换 */}
+
+
+
+
+ {/* ══════════════ 移动端汉堡按钮 ══════════════ */}
+
+
+
+ >
+ )
+}
diff --git a/src/lang/en/manage.json b/src/lang/en/manage.json
index 5829c6ee7..0f23a94c4 100644
--- a/src/lang/en/manage.json
+++ b/src/lang/en/manage.json
@@ -27,7 +27,13 @@
"ldap": "LDAP",
"s3": "S3",
"ftp": "FTP",
- "traffic": "Traffic"
+ "traffic": "Traffic",
+ "media": "Media Library",
+ "media_video": "Video",
+ "media_music": "Music",
+ "media_image": "Images",
+ "media_book": "Books",
+ "media_settings": "Settings"
},
"title": "OpenList Management",
"not_admin": "You are not an admin user, please login with an admin account.",
diff --git a/src/lang/en/settings.json b/src/lang/en/settings.json
index c624a41d7..908e06299 100755
--- a/src/lang/en/settings.json
+++ b/src/lang/en/settings.json
@@ -142,5 +142,14 @@
"version": "Version",
"video_autoplay": "Video autoplay",
"video_types": "Video types",
- "webauthn_login_enabled": "Webauthn login enabled"
+ "webauthn_login_enabled": "Webauthn login enabled",
+ "media_tmdb_key": "TMDB API Key",
+ "media_discogs_token": "Discogs Token",
+ "media_store_thumbnail": "Store thumbnail",
+ "media_thumbnail_mode": "Thumbnail storage mode",
+ "media_thumbnail_modes": {
+ "base64": "Base64 (Database)",
+ "local": "Local file"
+ },
+ "media_thumbnail_path": "Thumbnail storage path"
}
diff --git a/src/pages/home/Body.tsx b/src/pages/home/Body.tsx
index a8d032dcc..185af2f02 100644
--- a/src/pages/home/Body.tsx
+++ b/src/pages/home/Body.tsx
@@ -1,24 +1,13 @@
import { VStack } from "@hope-ui/solid"
-import { Nav } from "./Nav"
import { Obj } from "./Obj"
import { Readme } from "./Readme"
-import { Container } from "./Container"
import { Sidebar } from "./Sidebar"
export const Body = () => {
return (
-
-
+
+
-
{
/>
-
+
)
}
diff --git a/src/pages/home/Layout.tsx b/src/pages/home/Layout.tsx
index d5a485f1b..69184fad5 100644
--- a/src/pages/home/Layout.tsx
+++ b/src/pages/home/Layout.tsx
@@ -4,7 +4,6 @@ import { getSetting } from "~/store"
import { notify } from "~/utils"
import { Body } from "./Body"
import { Footer } from "./Footer"
-import { Header } from "./header/Header"
import { Toolbar } from "./toolbar/Toolbar"
import { onMount } from "solid-js"
@@ -21,12 +20,17 @@ const Index = () => {
}
})
return (
- <>
-
+
- >
+
)
}
diff --git a/src/pages/home/toolbar/Right.tsx b/src/pages/home/toolbar/Right.tsx
index 82ec5ac8b..2004deb0a 100644
--- a/src/pages/home/toolbar/Right.tsx
+++ b/src/pages/home/toolbar/Right.tsx
@@ -1,4 +1,4 @@
-import { Box, createDisclosure, VStack } from "@hope-ui/solid"
+import { Box, createDisclosure, HStack, VStack } from "@hope-ui/solid"
import { createMemo, Show } from "solid-js"
import { RightIcon } from "./Icon"
import { CgMoreO } from "solid-icons/cg"
@@ -14,146 +14,104 @@ import { Motion } from "solid-motionone"
import { isTocVisible, setTocDisabled } from "~/components"
import { BiSolidBookContent } from "solid-icons/bi"
-export const Right = () => {
- const { isOpen, onToggle } = createDisclosure({
- defaultIsOpen: localStorage.getItem("more-open") === "true",
- onClose: () => localStorage.setItem("more-open", "false"),
- onOpen: () => localStorage.setItem("more-open", "true"),
- })
- const margin = createMemo(() => (isOpen() ? "$4" : "$5"))
+// ─── 顶栏工具按钮(水平排列,嵌入顶栏使用)────────────────────
+export const TopBarActions = () => {
const isFolder = createMemo(() => objStore.state === State.Folder)
const { refresh } = usePath()
const { isShare } = useRouter()
return (
-
+
+ {
+ refresh(undefined, true)
+ }}
+ />
{
- onToggle()
- }}
- />
- }
+ when={isFolder() && !isShare() && (userCan("write") || objStore.write)}
>
-
-
- {
- refresh(undefined, true)
- }}
- />
-
- {/* */}
- {
- bus.emit("tool", "new_file")
- }}
- />
- {
- bus.emit("tool", "mkdir")
- }}
- />
- {
- bus.emit("tool", "recursiveMove")
- }}
- />
- {
- bus.emit("tool", "removeEmptyDirectory")
- }}
- />
- {
- selectAll(true)
- bus.emit("tool", "batchRename")
- }}
- />
- {
- bus.emit("tool", "upload")
- }}
- />
-
-
- {
- bus.emit("tool", "offline_download")
- }}
- />
-
-
- {
- setTocDisabled((disabled) => !disabled)
- }}
- />
-
-
- {
- bus.emit("tool", "local_settings")
- }}
- />
-
-
-
+ {
+ bus.emit("tool", "new_file")
+ }}
+ />
+ {
+ bus.emit("tool", "mkdir")
+ }}
+ />
+ {
+ bus.emit("tool", "recursiveMove")
+ }}
+ />
+ {
+ bus.emit("tool", "removeEmptyDirectory")
+ }}
+ />
+ {
+ selectAll(true)
+ bus.emit("tool", "batchRename")
+ }}
+ />
+ {
+ bus.emit("tool", "upload")
+ }}
+ />
+
+
+ {
+ bus.emit("tool", "offline_download")
+ }}
+ />
+
+
+ {
+ setTocDisabled((disabled) => !disabled)
+ }}
+ />
-
+
+ {
+ bus.emit("tool", "local_settings")
+ }}
+ />
+
)
}
+
+// ─── 原右下角浮动按钮(已迁移到顶栏,保留空组件避免引用报错)────
+export const Right = () => {
+ return null
+}
diff --git a/src/pages/manage/media/MediaManage.tsx b/src/pages/manage/media/MediaManage.tsx
new file mode 100644
index 000000000..36ddc35cb
--- /dev/null
+++ b/src/pages/manage/media/MediaManage.tsx
@@ -0,0 +1,858 @@
+import { createSignal, createResource, Show, For, createEffect } from "solid-js"
+import {
+ adminGetMediaConfigs,
+ adminSaveMediaConfig,
+ adminGetMediaItems,
+ adminUpdateMediaItem,
+ adminDeleteMediaItem,
+ adminStartMediaScan,
+ adminStartMediaScrape,
+ adminClearMediaDB,
+ adminGetMediaScanProgress,
+} from "~/utils/media_api"
+import type { MediaType, MediaItem, MediaConfig } from "~/types"
+
+// 别名,方便内部使用
+const getMediaConfig = async (mt: MediaType) => {
+ const resp = await adminGetMediaConfigs()
+ if (resp.code === 200) {
+ const found = (resp.data as MediaConfig[]).find((c) => c.media_type === mt)
+ return { code: 200, data: found ?? null }
+ }
+ return { code: resp.code, data: null }
+}
+const saveMediaConfig = adminSaveMediaConfig
+const listMediaItems = adminGetMediaItems
+const updateMediaItem = adminUpdateMediaItem
+const deleteMediaItem = adminDeleteMediaItem
+const scanMedia = adminStartMediaScan
+const scrapeMedia = adminStartMediaScrape
+const clearMediaDB = adminClearMediaDB
+const getMediaScanProgress = adminGetMediaScanProgress
+
+// ==================== 通用媒体管理页 ====================
+interface MediaManagePageProps {
+ mediaType: MediaType
+ title: string
+ icon: string
+}
+
+export const MediaManagePage = (props: MediaManagePageProps) => {
+ // 配置状态
+ const [config, setConfig] = createSignal({
+ media_type: props.mediaType,
+ enabled: false,
+ scan_path: "/",
+ path_merge: false,
+ last_scan_at: null,
+ last_scrape_at: null,
+ })
+ const [configSaving, setConfigSaving] = createSignal(false)
+
+ // 扫描/刮削状态
+ const [scanning, setScanning] = createSignal(false)
+ const [scraping, setScraping] = createSignal(false)
+ const [progress, setProgress] = createSignal<{
+ status: string
+ current: number
+ total: number
+ } | null>(null)
+
+ // 数据库管理状态
+ const [page, setPage] = createSignal(1)
+ const [editingItem, setEditingItem] = createSignal(null)
+ const [showEditModal, setShowEditModal] = createSignal(false)
+ const pageSize = 20
+
+ // 加载配置
+ const [configData] = createResource(
+ () => props.mediaType,
+ async (mt) => {
+ const resp = await getMediaConfig(mt)
+ if (resp.code === 200 && resp.data) {
+ setConfig(resp.data)
+ }
+ return resp.data
+ },
+ )
+
+ // 加载媒体条目
+ const [itemsData, { refetch: refetchItems }] = createResource(
+ () => ({ media_type: props.mediaType, page: page(), page_size: pageSize }),
+ async (params) => {
+ const resp = await listMediaItems(params)
+ if (resp.code === 200) return resp.data
+ return { content: [], total: 0 }
+ },
+ )
+
+ const items = () => (itemsData()?.content as MediaItem[]) ?? []
+ const total = () => itemsData()?.total ?? 0
+ const totalPages = () => Math.ceil(total() / pageSize)
+
+ // 保存配置
+ const handleSaveConfig = async () => {
+ setConfigSaving(true)
+ await saveMediaConfig(config())
+ setConfigSaving(false)
+ }
+
+ // 立即扫描
+ const handleScan = async () => {
+ setScanning(true)
+ setProgress({ status: "扫描中...", current: 0, total: 0 })
+ await scanMedia(props.mediaType)
+ // 轮询进度
+ const timer = setInterval(async () => {
+ const resp = await getMediaScanProgress(props.mediaType)
+ if (resp.code === 200 && resp.data) {
+ const d = resp.data
+ setProgress({
+ status: d.message || (d.running ? "扫描中..." : "完成"),
+ current: d.done,
+ total: d.total,
+ })
+ if (!d.running) {
+ clearInterval(timer)
+ setScanning(false)
+ refetchItems()
+ }
+ }
+ }, 1000)
+ }
+
+ // 立即刮削
+ const handleScrape = async () => {
+ setScraping(true)
+ await scrapeMedia(props.mediaType)
+ setScraping(false)
+ refetchItems()
+ }
+
+ // 清空数据库
+ const handleClear = async () => {
+ if (!confirm(`确定要清空 ${props.title} 的所有数据吗?此操作不可恢复!`))
+ return
+ await clearMediaDB(props.mediaType)
+ refetchItems()
+ }
+
+ // 保存编辑
+ const handleSaveItem = async () => {
+ if (!editingItem()) return
+ await updateMediaItem(editingItem()!)
+ setShowEditModal(false)
+ setEditingItem(null)
+ refetchItems()
+ }
+
+ // 删除条目
+ const handleDeleteItem = async (id: number) => {
+ if (!confirm("确定删除此条目?")) return
+ await deleteMediaItem(id)
+ refetchItems()
+ }
+
+ return (
+
+
+ {props.icon} {props.title}管理
+
+
+ {/* 配置区域 */}
+
+
+ 基础配置
+
+
+
+ {/* 启用开关 */}
+
+
+ {/* 扫描路径 */}
+
+
+ {/* 路径合并 */}
+
+
+ {/* 操作按钮(与控件同行) */}
+
+
+
+
+
+
+
+
+ {/* 进度显示 */}
+
+
+ {progress()?.status}
+ 0}>
+ {" "}
+ ({progress()?.current} / {progress()?.total})
+
+
+
+
+
+ {/* 数据库管理表格 */}
+
+
+
+ 数据库管理(共 {total()} 条)
+
+
+
+
+
+
+
+ {[
+ "文件路径",
+ "名称",
+ "封面",
+ "发布时间",
+ "评分",
+ "类型",
+ "作者/演员",
+ "隐藏",
+ "操作",
+ ].map((h) => (
+ |
+ {h}
+ |
+ ))}
+
+
+
+
+
+ 加载中...
+ |
+
+ }
+ >
+
+ {(item) => (
+ {
+ e.currentTarget.style.background = "#f8fafc"
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.background = "transparent"
+ }}
+ >
+ |
+
+ {item.file_path}
+
+ |
+
+ {item.scraped_name || item.file_name}
+ |
+
+
+
+
+ |
+
+ {item.release_date?.slice(0, 10)}
+ |
+
+ {item.rating > 0 ? item.rating.toFixed(1) : "-"}
+ |
+
+ {item.genre?.split(",")[0] || "-"}
+ |
+
+
+ {item.authors
+ ? JSON.parse(item.authors || "[]")
+ .slice(0, 2)
+ .join(", ")
+ : "-"}
+
+ |
+
+ {
+ await updateMediaItem({
+ ...item,
+ hidden: !item.hidden,
+ })
+ refetchItems()
+ }}
+ style={{
+ width: "36px",
+ height: "20px",
+ "border-radius": "10px",
+ background: item.hidden ? "#6366f1" : "#d1d5db",
+ position: "relative",
+ cursor: "pointer",
+ transition: "background 0.2s",
+ }}
+ >
+
+
+ |
+
+
+
+
+
+ |
+
+ )}
+
+
+
+
+
+
+ {/* 分页 */}
+
1}>
+
+
+
+ {page()} / {totalPages()}
+
+
+
+
+
+
+ {/* 编辑弹窗 */}
+
+ {
+ if (e.target === e.currentTarget) setShowEditModal(false)
+ }}
+ >
+
+
+ 编辑媒体信息
+
+
+
+ {[
+ { key: "scraped_name", label: "名称" },
+ { key: "cover", label: "封面URL" },
+ { key: "release_date", label: "发布时间" },
+ { key: "genre", label: "类型(逗号分隔)" },
+ { key: "authors", label: "作者/演员(JSON数组)" },
+ ].map(({ key, label }) => (
+
+
+
+ setEditingItem((item) => ({
+ ...item!,
+ [key]: e.currentTarget.value,
+ }))
+ }
+ style={{
+ width: "100%",
+ border: "1px solid #d1d5db",
+ "border-radius": "6px",
+ padding: "7px 10px",
+ "font-size": "13px",
+ outline: "none",
+ "box-sizing": "border-box",
+ }}
+ />
+
+ ))}
+
+ {/* 评分 */}
+
+
+
+ setEditingItem((item) => ({
+ ...item!,
+ rating: parseFloat(e.currentTarget.value),
+ }))
+ }
+ style={{
+ width: "100%",
+ border: "1px solid #d1d5db",
+ "border-radius": "6px",
+ padding: "7px 10px",
+ "font-size": "13px",
+ outline: "none",
+ "box-sizing": "border-box",
+ }}
+ />
+
+
+ {/* 简介 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const manageBtnStyle = (color: string) => ({
+ background: color,
+ border: "none",
+ "border-radius": "8px",
+ color: "white",
+ padding: "8px 16px",
+ "font-size": "13px",
+ "font-weight": "500",
+ cursor: "pointer",
+})
+
+// ==================== 4个具体管理页 ====================
+
+export const VideoManage = () => (
+
+)
+
+export const MusicManage = () => (
+
+)
+
+export const ImageManage = () => (
+
+)
+
+export const BookManage = () => (
+
+)
diff --git a/src/pages/manage/sidemenu_items.tsx b/src/pages/manage/sidemenu_items.tsx
index 6776e38e4..fcefa203a 100644
--- a/src/pages/manage/sidemenu_items.tsx
+++ b/src/pages/manage/sidemenu_items.tsx
@@ -199,6 +199,59 @@ export const side_menu_items: SideMenuItem[] = [
icon: FaSolidDatabase,
component: lazy(() => import("./backup-restore")),
},
+ {
+ title: "manage.sidemenu.media",
+ icon: BsCameraFill,
+ to: "/@manage/media",
+ children: [
+ {
+ title: "manage.sidemenu.media_video",
+ icon: BsCameraFill,
+ to: "/@manage/media/video",
+ component: lazy(() =>
+ import("./media/MediaManage").then((m) => ({
+ default: m.VideoManage,
+ })),
+ ),
+ },
+ {
+ title: "manage.sidemenu.media_music",
+ icon: BsCameraFill,
+ to: "/@manage/media/music",
+ component: lazy(() =>
+ import("./media/MediaManage").then((m) => ({
+ default: m.MusicManage,
+ })),
+ ),
+ },
+ {
+ title: "manage.sidemenu.media_image",
+ icon: BsCameraFill,
+ to: "/@manage/media/image",
+ component: lazy(() =>
+ import("./media/MediaManage").then((m) => ({
+ default: m.ImageManage,
+ })),
+ ),
+ },
+ {
+ title: "manage.sidemenu.media_book",
+ icon: BsCameraFill,
+ to: "/@manage/media/book",
+ component: lazy(() =>
+ import("./media/MediaManage").then((m) => ({
+ default: m.BookManage,
+ })),
+ ),
+ },
+ {
+ title: "manage.sidemenu.media_settings",
+ icon: BsGearFill,
+ to: "/@manage/media/settings",
+ component: () => ,
+ },
+ ],
+ },
{
title: "manage.sidemenu.about",
icon: BsFront,
diff --git a/src/pages/media/MediaBrowser.tsx b/src/pages/media/MediaBrowser.tsx
new file mode 100644
index 000000000..170320589
--- /dev/null
+++ b/src/pages/media/MediaBrowser.tsx
@@ -0,0 +1,511 @@
+import {
+ createSignal,
+ createResource,
+ createEffect,
+ createMemo,
+ For,
+ Show,
+ Switch,
+ Match,
+} from "solid-js"
+import { useColorMode } from "@hope-ui/solid"
+import { getMediaList, getMediaFolders } from "~/utils/media_api"
+import type { MediaItem, MediaType } from "~/types"
+import { getMediaName } from "~/types"
+
+interface MediaBrowserProps {
+ mediaType: MediaType
+ onItemClick: (item: MediaItem) => void
+ onItemsChange?: (items: MediaItem[]) => void
+ renderCard: (item: MediaItem) => any
+ renderListRow?: (item: MediaItem) => any
+}
+
+type ViewMode = "waterfall" | "list"
+type BrowseMode = "all" | "folder"
+type OrderBy = "name" | "date" | "size"
+type OrderDir = "asc" | "desc"
+
+export const MediaBrowser = (props: MediaBrowserProps) => {
+ const { colorMode } = useColorMode()
+ const isDark = createMemo(() => colorMode() === "dark")
+
+ // 主题色 tokens
+ const toolbarBg = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.03)",
+ )
+ const toolbarBorder = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.08)",
+ )
+ const dividerColor = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
+ )
+ const btnActiveBg = "rgba(99,102,241,0.3)"
+ const btnActiveBorder = "rgba(99,102,241,0.5)"
+ const btnActiveColor = "#a5b4fc"
+ const btnInactiveBg = createMemo(() =>
+ isDark() ? "transparent" : "transparent",
+ )
+ const btnInactiveBorder = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.15)",
+ )
+ const btnInactiveColor = createMemo(() => (isDark() ? "#64748b" : "#64748b"))
+ const sortLabelColor = createMemo(() => (isDark() ? "#475569" : "#64748b"))
+ const sortActiveBg = createMemo(() => "rgba(99,102,241,0.3)")
+ const sortActiveBorder = createMemo(() => "rgba(99,102,241,0.5)")
+ const sortActiveColor = "#a5b4fc"
+ const sortInactiveBg = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)",
+ )
+ const sortInactiveBorder = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.12)",
+ )
+ const sortInactiveColor = createMemo(() => (isDark() ? "#94a3b8" : "#475569"))
+ const searchBg = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)",
+ )
+ const searchBorder = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.15)",
+ )
+ const searchColor = createMemo(() => (isDark() ? "#e2e8f0" : "#1e293b"))
+ const folderBtnBg = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.04)",
+ )
+ const emptyColor = createMemo(() => (isDark() ? "#475569" : "#94a3b8"))
+ const listItemBg = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
+ )
+ const listItemBorder = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.06)",
+ )
+ const listItemTextColor = createMemo(() => (isDark() ? "#e2e8f0" : "#1e293b"))
+ const paginationBg = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)",
+ )
+ const paginationBorder = createMemo(() =>
+ isDark() ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.12)",
+ )
+ const paginationColor = createMemo(() => (isDark() ? "#94a3b8" : "#475569"))
+ const paginationDisabledColor = createMemo(() =>
+ isDark() ? "#334155" : "#cbd5e1",
+ )
+ const paginationInfoColor = createMemo(() =>
+ isDark() ? "#64748b" : "#94a3b8",
+ )
+
+ const [viewMode, setViewMode] = createSignal("waterfall")
+ const [browseMode, setBrowseMode] = createSignal("all")
+ const [orderBy, setOrderBy] = createSignal("name")
+ const [orderDir, setOrderDir] = createSignal("asc")
+ const [page, setPage] = createSignal(1)
+ const [keyword, setKeyword] = createSignal("")
+ const [selectedFolder, setSelectedFolder] = createSignal("")
+
+ const pageSize = 40
+
+ // 加载媒体列表
+ const [mediaData] = createResource(
+ () => ({
+ media_type: props.mediaType,
+ page: page(),
+ page_size: pageSize,
+ order_by: orderBy(),
+ order_dir: orderDir(),
+ folder_path: browseMode() === "folder" ? selectedFolder() : undefined,
+ keyword: keyword() || undefined,
+ }),
+ async (params) => {
+ const resp = await getMediaList(params)
+ if (resp.code === 200) return resp.data
+ return { content: [], total: 0 }
+ },
+ )
+
+ // 加载文件夹列表
+ const [foldersData] = createResource(
+ () => (browseMode() === "folder" ? props.mediaType : null),
+ async (mt) => {
+ if (!mt) return []
+ const resp = await getMediaFolders(mt)
+ if (resp.code === 200) return resp.data
+ return []
+ },
+ )
+
+ const items = () => (mediaData()?.content as MediaItem[]) ?? []
+ const total = () => mediaData()?.total ?? 0
+ const totalPages = () => Math.ceil(total() / pageSize)
+
+ // 数据变化时通知父组件
+ createEffect(() => {
+ const list = items()
+ if (list.length > 0) props.onItemsChange?.(list)
+ })
+
+ const toggleOrder = (col: OrderBy) => {
+ if (orderBy() === col) {
+ setOrderDir(orderDir() === "asc" ? "desc" : "asc")
+ } else {
+ setOrderBy(col)
+ setOrderDir("asc")
+ }
+ setPage(1)
+ }
+
+ const OrderBtn = (p: { col: OrderBy; label: string }) => (
+
+ )
+
+ return (
+
+ {/* 工具栏 */}
+
+ {/* 浏览模式 */}
+
+ {(["all", "folder"] as BrowseMode[]).map((mode) => (
+
+ ))}
+
+
+
+
+ {/* 排序 */}
+
+
+ 排序:
+
+
+
+
+
+
+
+
+ {/* 搜索 */}
+
{
+ setKeyword(e.currentTarget.value)
+ setPage(1)
+ }}
+ style={{
+ background: searchBg(),
+ border: `1px solid ${searchBorder()}`,
+ "border-radius": "8px",
+ color: searchColor(),
+ padding: "6px 12px",
+ "font-size": "13px",
+ outline: "none",
+ width: "160px",
+ }}
+ />
+
+ {/* 视图切换 */}
+
+ {(["waterfall", "list"] as ViewMode[]).map((mode) => (
+
+ ))}
+
+
+
+ {/* 目录浏览模式 - 文件夹列表 */}
+
+
+
+
+ {(folder) => (
+
+ )}
+
+
+
+
+ {/* 内容区域 */}
+
+
+ ⏳
+
+ 加载中...
+
+ }
+ >
+ 0}
+ fallback={
+
+ }
+ >
+
+ {/* 瀑布流视图 */}
+
+
+
+ {(item) => (
+ props.onItemClick(item)}
+ >
+ {props.renderCard(item)}
+
+ )}
+
+
+
+
+ {/* 列表视图 */}
+
+
+
+ {(item) => (
+ props.onItemClick(item)}
+ style={{
+ display: "flex",
+ "align-items": "center",
+ gap: "12px",
+ padding: "10px 16px",
+ background: listItemBg(),
+ "border-radius": "8px",
+ border: `1px solid ${listItemBorder()}`,
+ cursor: "pointer",
+ transition: "background 0.15s",
+ }}
+ >
+ {props.renderListRow ? (
+ props.renderListRow(item)
+ ) : (
+ <>
+ 🎬
+
+ {getMediaName(item)}
+
+ >
+ )}
+
+ )}
+
+
+
+
+
+
+
+ {/* 分页 */}
+