Add MusicWorkshop application

This commit is contained in:
liumangmang
2026-04-30 14:34:28 +08:00
parent 4cb403c956
commit 796f19990f
62 changed files with 21614 additions and 2168 deletions
+143
View File
@@ -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>
);
}
+36
View File
@@ -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>
);
}