feat: 添加 Admin Dashboard — React 19 SPA,包含 7 个页面
- Dashboard: 健康状态轮询、状态卡片、内存统计、快捷操作 - Login: 二维码展示 + 3 秒自动轮询 + 倒计时 + 登出 - Browser: 探索/搜索/用户三标签页,Feed 网格、详情面板、评论树 - Publish: 图文/视频发布表单,支持标签、可见性、定时发布 - Interactions: 点赞/取消点赞、收藏、评论、回复 + 操作日志 - API Tester: 端点选择器、请求体编辑器、cURL 生成、响应查看、历史记录 - Settings: Token 配置、服务器 URL 设置 后端改动: - app.ts: 生产环境提供 dist/web/ 静态文件服务 + SPA fallback - Dockerfile: 添加 web 构建阶段 - package.json: 添加 build:web、build:all、dev:web 脚本 技术栈: React 19 + TypeScript + Vite 6 + Tailwind CSS(暗色主题) 产物: 85.5 KB gzip JS + 4 KB gzip CSS,零重型依赖
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
||||
|
||||
interface AuthState {
|
||||
token: string;
|
||||
serverUrl: string;
|
||||
setToken: (t: string) => void;
|
||||
setServerUrl: (u: string) => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthState | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [token, setTokenState] = useState(() => localStorage.getItem('smcp_token') || '');
|
||||
const [serverUrl, setServerUrlState] = useState(() => localStorage.getItem('smcp_server_url') || '');
|
||||
|
||||
const setToken = useCallback((t: string) => {
|
||||
localStorage.setItem('smcp_token', t);
|
||||
setTokenState(t);
|
||||
}, []);
|
||||
|
||||
const setServerUrl = useCallback((u: string) => {
|
||||
localStorage.setItem('smcp_server_url', u);
|
||||
setServerUrlState(u);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ token, serverUrl, setToken, setServerUrl }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth(): AuthState {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
||||
|
||||
type ToastType = 'success' | 'error' | 'info' | 'warning';
|
||||
|
||||
interface Toast {
|
||||
id: number;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
toasts: Toast[];
|
||||
toast: (type: ToastType, message: string) => void;
|
||||
dismiss: (id: number) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastState | null>(null);
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
||||
const dismiss = useCallback((id: number) => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
}, []);
|
||||
|
||||
const toast = useCallback(
|
||||
(type: ToastType, message: string) => {
|
||||
const id = nextId++;
|
||||
setToasts((prev) => [...prev, { id, type, message }]);
|
||||
setTimeout(() => dismiss(id), 4000);
|
||||
},
|
||||
[dismiss],
|
||||
);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ toasts, toast, dismiss }}>
|
||||
{children}
|
||||
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm">
|
||||
{toasts.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
onClick={() => dismiss(t.id)}
|
||||
className={`px-4 py-3 rounded-lg shadow-lg cursor-pointer text-sm font-medium transition-all ${
|
||||
t.type === 'success'
|
||||
? 'bg-dark-success/20 text-dark-success border border-dark-success/30'
|
||||
: t.type === 'error'
|
||||
? 'bg-dark-danger/20 text-dark-danger border border-dark-danger/30'
|
||||
: t.type === 'warning'
|
||||
? 'bg-dark-warning/20 text-dark-warning border border-dark-warning/30'
|
||||
: 'bg-dark-accent/20 text-dark-accent border border-dark-accent/30'
|
||||
}`}
|
||||
>
|
||||
{t.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useToast(): ToastState {
|
||||
const ctx = useContext(ToastContext);
|
||||
if (!ctx) throw new Error('useToast must be used within ToastProvider');
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user