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:
2026-03-01 13:58:55 +08:00
parent 6d35387e2b
commit c6a8177718
51 changed files with 5665 additions and 1 deletions
+110
View File
@@ -0,0 +1,110 @@
import { apiFetch } from './client';
import type {
LoginStatus,
QRCodeResult,
Feed,
FeedDetail,
UserProfile,
SearchFilters,
HealthResponse,
ApiResponse,
PublishResult,
InteractionResult,
CommentResult,
} from './types';
// Health (no auth required)
export const getHealth = () =>
apiFetch<HealthResponse>('/health');
// Login
export const getLoginStatus = () =>
apiFetch<ApiResponse<LoginStatus>>('/api/xhs/login/status');
export const getLoginQRCode = () =>
apiFetch<ApiResponse<QRCodeResult>>('/api/xhs/login/qrcode');
export const deleteCookies = () =>
apiFetch<ApiResponse<{ message: string }>>('/api/xhs/login/cookies', { method: 'DELETE' });
// Feeds
export const listFeeds = () =>
apiFetch<ApiResponse<Feed[]>>('/api/xhs/feeds');
export const searchFeeds = (keyword: string, filters?: SearchFilters) =>
apiFetch<ApiResponse<Feed[]>>('/api/xhs/search', {
method: 'POST',
body: JSON.stringify({ keyword, filters }),
});
export const getFeedDetail = (feedId: string, xsecToken: string, loadAllComments = false) =>
apiFetch<ApiResponse<FeedDetail>>('/api/xhs/feeds/detail', {
method: 'POST',
body: JSON.stringify({ feed_id: feedId, xsec_token: xsecToken, load_all_comments: loadAllComments }),
});
// User
export const getUserProfile = (userId: string, xsecToken: string) =>
apiFetch<ApiResponse<UserProfile>>('/api/xhs/user/profile', {
method: 'POST',
body: JSON.stringify({ user_id: userId, xsec_token: xsecToken }),
});
// Publish
export const publishImage = (data: {
title: string;
content: string;
images: string[];
tags?: string[];
schedule_at?: string;
is_original?: boolean;
visibility?: 'public' | 'private' | 'friends';
}) =>
apiFetch<ApiResponse<PublishResult>>('/api/xhs/publish/image', {
method: 'POST',
body: JSON.stringify(data),
});
export const publishVideo = (data: {
title: string;
content: string;
video: string;
tags?: string[];
schedule_at?: string;
visibility?: 'public' | 'private' | 'friends';
}) =>
apiFetch<ApiResponse<PublishResult>>('/api/xhs/publish/video', {
method: 'POST',
body: JSON.stringify(data),
});
// Interactions
export const postComment = (feedId: string, xsecToken: string, content: string) =>
apiFetch<ApiResponse<CommentResult>>('/api/xhs/comment', {
method: 'POST',
body: JSON.stringify({ feed_id: feedId, xsec_token: xsecToken, content }),
});
export const replyComment = (data: {
feed_id: string;
xsec_token: string;
content: string;
comment_id?: string;
user_id?: string;
}) =>
apiFetch<ApiResponse<CommentResult>>('/api/xhs/comment/reply', {
method: 'POST',
body: JSON.stringify(data),
});
export const toggleLike = (feedId: string, xsecToken: string, unlike = false) =>
apiFetch<ApiResponse<InteractionResult>>('/api/xhs/like', {
method: 'POST',
body: JSON.stringify({ feed_id: feedId, xsec_token: xsecToken, unlike }),
});
export const toggleFavorite = (feedId: string, xsecToken: string, unfavorite = false) =>
apiFetch<ApiResponse<InteractionResult>>('/api/xhs/favorite', {
method: 'POST',
body: JSON.stringify({ feed_id: feedId, xsec_token: xsecToken, unfavorite }),
});