Add MusicWorkshop application
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
Database,
|
||||
History,
|
||||
LayoutDashboard,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Wifi
|
||||
} from 'lucide-react';
|
||||
import { NavLink, Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ to: '/workbench', label: '工作台', icon: LayoutDashboard },
|
||||
{ to: '/library', label: '音乐库', icon: Database },
|
||||
{ to: '/exceptions', label: '异常中心', icon: AlertTriangle },
|
||||
{ to: '/history', label: '任务历史', icon: History },
|
||||
{ to: '/settings', label: '系统配置', icon: Settings }
|
||||
];
|
||||
|
||||
const PAGE_TITLES = {
|
||||
'/workbench': '工作台',
|
||||
'/library': '音乐库',
|
||||
'/exceptions': '异常中心',
|
||||
'/history': '任务历史',
|
||||
'/settings': '系统配置'
|
||||
};
|
||||
|
||||
export default function AppLayout({ connState, taskState }) {
|
||||
const location = useLocation();
|
||||
const pageTitle = PAGE_TITLES[location.pathname] || '工作台';
|
||||
const isWorkbenchPage = location.pathname === '/workbench';
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen overflow-hidden bg-slate-950 font-sans text-slate-300">
|
||||
<div className="flex w-64 flex-col border-r border-slate-800 bg-slate-900">
|
||||
<div className="flex items-center space-x-3 p-6 text-emerald-400">
|
||||
<Activity className="h-8 w-8" />
|
||||
<span className="text-2xl font-bold tracking-wider text-white">音流工坊</span>
|
||||
</div>
|
||||
<div className="mb-2 px-4 text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||
主菜单
|
||||
</div>
|
||||
<nav className="flex-1 space-y-1 px-3">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<NavButton
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
<div className="border-t border-slate-800 p-4 text-xs text-slate-500">
|
||||
Navidrome Auto-Ingest Engine v1.2.0
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-b border-slate-800 bg-slate-900/50 px-6">
|
||||
<h1 className="text-lg font-semibold text-white">{pageTitle}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isWorkbenchPage && (
|
||||
<div className="flex items-center space-x-2 rounded-full border border-slate-700/50 bg-slate-800/50 px-3 py-1.5">
|
||||
{connState === 'connected' ? (
|
||||
<>
|
||||
<span className="relative flex h-2.5 w-2.5">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
|
||||
</span>
|
||||
<Wifi className="h-4 w-4 text-emerald-400" />
|
||||
<span className="text-xs font-medium text-emerald-400">
|
||||
实时连接中 (WS)
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 animate-spin text-amber-400" />
|
||||
<span className="text-xs font-medium text-amber-400">轮询兜底中</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-slate-400">系统状态:</span>
|
||||
<span
|
||||
className={`rounded px-2.5 py-1 text-xs font-semibold ${
|
||||
taskState === 'unconfigured'
|
||||
? 'bg-slate-800 text-slate-400'
|
||||
: taskState === 'ready'
|
||||
? 'border border-blue-500/30 bg-blue-500/20 text-blue-400'
|
||||
: taskState === 'running'
|
||||
? 'border border-emerald-500/30 bg-emerald-500/20 text-emerald-400'
|
||||
: taskState === 'failed'
|
||||
? 'border border-rose-500/30 bg-rose-500/20 text-rose-400'
|
||||
: 'bg-slate-700 text-white'
|
||||
}`}
|
||||
>
|
||||
{taskState === 'unconfigured'
|
||||
? '未配置'
|
||||
: taskState === 'ready'
|
||||
? '已配置,待机中'
|
||||
: taskState === 'running'
|
||||
? '任务执行中'
|
||||
: taskState === 'failed'
|
||||
? '任务失败'
|
||||
: '批次完成'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main
|
||||
className={`flex-1 min-h-0 p-6 ${
|
||||
location.pathname === '/workbench'
|
||||
? 'overflow-hidden'
|
||||
: 'overflow-y-auto'
|
||||
}`}
|
||||
>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavButton({ to, icon: Icon, label }) {
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
`flex w-full items-center space-x-3 rounded-lg px-3 py-3 transition-colors ${
|
||||
isActive
|
||||
? 'bg-slate-800 text-white'
|
||||
: 'hover:bg-slate-800/50 hover:text-slate-100'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span className="font-medium">{label}</span>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
export default function Pagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPrev,
|
||||
onNext,
|
||||
summary
|
||||
}) {
|
||||
return (
|
||||
<div className="flex shrink-0 items-center justify-between border-t border-slate-800 bg-slate-900/50 px-5 py-4">
|
||||
<div className="text-xs text-slate-500">{summary}</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onPrev}
|
||||
disabled={currentPage === 1}
|
||||
className="flex items-center rounded bg-slate-800 px-3 py-1.5 text-xs text-slate-300 transition hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<ChevronLeft className="mr-1 h-4 w-4" />
|
||||
上一页
|
||||
</button>
|
||||
<div className="rounded border border-slate-800 bg-slate-950 px-3 py-1.5 font-mono text-xs text-slate-400">
|
||||
{currentPage} / {totalPages}
|
||||
</div>
|
||||
<button
|
||||
onClick={onNext}
|
||||
disabled={currentPage === totalPages}
|
||||
className="flex items-center rounded bg-slate-800 px-3 py-1.5 text-xs text-slate-300 transition hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
下一页
|
||||
<ChevronRight className="ml-1 h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user