Improve frontend navigation and mobile UX

- Add mobile bottom tab bar for media browsing (Home, Libraries, Favourites, Playlists, More)
- Split mobile drawer: media links first, admin links under a Management section
- Show contextual back button in mobile header for deep routes
- Add reusable PageBackButton and PageHeader components
- Add back navigation to playlist detail, library page, and missing media state
- Embed queue panels in TaskQueuePage to avoid duplicate page titles
- Expose theme toggle in user menu on small screens

Co-authored-by: truewhile <truewhile@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-09-02 02:19:25 +00:00
parent 1171fe9464
commit eba7904c99
15 changed files with 388 additions and 37 deletions
+7
View File
@@ -13,6 +13,7 @@ import {
type SortOrder,
} from '../utils/mediaSort'
import { LibraryPageDialogs } from './LibraryPageDialogs'
import { PageBackButton } from '../components/PageBackButton'
import { LibraryPageHeader } from './LibraryPageHeader'
import { LibraryMediaSections } from './LibraryMediaSections'
import { LibrarySeriesDetailSection } from './LibrarySeriesDetailSection'
@@ -181,6 +182,12 @@ export function LibraryPage() {
return (
<div className="space-y-6">
{!selectedSeries && (
<div className="hidden lg:block">
<PageBackButton to="/libraries" label="全部媒体库" compact />
</div>
)}
{!selectedSeries && (
<LibraryPageHeader
library={library}
+7 -2
View File
@@ -1,6 +1,8 @@
import { ArrowLeft, Heart, Play, RefreshCw } from 'lucide-react'
import { Link } from 'react-router-dom'
import { PageBackButton } from '../components/PageBackButton'
import { ExternalPlayerButton } from '../components/ExternalPlayerButton'
import { ManualScrapeDialog } from '../components/ManualScrapeDialog'
import { MetadataEditDialog } from '../components/MetadataEditDialog'
@@ -63,8 +65,11 @@ export function MediaDetailLoading() {
export function MediaDetailMissing() {
return (
<div className="text-center py-24 bg-white rounded-2xl border border-gray-200">
<p className="text-gray-500">媒体资源已被移除或不存在</p>
<div className="space-y-4 py-12 text-center">
<PageBackButton label="返回上一页" className="mx-auto" />
<div className="rounded-2xl border border-gray-200 bg-white py-16">
<p className="text-gray-500">媒体资源已被移除或不存在</p>
</div>
</div>
)
}
+7 -4
View File
@@ -5,6 +5,7 @@ import toast from 'react-hot-toast'
import { playbackAPI, type PlaylistDetail } from '../api/playback'
import { MediaCard } from '../components/MediaCard'
import { PageHeader } from '../components/PageHeader'
export function PlaylistDetailPage() {
const { id = '' } = useParams()
@@ -24,10 +25,12 @@ export function PlaylistDetailPage() {
return (
<div className="space-y-6">
<header>
<h1 className="font-display text-3xl font-bold text-ink-600">{detail.playlist.name}</h1>
<p className="text-sm text-ink-50">{detail.items.length} 部影片</p>
</header>
<PageHeader
backTo="/playlists"
backLabel="播放列表"
title={detail.playlist.name}
description={`${detail.items.length} 部影片`}
/>
{detail.items.length === 0 && (
<p className="text-ink-50">暂无内容,前往媒体详情页通过「加入播放列表」添加。</p>
+3 -2
View File
@@ -60,7 +60,7 @@ const TYPE_LABELS: Record<string, string> = {
const PAGE_SIZE = 50
export function ScraperQueuePage() {
export function ScraperQueuePage({ embedded = false }: { embedded?: boolean }) {
const [snapshot, setSnapshot] = useState<ScrapeQueueSnapshot | null>(null)
const [filter, setFilter] = useState<'all' | ScrapeTaskStatus>('all')
const [search, setSearch] = useState('')
@@ -251,7 +251,7 @@ export function ScraperQueuePage() {
return (
<div className="space-y-6">
{/* 1. Header */}
{!embedded && (
<header className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-primary-400/30 bg-primary-400/10 text-brand-500 shadow-sm">
@@ -385,6 +385,7 @@ export function ScraperQueuePage() {
</details>
</div>
</header>
)}
{/* 2. Status Cards */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
+9 -2
View File
@@ -55,7 +55,13 @@ function getFileIcon(filename: string): ReactNode {
return <File size={15} className="text-gray-400 shrink-0" />
}
export function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) {
export function StrmQueuePanel({
kind,
embedded = false,
}: {
kind: 'download' | 'upload'
embedded?: boolean
}) {
const [snapshot, setSnapshot] = useState<StrmQueueSnapshot | null>(null)
const [filter, setFilter] = useState<'all' | StrmTaskStatus>('all')
const [search, setSearch] = useState('')
@@ -242,7 +248,7 @@ export function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) {
return (
<div className="space-y-6">
{/* 1. Header */}
{!embedded && (
<header className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-primary-400/30 bg-primary-400/10 text-brand-500 shadow-sm">
@@ -403,6 +409,7 @@ export function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) {
</details>
</div>
</header>
)}
{/* 2. Interactive Status Cards */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
+6 -6
View File
@@ -115,9 +115,9 @@ export function TaskQueuePage() {
</div>
{/* 内容 */}
{type === 'scrape' && <ScraperQueuePage />}
{type === 'download' && <StrmQueuePanel kind="download" />}
{type === 'upload' && <StrmQueuePanel kind="upload" />}
{type === 'scrape' && <ScraperQueuePage embedded />}
{type === 'download' && <StrmQueuePanel kind="download" embedded />}
{type === 'upload' && <StrmQueuePanel kind="upload" embedded />}
{type === 'all' && (
<div className="space-y-10">
<section>
@@ -125,21 +125,21 @@ export function TaskQueuePage() {
<Sparkles size={18} className="text-brand-500" />
刮削队列
</h2>
<ScraperQueuePage />
<ScraperQueuePage embedded />
</section>
<section>
<h2 className="mb-2 flex items-center gap-2 font-display text-xl font-bold text-ink-600">
<Download size={18} className="text-brand-500" />
下载队列
</h2>
<StrmQueuePanel kind="download" />
<StrmQueuePanel kind="download" embedded />
</section>
<section>
<h2 className="mb-2 flex items-center gap-2 font-display text-xl font-bold text-ink-600">
<Upload size={18} className="text-brand-500" />
上传队列
</h2>
<StrmQueuePanel kind="upload" />
<StrmQueuePanel kind="upload" embedded />
</section>
</div>
)}