feat: Warm Industrial UI redesign + social footer + code deduplication

Design System (Phase A):
- New palette: warm charcoal #111110 / amber gold #c9954a / sage green #7a9a8a
- Fonts: Cabinet Grotesk (heading) + Geist (body) + JetBrains Mono (data)
- SVG grain texture overlay, collapsed sidebar with hover expand
- Bento grid dashboard with WideContinueCard + PosterCard + QuickLink
- Backward-compatible CSS aliases for glass-panel/neon-button/input-base

Social Footer (Phase B):
- AppFooter component with GitHub repo / author homepage / TG group links
- Embedded in LoginPage, Layout (global), and SettingsPage
- TG group badge added to README.md

Code Cleanup:
- Removed deprecated DownloadClientCard / NotifyChannelCard components
- Merged downloadClient/notify API clients into unified modules
- Route deduplication: removed authed-group duplicates for admin endpoints
- Removed redundant AdminPage tabs (sites/downloads/notify/scheduler)
- UI fixes: blank pages (discover/download-clients/notify-channels) nil slice defense
- Email channel support in NotifyChannelsPage
@
This commit is contained in:
ShukeBta
2026-05-17 23:13:26 +08:00
parent 9be81222cc
commit 828ca36d7f
18 changed files with 732 additions and 910 deletions
+132 -88
View File
@@ -36,11 +36,11 @@ import {
} from 'lucide-react'
import clsx from 'clsx'
import { AppFooter } from './AppFooter'
import { libraryAPI } from '../api/library'
import { useAuthStore } from '../stores/auth'
import type { Library } from '../types'
// Top-level chrome: a fixed left rail + scrollable content area.
export function Layout() {
const navigate = useNavigate()
const user = useAuthStore((s) => s.user)
@@ -58,96 +58,132 @@ export function Layout() {
return (
<div className="flex min-h-full">
<aside className="hidden w-64 shrink-0 flex-col border-r border-white/5 bg-surface-900/80 px-4 py-6 backdrop-blur md:flex">
<Link to="/" className="mb-8 flex items-center gap-2 px-2">
<Film className="h-6 w-6 text-primary-400" />
<span className="font-display text-xl font-bold tracking-wide text-white">
MediaStationGo
</span>
</Link>
<nav className="flex-1 space-y-1 overflow-y-auto pr-1">
<SidebarLink to="/" icon={<Home size={18} />} label="首页" end />
<SidebarLink to="/discover" icon={<Compass size={18} />} label="发现" />
<SidebarLink to="/search" icon={<Search size={18} />} label="搜索" />
<SidebarLink to="/ai" icon={<Sparkles size={18} />} label="AI 助手" />
<SidebarLink to="/favourites" icon={<Heart size={18} />} label="收藏" />
<SidebarLink to="/playlists" icon={<ListMusic size={18} />} label="播放列表" />
<SidebarLink to="/history" icon={<Clock size={18} />} label="观看历史" />
<SidebarLink to="/poster-wall" icon={<GalleryHorizontalEnd size={18} />} label="海报墙" />
<div className="mt-6 px-2 text-xs uppercase tracking-wider text-slate-500">
媒体库
</div>
{libraries.length === 0 && (
<div className="px-2 py-1 text-sm text-slate-500">暂无媒体库</div>
{/* ── Sidebar group — collapsed 64px, expands to 240px on hover ── */}
<div className="group relative z-30 shrink-0">
<aside
className={clsx(
'flex h-full flex-col border-r border-cream-900/15 bg-surface-600',
'w-16 transition-all duration-300 ease-out',
'group-hover:w-60',
)}
{libraries.map((lib) => (
<SidebarLink
key={lib.id}
to={`/library/${lib.id}`}
icon={<LibraryIcon size={18} />}
label={lib.name}
/>
))}
<div className="mt-6 px-2 text-xs uppercase tracking-wider text-slate-500">
自动化
</div>
<SidebarLink to="/downloads" icon={<CloudDownload size={18} />} label="下载" />
<SidebarLink to="/subscriptions" icon={<Rss size={18} />} label="RSS 订阅" />
<SidebarLink to="/dlna" icon={<Cast size={18} />} label="DLNA 投屏" />
<SidebarLink to="/sites" icon={<Globe size={18} />} label="站点管理" />
<SidebarLink to="/site-search" icon={<Search size={18} />} label="站点搜索" />
<div className="mt-6 px-2 text-xs uppercase tracking-wider text-slate-500">
账号
</div>
<SidebarLink to="/profile" icon={<UserIcon size={18} />} label="个人资料" />
<SidebarLink to="/play-profiles" icon={<UserCog size={18} />} label="观影 Profile" />
{user?.role === 'admin' && (
<>
<div className="mt-6 px-2 text-xs uppercase tracking-wider text-slate-500">
管理
</div>
<SidebarLink to="/tasks" icon={<ListChecks size={18} />} label="实时任务" />
<SidebarLink to="/stats" icon={<Activity size={18} />} label="运行状态" />
<SidebarLink to="/storage" icon={<HardDrive size={18} />} label="存储" />
<SidebarLink to="/files" icon={<FolderTree size={18} />} label="文件浏览" />
<SidebarLink to="/duplicates" icon={<Copy size={18} />} label="重复文件" />
<SidebarLink to="/scheduler" icon={<Clock size={18} />} label="定时任务" />
<SidebarLink to="/sites" icon={<Globe size={18} />} label="站点管理" />
<SidebarLink to="/strm" icon={<Link2 size={18} />} label="STRM 管理" />
<SidebarLink to="/tools" icon={<Wrench size={18} />} label="运维工具" />
<SidebarLink to="/notify-channels" icon={<Bell size={18} />} label="通知渠道" />
<SidebarLink to="/download-clients" icon={<Server size={18} />} label="下载器" />
<SidebarLink to="/storage-config" icon={<Cloud size={18} />} label="外部存储" />
<SidebarLink to="/license" icon={<KeySquare size={18} />} label="许可证" />
<SidebarLink to="/assistant" icon={<MessageSquare size={18} />} label="AI 对话" />
<SidebarLink to="/settings" icon={<Sliders size={18} />} label="系统设置" />
<SidebarLink to="/recycle" icon={<Trash2 size={18} />} label="回收站" />
<SidebarLink to="/admin" icon={<Settings size={18} />} label="管理后台" />
</>
)}
</nav>
<button
onClick={handleLogout}
className="mt-4 flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-slate-400 transition hover:bg-white/5 hover:text-white"
>
<LogOut size={16} />
退出登录 ({user?.username})
</button>
</aside>
{/* Logo */}
<Link to="/" className="flex items-center gap-3 px-4 py-5">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-brand-500/15">
<Film className="h-4 w-4 text-brand-400" />
</div>
<span
className={clsx(
'font-display text-lg font-bold tracking-tight text-cream-100',
'overflow-hidden whitespace-nowrap opacity-0 transition-opacity duration-200',
'group-hover:opacity-100',
)}
>
MediaStation
</span>
</Link>
<main className="flex-1 overflow-y-auto px-6 py-8">
<Outlet />
{/* Navigation */}
<nav className="flex-1 space-y-0.5 overflow-y-auto px-2 pb-4">
<SidebarLink to="/" icon={<Home size={18} />} label="首页" end />
<SectionHeader label="浏览" />
<SidebarLink to="/discover" icon={<Compass size={18} />} label="发现" />
<SidebarLink to="/search" icon={<Search size={18} />} label="搜索" />
<SidebarLink to="/favourites" icon={<Heart size={18} />} label="收藏" />
<SidebarLink to="/playlists" icon={<ListMusic size={18} />} label="播放列表" />
<SidebarLink to="/history" icon={<Clock size={18} />} label="观看历史" />
<SidebarLink to="/poster-wall" icon={<GalleryHorizontalEnd size={18} />} label="海报墙" />
<SidebarLink to="/ai" icon={<Sparkles size={18} />} label="AI 助手" />
<SectionHeader label="媒体库" />
{libraries.length === 0 && (
<div className="hidden px-3 py-1.5 text-xs text-cream-500/50 group-hover:block">
暂无媒体库
</div>
)}
{libraries.map((lib) => (
<SidebarLink
key={lib.id}
to={`/library/${lib.id}`}
icon={<LibraryIcon size={18} />}
label={lib.name}
/>
))}
<SectionHeader label="自动化" />
<SidebarLink to="/downloads" icon={<CloudDownload size={18} />} label="下载" />
<SidebarLink to="/subscriptions" icon={<Rss size={18} />} label="RSS 订阅" />
<SidebarLink to="/dlna" icon={<Cast size={18} />} label="DLNA 投屏" />
<SidebarLink to="/site-search" icon={<Search size={18} />} label="站点搜索" />
<SectionHeader label="账号" />
<SidebarLink to="/profile" icon={<UserIcon size={18} />} label="个人资料" />
<SidebarLink to="/play-profiles" icon={<UserCog size={18} />} label="观影 Profile" />
{user?.role === 'admin' && (
<>
<SectionHeader label="管理" />
<SidebarLink to="/admin" icon={<Settings size={18} />} label="管理后台" />
<SidebarLink to="/tasks" icon={<ListChecks size={18} />} label="实时任务" />
<SidebarLink to="/stats" icon={<Activity size={18} />} label="运行状态" />
<SidebarLink to="/sites" icon={<Globe size={18} />} label="站点管理" />
<SidebarLink to="/notify-channels" icon={<Bell size={18} />} label="通知渠道" />
<SidebarLink to="/download-clients" icon={<Server size={18} />} label="下载器" />
<SidebarLink to="/scheduler" icon={<Clock size={18} />} label="定时任务" />
<SidebarLink to="/storage" icon={<HardDrive size={18} />} label="存储" />
<SidebarLink to="/storage-config" icon={<Cloud size={18} />} label="外部存储" />
<SidebarLink to="/files" icon={<FolderTree size={18} />} label="文件浏览" />
<SidebarLink to="/duplicates" icon={<Copy size={18} />} label="重复文件" />
<SidebarLink to="/strm" icon={<Link2 size={18} />} label="STRM 管理" />
<SidebarLink to="/tools" icon={<Wrench size={18} />} label="运维工具" />
<SidebarLink to="/license" icon={<KeySquare size={18} />} label="许可证" />
<SidebarLink to="/assistant" icon={<MessageSquare size={18} />} label="AI 对话" />
<SidebarLink to="/settings" icon={<Sliders size={18} />} label="系统设置" />
<SidebarLink to="/recycle" icon={<Trash2 size={18} />} label="回收站" />
</>
)}
</nav>
{/* Bottom: logout */}
<div className="border-t border-cream-900/15 px-3 py-3">
<button
onClick={handleLogout}
className="flex w-full items-center gap-3 rounded-lg px-2 py-2 text-sm text-cream-400 transition hover:bg-cream-900/10 hover:text-cream-200"
title={`退出登录 (${user?.username})`}
>
<LogOut size={16} />
<span className="overflow-hidden whitespace-nowrap opacity-0 transition-opacity duration-200 group-hover:opacity-100">
退出
</span>
</button>
</div>
</aside>
</div>
{/* ── Main content ── */}
<main className="flex flex-1 flex-col overflow-y-auto">
<div className="flex-1 px-6 py-8">
<Outlet />
</div>
<AppFooter className="py-4" />
</main>
</div>
)
}
/** Section divider — visible only when sidebar is expanded */
function SectionHeader({ label }: { label: string }) {
return (
<div className="mt-4 mb-1 overflow-hidden whitespace-nowrap px-3 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
<span className="text-[10px] font-medium uppercase tracking-[0.15em] text-cream-500/50">
{label}
</span>
</div>
)
}
/** Single nav link — icon always visible, label fades in on expand */
function SidebarLink({
to,
icon,
@@ -165,15 +201,23 @@ function SidebarLink({
end={end}
className={({ isActive }) =>
clsx(
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition',
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors',
isActive
? 'bg-primary-400/10 text-primary-400'
: 'text-slate-300 hover:bg-white/5 hover:text-white',
? 'bg-brand-500/10 text-brand-400'
: 'text-cream-400 hover:bg-cream-900/10 hover:text-cream-200',
)
}
>
{icon}
<span className="truncate">{label}</span>
<span className="flex shrink-0 items-center justify-center w-5 h-5">{icon}</span>
<span
className={clsx(
'overflow-hidden whitespace-nowrap truncate',
'opacity-0 transition-opacity duration-200',
'group-hover:opacity-100',
)}
>
{label}
</span>
</NavLink>
)
}