mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 07:26:36 +08:00
[重构] 前端改造v1
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function EmptyState({ title, description }: EmptyStateProps) {
|
||||
return (
|
||||
<div className='rounded-2xl border border-dashed border-white/10 bg-white/5 px-5 py-6 text-sm'>
|
||||
<p className='text-base font-semibold text-white'>{title}</p>
|
||||
<p className='mt-2 leading-6 text-[var(--foreground-secondary)]'>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
interface ErrorStateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function ErrorState({ title, description }: ErrorStateProps) {
|
||||
return (
|
||||
<div className='rounded-2xl border border-rose-400/20 bg-rose-500/10 px-5 py-6 text-sm'>
|
||||
<p className='text-base font-semibold text-rose-100'>{title}</p>
|
||||
<p className='mt-2 leading-6 text-rose-100/80'>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AppCard } from '@/components/ui/app-card';
|
||||
import { StatusBadge } from '@/components/ui/status-badge';
|
||||
|
||||
interface FeaturePlaceholderProps {
|
||||
title: string;
|
||||
description: string;
|
||||
milestones: string[];
|
||||
}
|
||||
|
||||
export function FeaturePlaceholder({
|
||||
title,
|
||||
description,
|
||||
milestones,
|
||||
}: FeaturePlaceholderProps) {
|
||||
return (
|
||||
<AppCard
|
||||
title={title}
|
||||
description={description}
|
||||
action={<StatusBadge label='阶段 1 骨架完成' variant='success' />}
|
||||
>
|
||||
<ul className='space-y-3 text-sm leading-6 text-[var(--foreground-secondary)]'>
|
||||
{milestones.map((item) => (
|
||||
<li key={item} className='flex gap-3'>
|
||||
<span className='mt-2 h-2 w-2 shrink-0 rounded-full bg-sky-400' />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</AppCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
interface LoadingStateProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LoadingState({ className }: LoadingStateProps) {
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<div className='h-4 w-32 animate-pulse rounded bg-white/10' />
|
||||
<div className='h-24 animate-pulse rounded-2xl bg-white/5' />
|
||||
<div className='h-24 animate-pulse rounded-2xl bg-white/5' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { DashboardSidebar } from '@/components/layout/dashboard-sidebar';
|
||||
import { DashboardTopbar } from '@/components/layout/dashboard-topbar';
|
||||
|
||||
interface DashboardShellProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function DashboardShell({ children }: DashboardShellProps) {
|
||||
return (
|
||||
<div className='flex min-h-screen bg-transparent text-[var(--foreground-primary)]'>
|
||||
<DashboardSidebar />
|
||||
<div className='flex min-w-0 flex-1 flex-col'>
|
||||
<DashboardTopbar />
|
||||
<main className='flex-1 px-4 py-6 md:px-8 md:py-8'>{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { dashboardNavigation } from '@/lib/constants/navigation';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { isPathActive } from '@/lib/utils/navigation';
|
||||
import { useAppShellStore } from '@/store/app-shell';
|
||||
|
||||
export function DashboardSidebar() {
|
||||
const pathname = usePathname();
|
||||
const currentPath = pathname ?? '/';
|
||||
const isSidebarCollapsed = useAppShellStore((state) => state.isSidebarCollapsed);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'sticky top-0 hidden h-screen shrink-0 border-r border-[var(--border-default)] bg-[var(--surface-panel)]/95 px-3 py-6 backdrop-blur lg:block',
|
||||
isSidebarCollapsed ? 'w-24' : 'w-72',
|
||||
)}
|
||||
>
|
||||
<div className='flex h-full flex-col gap-6'>
|
||||
<div className='flex items-center gap-3 rounded-2xl border border-[var(--border-default)] bg-white/5 px-3 py-3'>
|
||||
<div className='flex h-11 w-11 items-center justify-center rounded-2xl bg-sky-400/15 text-lg font-semibold text-sky-200'>
|
||||
AF
|
||||
</div>
|
||||
{!isSidebarCollapsed ? (
|
||||
<div>
|
||||
<p className='text-sm font-semibold text-white'>ATSFlare</p>
|
||||
<p className='text-xs text-[var(--foreground-secondary)]'>控制面新版工程</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<nav className='flex-1 space-y-2'>
|
||||
{dashboardNavigation.map((item) => {
|
||||
const active = isPathActive(currentPath, item.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-start gap-3 rounded-2xl border px-3 py-3 transition-colors',
|
||||
active
|
||||
? 'border-sky-300/30 bg-sky-400/10 text-white'
|
||||
: 'border-transparent text-[var(--foreground-secondary)] hover:border-[var(--border-default)] hover:bg-white/5 hover:text-white',
|
||||
)}
|
||||
>
|
||||
<span className='mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-white/5 text-xs font-semibold'>
|
||||
{item.shortLabel.slice(0, 1)}
|
||||
</span>
|
||||
{!isSidebarCollapsed ? (
|
||||
<span className='min-w-0'>
|
||||
<span className='block text-sm font-medium'>{item.label}</span>
|
||||
<span className='mt-1 block text-xs leading-5 text-inherit/80'>
|
||||
{item.description}
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{!isSidebarCollapsed ? (
|
||||
<div className='rounded-2xl border border-emerald-400/15 bg-emerald-500/10 px-4 py-4 text-xs leading-6 text-emerald-100/90'>
|
||||
阶段 1 已完成目录骨架、静态导出与基础布局初始化。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { getCurrentNavigationItem } from '@/lib/utils/navigation';
|
||||
import { publicEnv } from '@/lib/env/public-env';
|
||||
import { useAppShellStore } from '@/store/app-shell';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export function DashboardTopbar() {
|
||||
const pathname = usePathname();
|
||||
const currentPath = pathname ?? '/';
|
||||
const toggleSidebar = useAppShellStore((state) => state.toggleSidebar);
|
||||
const currentItem = getCurrentNavigationItem(currentPath);
|
||||
|
||||
return (
|
||||
<header className='sticky top-0 z-10 border-b border-[var(--border-default)] bg-[var(--surface-panel)]/75 px-4 py-4 backdrop-blur md:px-8'>
|
||||
<div className='flex flex-col gap-4 md:flex-row md:items-center md:justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={toggleSidebar}
|
||||
className='inline-flex h-11 w-11 items-center justify-center rounded-2xl border border-[var(--border-default)] bg-white/5 text-lg text-white transition hover:bg-white/10'
|
||||
aria-label='切换侧边栏'
|
||||
>
|
||||
≡
|
||||
</button>
|
||||
<div>
|
||||
<p className='text-xs uppercase tracking-[0.24em] text-[var(--foreground-secondary)]'>当前模块</p>
|
||||
<h2 className='text-lg font-semibold text-white'>
|
||||
{currentItem?.label ?? 'ATSFlare 控制台'}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-3 text-sm text-[var(--foreground-secondary)]'>
|
||||
<span className='rounded-full border border-[var(--border-default)] px-3 py-1.5'>
|
||||
静态导出模式
|
||||
</span>
|
||||
<span className='rounded-full border border-[var(--border-default)] px-3 py-1.5'>
|
||||
版本 {publicEnv.appVersion}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, description }: PageHeaderProps) {
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
<p className='text-sm font-medium uppercase tracking-[0.24em] text-sky-300'>ATSFlare</p>
|
||||
<div className='space-y-2'>
|
||||
<h1 className='text-3xl font-semibold tracking-tight text-white'>{title}</h1>
|
||||
<p className='max-w-3xl text-sm leading-7 text-[var(--foreground-secondary)]'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Link from 'next/link';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { publicEnv } from '@/lib/env/public-env';
|
||||
|
||||
interface PublicShellProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function PublicShell({ children }: PublicShellProps) {
|
||||
return (
|
||||
<div className='flex min-h-screen items-center justify-center px-4 py-12'>
|
||||
<div className='w-full max-w-3xl rounded-[var(--radius-xl)] border border-[var(--border-default)] bg-[var(--surface-panel)]/85 p-8 shadow-[var(--shadow-soft)] backdrop-blur'>
|
||||
<div className='mb-8 flex flex-col gap-4 border-b border-[var(--border-default)] pb-6 md:flex-row md:items-end md:justify-between'>
|
||||
<div>
|
||||
<p className='text-sm font-medium uppercase tracking-[0.24em] text-sky-300'>ATSFlare</p>
|
||||
<h1 className='mt-2 text-3xl font-semibold text-white'>管理端改造进行中</h1>
|
||||
<p className='mt-2 text-sm leading-6 text-[var(--foreground-secondary)]'>
|
||||
认证流程将在阶段 2 接入。当前页面用于承载入口骨架与路由占位。
|
||||
</p>
|
||||
</div>
|
||||
<div className='text-sm text-[var(--foreground-secondary)]'>
|
||||
<span className='rounded-full border border-[var(--border-default)] px-3 py-1.5'>
|
||||
{publicEnv.appVersion}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-6'>{children}</div>
|
||||
|
||||
<div className='mt-8 border-t border-[var(--border-default)] pt-6 text-sm text-[var(--foreground-secondary)]'>
|
||||
<Link href='/' className='text-sky-300 transition hover:text-sky-200'>
|
||||
返回新版总览
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
interface AppProvidersProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AppProviders({ children }: AppProvidersProps) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
staleTime: 30_000,
|
||||
},
|
||||
mutations: {
|
||||
retry: 0,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
interface AppCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
title?: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
export function AppCard({
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: AppCardProps) {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'rounded-[var(--radius-xl)] border border-[var(--border-default)] bg-[var(--surface-card)]/90 shadow-[var(--shadow-soft)] backdrop-blur',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{(title || description || action) && (
|
||||
<header className='flex flex-col gap-3 border-b border-[var(--border-default)] px-6 py-5 md:flex-row md:items-start md:justify-between'>
|
||||
<div className='space-y-1'>
|
||||
{title ? <h2 className='text-lg font-semibold text-white'>{title}</h2> : null}
|
||||
{description ? (
|
||||
<p className='text-sm leading-6 text-[var(--foreground-secondary)]'>{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{action ? <div className='shrink-0'>{action}</div> : null}
|
||||
</header>
|
||||
)}
|
||||
<div className='px-6 py-5'>{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
const variantClasses = {
|
||||
success: 'border-emerald-400/25 bg-emerald-500/15 text-emerald-200',
|
||||
warning: 'border-amber-400/25 bg-amber-500/15 text-amber-200',
|
||||
danger: 'border-rose-400/25 bg-rose-500/15 text-rose-200',
|
||||
info: 'border-sky-400/25 bg-sky-500/15 text-sky-200',
|
||||
} as const;
|
||||
|
||||
interface StatusBadgeProps {
|
||||
label: string;
|
||||
variant?: keyof typeof variantClasses;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StatusBadge({
|
||||
label,
|
||||
variant = 'info',
|
||||
className,
|
||||
}: StatusBadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium tracking-wide',
|
||||
variantClasses[variant],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user