diff --git a/frontend-v2/src/App.tsx b/frontend-v2/src/App.tsx
index 73a295c..278f5a5 100644
--- a/frontend-v2/src/App.tsx
+++ b/frontend-v2/src/App.tsx
@@ -10,17 +10,61 @@ import { ImBotHub } from '@/pages/front/ImBotHub';
import { AppServices } from '@/pages/front/AppServices';
import { DashboardPage } from '@/pages/front/DashboardPage';
import { ProfilePage } from '@/pages/front/ProfilePage';
+import { MarketPage } from '@/pages/front/MarketPage';
+import { NotificationsPage } from '@/pages/front/NotificationsPage';
+import { EvolutionPage } from '@/pages/front/EvolutionPage';
import { ComingSoon } from '@/components/common/ComingSoon';
import {
- Zap,
Bot,
Puzzle,
Workflow,
Shield,
Radio,
- Bell,
- Sparkles,
} from 'lucide-react';
+import { useCurrentUser } from '@/hooks/useApi';
+import { AdminLayout } from '@/pages/admin/AdminLayout';
+import { AdminDashboard } from '@/pages/admin/AdminDashboard';
+import {
+ UsersPage,
+ RolesPage,
+ TenantPage,
+ ModelsPage,
+ KnowledgeAdminPage,
+ AssetsPage,
+ StoragePage,
+ AgentsAdminPage,
+ McpPage,
+ McpMarketPage,
+ SkillsAdminPage,
+ WorkflowAdminPage,
+ AutomationPage,
+ AppsAdminPage,
+ CodeExecPage,
+ DistributionsPage,
+ AdsPage,
+ MarketAdminPage,
+ PaymentPage,
+ BillingPage,
+ MembershipPage,
+ ServicesPage,
+ LogsPage,
+ DeveloperPage,
+ AnalyticsPage,
+} from '@/pages/admin/AdminPlaceholders';
+
+// 管理员守卫:复用 frontend-v2 的 auth_token 认证链,role=admin|super_admin 放行。
+function AdminGuard({ children }: { children: React.ReactNode }) {
+ const { data: user, isLoading } = useCurrentUser();
+ const token = localStorage.getItem('auth_token');
+
+ if (!token) return ;
+ if (isLoading) {
+ return
加载中…
;
+ }
+ const isAdmin = user?.role === 'admin' || user?.role === 'super_admin';
+ if (!isAdmin) return ;
+ return <>{children}>;
+}
function App() {
return (
@@ -35,21 +79,7 @@ function App() {
} />
} />
{/* 占位页:真实功能开发中,统一 amber 金暖白「敬请期待」态 */}
-
- }
- />
+ } />
} />
} />
-
- }
- />
-
- }
- />
+ } />
+ } />
+
+ {/* Admin 后台:管理员守卫 + AdminLayout 外壳 + 嵌套二级路由 */}
+
+
+
+ }
+ >
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
} />
diff --git a/frontend-v2/src/api/client.ts b/frontend-v2/src/api/client.ts
index 05174c2..e4eaa65 100644
--- a/frontend-v2/src/api/client.ts
+++ b/frontend-v2/src/api/client.ts
@@ -31,6 +31,12 @@ async function data(path: string, options: RequestInit = {}): Promise {
return env.data;
}
+// im_bot_user.py 专用 envelope:`{success, data, ...}`(无 code 字段)。
+async function successData(path: string, options: RequestInit = {}): Promise {
+ const env = await request<{ success: boolean; data: T }>(path, options);
+ return env.data;
+}
+
// ─── Auth ───────────────────────────────────────────────────────────────────────
export const authApi = {
@@ -46,7 +52,7 @@ export const authApi = {
body: JSON.stringify(data),
}),
- me: () => request<{ id: string; username: string; email: string; avatar_url: string }>('/auth/me'),
+ me: () => request<{ id: string; username: string; email: string; avatar_url: string; role?: string }>('/auth/me'),
logout: () => request('/auth/logout', { method: 'POST' }),
@@ -56,13 +62,19 @@ export const authApi = {
// ─── Chat ────────────────────────────────────────────────────────────────────────
+// 后端契约 (chat.py):list/messages/create 均返回 {code,data:[...]}/{code,data:{...}}
+// 列表字段:id,title,model_id,status,pinned,source,created_at,updated_at(无 message_count)
+// 消息字段:id,conversation_id,role,content,model_id,created_at,token_used
export interface Conversation {
id: string;
title: string;
- model: string;
+ model_id: string;
+ status?: string;
+ pinned?: boolean;
+ source?: string;
created_at: string;
updated_at: string;
- message_count: number;
+ message_count?: number; // 后端未返回,前端可选兼容
}
export interface ChatMessage {
@@ -70,24 +82,25 @@ export interface ChatMessage {
conversation_id: string;
role: 'user' | 'assistant';
content: string;
- model: string;
+ model_id: string;
created_at: string;
+ token_used?: number;
}
export const chatApi = {
- listConversations: () => request('/chat/conversations'),
+ listConversations: () => data('/chat/conversations'),
- createConversation: (data: { title?: string; model?: string }) =>
- request('/chat/conversations', {
+ createConversation: (payload: { title?: string; model?: string }) =>
+ data('/chat/conversations', {
method: 'POST',
- body: JSON.stringify(data),
+ body: JSON.stringify(payload),
}),
deleteConversation: (id: string) =>
request(`/chat/conversations/${id}`, { method: 'DELETE' }),
getMessages: (convId: string) =>
- request(`/chat/conversations/${convId}/messages`),
+ data(`/chat/conversations/${convId}/messages`),
sendMessage: (convId: string, data: { content: string; model?: string }) =>
request(`/chat/conversations/${convId}/messages`, {
@@ -130,23 +143,41 @@ export const chatApi = {
// ─── Knowledge ───────────────────────────────────────────────────────────────────
+// 后端契约 (knowledge.py):
+// list 返回 {code,data:{bases:[...],storage_used,storage_limit,membership_level}}
+// create 返回 {code,data:{id,name,description,created_at}}(缺 document_count/status/updated_at)
export interface KnowledgeBase {
id: string;
name: string;
description: string;
document_count: number;
- status: 'active' | 'building' | 'error';
+ status: 'active' | 'building' | 'error' | string;
created_at: string;
updated_at: string;
}
-export const knowledgeApi = {
- listBases: () => request('/knowledge/bases'),
+export interface KnowledgeBaseList {
+ bases: KnowledgeBase[];
+ storage_used: number;
+ storage_limit: number;
+ membership_level: number;
+}
- createBase: (data: { name: string; description?: string }) =>
- request('/knowledge/bases', {
+// 创建返回的子集(字段以 create 真实返回为准,缺失字段前端填充默认值)
+export interface KnowledgeBaseCreated {
+ id: string;
+ name: string;
+ description: string;
+ created_at: string;
+}
+
+export const knowledgeApi = {
+ listBases: () => data('/knowledge/bases'),
+
+ createBase: (payload: { name: string; description?: string }) =>
+ data('/knowledge/bases', {
method: 'POST',
- body: JSON.stringify(data),
+ body: JSON.stringify(payload),
}),
deleteBase: (id: string) =>
@@ -164,45 +195,169 @@ export const knowledgeApi = {
};
// ─── Market / Apps ──────────────────────────────────────────────────────────────
+// 后端契约:app/api/market.py → GET /apps (list_apps) 返回
+// { code:0, data:{ total, page, page_size, items:[MarketApp] } }
+// 字段以 list_apps 真实 item 为准(见 #993 重写)。
+
+export type MarketAppType = 'standalone' | 'embedded' | 'skill' | 'agent' | 'workflow';
+export type MarketAppSource = 'official' | 'community' | 'third_party';
+export type MarketAppStatus = 'draft' | 'pending' | 'published' | 'rejected' | 'offline';
export interface MarketApp {
id: string;
name: string;
description: string;
- icon: string;
- category: string;
- status: 'enabled' | 'disabled';
- type: 'standalone' | 'embedded';
- downloads: number;
+ icon: string | null;
+ type: MarketAppType;
+ source: MarketAppSource;
+ publisher_id: string;
+ publisher_name: string;
+ is_member_free: boolean;
+ join_promotion: boolean;
+ status: MarketAppStatus;
+ price: number;
+ install_count: number;
+ rating_avg: number;
+ review_count: number;
+ created_at: string;
+ capability: string | null;
+ category: string | null;
+ tags: string[];
+ installed: boolean;
+ version: string | null;
+ security_scan: string | null;
+ verified: boolean;
+ dependencies: string[];
+}
+
+// 详情页用:list 字段 + 可能更丰富的描述/截图/长文
+export interface MarketAppDetail extends MarketApp {
+ long_description?: string;
+ screenshots?: string[];
+ homepage_url?: string | null;
+ updated_at?: string | null;
+}
+
+export interface MarketReview {
+ id: string;
+ app_id: string;
+ user_id: string;
+ user_name: string;
rating: number;
+ comment: string;
+ created_at: string;
+}
+
+export interface MarketReviewList {
+ total: number;
+ page: number;
+ page_size: number;
+ items: MarketReview[];
+}
+
+export interface MarketAd {
+ id: string;
+ title: string;
+ image_url: string;
+ url: string;
+}
+
+export interface MarketAppList {
+ total: number;
+ page: number;
+ page_size: number;
+ items: MarketApp[];
+}
+
+export type MarketSort = 'latest' | 'popular' | 'rating';
+
+// 后端契约 (market.py /my-apps):{code,data:[{id,app_id,name,icon,type,price,purchased_at}]}
+// 与 MarketApp 字段差距大(无 installed/status/description/version),单独建模。
+export interface MyApp {
+ id: string;
+ app_id: string;
+ name: string;
+ icon: string | null;
+ type: MarketAppType;
+ price: number;
+ purchased_at: string | null;
}
export const marketApi = {
- listApps: () => request('/market/apps'),
+ // 分页 + 筛选 + 排序 + 搜索,解包 {data:{total,page,page_size,items}}
+ listApps: (params: {
+ type?: MarketAppType;
+ source?: MarketAppSource;
+ capability?: string;
+ search?: string;
+ sort?: MarketSort;
+ page?: number;
+ page_size?: number;
+ } = {}) => {
+ const qs = new URLSearchParams();
+ if (params.type) qs.set('type', params.type);
+ if (params.source) qs.set('source', params.source);
+ if (params.capability) qs.set('capability', params.capability);
+ if (params.search) qs.set('search', params.search);
+ if (params.sort) qs.set('sort', params.sort);
+ if (params.page) qs.set('page', String(params.page));
+ if (params.page_size) qs.set('page_size', String(params.page_size));
+ const q = qs.toString();
+ return data(`/apps${q ? `?${q}` : ''}`);
+ },
- getMyApps: () => request('/market/my-apps'),
+ getApp: (appId: string) =>
+ data(`/apps/${appId}`),
+ getMyApps: () =>
+ data('/my-apps'),
+
+ getReviews: (appId: string, params: { page?: number; page_size?: number } = {}) => {
+ const qs = new URLSearchParams();
+ if (params.page) qs.set('page', String(params.page));
+ if (params.page_size) qs.set('page_size', String(params.page_size));
+ const q = qs.toString();
+ return data(`/reviews/${appId}${q ? `?${q}` : ''}`);
+ },
+
+ getAds: () =>
+ data('/ads'),
+
+ // ─── 写操作(后续迭代启用)───
installApp: (appId: string) =>
- request(`/market/apps/${appId}/install`, { method: 'POST' }),
+ request(`/apps/${appId}/install`, { method: 'POST' }),
uninstallApp: (appId: string) =>
- request(`/market/apps/${appId}/uninstall`, { method: 'DELETE' }),
+ request(`/apps/${appId}/uninstall`, { method: 'DELETE' }),
- getAds: () => request<{ id: string; title: string; image_url: string; url: string }[]>('/market/ads'),
+ purchaseApp: (appId: string) =>
+ request(`/apps/${appId}/purchase`, { method: 'POST' }),
+
+ reviewApp: (appId: string, payload: { rating: number; comment: string }) =>
+ request(`/apps/${appId}/review`, {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ }),
};
// ─── IM Bot ──────────────────────────────────────────────────────────────────────
+// 后端契约 (im_bot_user.py /status):{success,data:{connected,status,configured,baseurl,ilink_bot_id,username,connected_at}}
+// 注意:data 内无 message_count / last_active(前端页面已用可选默认值兼容)。
export interface ImBotStatus {
- status: 'connected' | 'disconnected' | 'expired';
- ilink_bot_id: string;
- connected_at: string;
- last_active: string;
- message_count: number;
+ status: 'connected' | 'disconnected' | 'expired' | string;
+ ilink_bot_id: string | null;
+ connected_at: string | null;
+ last_active?: string | null;
+ message_count?: number;
+ connected?: boolean;
+ configured?: boolean;
+ username?: string | null;
+ baseurl?: string | null;
}
export const imBotApi = {
- getStatus: () => request('/im-bot/status'),
+ getStatus: () => successData('/im-bot/status'),
getQRCode: () => request<{ qr_url: string; expire_at: string }>('/im-bot/qrcode'),
@@ -407,3 +562,255 @@ export const tokenApi = {
getBalance: () => data('/token/balance'),
};
+
+// ─── Notifications 消息通知 ──────────────────────────────────────────────────────
+// 后端契约 (notifications.py):全部 {code,data,message}
+// GET / → { total, page, page_size, items:[NotificationItem] }
+// PUT /{id}/read → {code,message}; PUT /read-all?type= → {code,data:{count},message}
+// GET /unread-count → {code,data:{count},message}
+
+export interface NotificationItem {
+ id: string;
+ type: string | null;
+ title: string;
+ content: string;
+ is_read: boolean;
+ related_id: string | null;
+ related_type: string | null;
+ created_at: string | null;
+}
+
+export interface NotificationList {
+ total: number;
+ page: number;
+ page_size: number;
+ items: NotificationItem[];
+}
+
+export const notificationsApi = {
+ list: (params: { type?: string; is_read?: boolean; page?: number; page_size?: number } = {}) => {
+ const qs = new URLSearchParams();
+ if (params.type) qs.set('type', params.type);
+ if (params.is_read !== undefined) qs.set('is_read', String(params.is_read));
+ if (params.page) qs.set('page', String(params.page));
+ if (params.page_size) qs.set('page_size', String(params.page_size));
+ const q = qs.toString();
+ return data(`/notifications/${q ? `?${q}` : ''}`);
+ },
+
+ markRead: (id: string) =>
+ request(`/notifications/${id}/read`, { method: 'PUT' }),
+
+ markAllRead: (type?: string) =>
+ request(`/notifications/read-all${type ? `?type=${encodeURIComponent(type)}` : ''}`, { method: 'PUT' }),
+
+ unreadCount: () =>
+ data<{ count: number }>('/notifications/unread-count'),
+};
+
+// ─── Admin 后台 ──────────────────────────────────────────────────────────────────
+// 后端契约 (app/api/admin/*):绝大多数端点返回统一信封 {code,data,message},
+// 但 dashboard.py 的 `/admin/dashboard/stats` 返回**裸 dict**(无 code/data 包裹),
+// `/admin/dashboard/daily-stats` 返回 {code,data:{days,data}}。
+// 因此 adminApi 不复用 data(),改用 rawAdmin() 解包并按端点分别处理 envelope。
+
+export interface AdminDashboardStats {
+ total_users: number;
+ active_users: number;
+ total_models: number;
+ enabled_models: number;
+ total_conversations: number;
+ active_conversations: number;
+ valid_conversations: number;
+ total_knowledge_bases: number;
+ total_documents: number;
+ today_orders: number;
+ today_subscription_orders: number;
+ today_tokens: number;
+ week_tokens: number;
+ total_tokens: number;
+ today_conversations: number;
+ server_time: string;
+}
+
+export interface AdminDailyStat {
+ date: string;
+ tokens: number;
+}
+
+export interface AdminDailyStats {
+ days: string[];
+ data: AdminDailyStat[];
+}
+
+// 统一信封解包(与 data() 相同语义,但显式标记 admin 域,便于后续差异演进)
+async function adminData(path: string, options: RequestInit = {}): Promise {
+ const env = await request<{ code: number; data: T; message: string }>(path, options);
+ return env.data;
+}
+
+export const adminApi = {
+ // /stats 裸 dict(无 code/data 包裹)→ 走 raw request,直接返回 json
+ getStats: () => request('/admin/dashboard/stats'),
+
+ // /daily-stats 标准信封 {code,data:{days,data}}
+ getDailyStats: () => adminData('/admin/dashboard/daily-stats'),
+};
+
+// ─── Evolution AI 自进化 ────────────────────────────────────────────────────────
+// 后端契约 (evolution.py + services/evolution_service.py):全部 {code,data,...}
+// 技能状态 state: main | staging | baby | frozen
+// 事件类型 event_type: created | edited | promoted | frozen | ...(字符串透传)
+
+export type EvolutionSkillState = 'main' | 'staging' | 'baby' | 'frozen';
+
+export interface EvolutionSkill {
+ id: string;
+ skill_name: string;
+ description: string;
+ tags: string[];
+ category: string;
+ state: EvolutionSkillState;
+ use_count: number;
+ version: number;
+ last_used_at: string | null;
+ created_at: string | null;
+ updated_at: string | null;
+ skill_md_content?: string | null; // 仅 detail 返回
+}
+
+export interface EvolutionEvent {
+ id: string;
+ skill_name: string;
+ event_type: string;
+ description: string;
+ version_before: number | null;
+ version_after: number | null;
+ created_at: string | null;
+}
+
+export interface EvolutionTrajectory {
+ id: string;
+ conversation_id: string;
+ file_path: string | null;
+ status: string;
+ atom_count: number | null;
+ token_used: number | null;
+ created_at: string | null;
+ processed_at: string | null;
+ weightscore?: number;
+ atom_id?: string | null;
+}
+
+export interface EvolutionSkillTree {
+ categories: { name: string; skills: EvolutionSkill[] }[];
+ total_count: number;
+ main_count: number;
+ baby_count: number;
+ staging_count: number;
+ frozen_count: number;
+}
+
+export interface EvolutionStatistics {
+ ranking: { skill_name: string; category: string; use_count: number; state: string; last_used_at: string | null }[];
+ total_use_count: number;
+ category_distribution: { category: string; count: number }[];
+ daily_trend: { date: string; count: number }[];
+ low_use_skills: EvolutionSkill[];
+}
+
+export interface EvolutionDimension {
+ label: string;
+ value: number;
+ maxValue: number;
+}
+
+export interface EvolutionProfile {
+ level: number;
+ level_title: string;
+ total_skills: number;
+ active_skills: number;
+ staging_skills: number;
+ baby_skills: number;
+ total_use_count: number;
+ dimensions: EvolutionDimension[];
+ tags: string[];
+ growth_suggestions: string[];
+}
+
+export interface EvolutionPipelineStatus {
+ scheduler_running: boolean;
+ watcher_status: string;
+ pipeline_status: string;
+ discovered_count: number;
+ done_count: number;
+ total_skills: number;
+ total_trajectories: number;
+}
+
+export interface EvolutionWatcherStatus {
+ user_id: string;
+ scheduler_running: boolean;
+ watcher_status: string;
+ pipeline_status: string;
+ pipeline_processed: number;
+ watcher_error: string | null;
+ skill_count: number;
+ trajectory_count: number;
+}
+
+export interface EvolutionReport {
+ id: string;
+ report_type: string;
+ period_start: string | null;
+ period_end: string | null;
+ created_at: string | null;
+}
+
+export interface EvolutionReportDetail extends EvolutionReport {
+ user_id: string;
+ report_data: Record;
+}
+
+export const evolutionApi = {
+ // 技能管理
+ listSkills: (params: { state?: string; keyword?: string; sort_by?: string; order?: string } = {}) => {
+ const qs = new URLSearchParams();
+ if (params.state) qs.set('state', params.state);
+ if (params.keyword) qs.set('keyword', params.keyword);
+ if (params.sort_by) qs.set('sort_by', params.sort_by);
+ if (params.order) qs.set('order', params.order);
+ const q = qs.toString();
+ return data(`/evolution/skills${q ? `?${q}` : ''}`);
+ },
+
+ getSkillDetail: (skillName: string) =>
+ data(`/evolution/skills/${encodeURIComponent(skillName)}`),
+
+ // 进化事件
+ listEvents: (params: { event_type?: string; skill_name?: string; limit?: number } = {}) => {
+ const qs = new URLSearchParams();
+ if (params.event_type) qs.set('event_type', params.event_type);
+ if (params.skill_name) qs.set('skill_name', params.skill_name);
+ if (params.limit) qs.set('limit', String(params.limit));
+ const q = qs.toString();
+ return data(`/evolution/events${q ? `?${q}` : ''}`);
+ },
+
+ // 驾驶舱
+ getSkillTree: () => data('/evolution/dashboard/skill-tree'),
+ getTimeline: (event_type?: string) =>
+ data(`/evolution/dashboard/timeline${event_type ? `?event_type=${encodeURIComponent(event_type)}` : ''}`),
+ getStatistics: () => data('/evolution/dashboard/statistics'),
+ getProfile: () => data('/evolution/dashboard/profile'),
+
+ // 流水线 / Watcher
+ getPipelineStatus: () => data('/evolution/pipeline/status'),
+ getWatcherStatus: () => data('/evolution/watcher/status'),
+
+ // 报告
+ listReports: (report_type?: string) =>
+ data(`/evolution/reports${report_type ? `?report_type=${encodeURIComponent(report_type)}` : ''}`),
+ getReportDetail: (reportId: string) =>
+ data(`/evolution/reports/${reportId}`),
+};
diff --git a/frontend-v2/src/pages/admin/AdminComingSoon.tsx b/frontend-v2/src/pages/admin/AdminComingSoon.tsx
new file mode 100644
index 0000000..7e6bf50
--- /dev/null
+++ b/frontend-v2/src/pages/admin/AdminComingSoon.tsx
@@ -0,0 +1,58 @@
+import type { LucideIcon } from 'lucide-react';
+import { Sparkles, Construction } from 'lucide-react';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+
+interface AdminComingSoonProps {
+ title: string;
+ icon: LucideIcon;
+ description?: string;
+ features?: string[];
+}
+
+/**
+ * admin 后台统一「模块开发中」占位页(amber 金暖白)。
+ * 旧 admin 共 33 个二级模块,初期仅 Dashboard 为真实数据页,
+ * 其余模块用此占位,保证后台导航点击不出现空白断崖。
+ */
+export function AdminComingSoon({ title, icon: Icon, description, features }: AdminComingSoonProps) {
+ return (
+
+
+
+
+
+
+
+
+
{title}
+
+
+ 建设中
+
+
+
+
+ {description ?? `${title} 模块正在开发中,敬请期待。上线后这里将提供完整的后台管理能力。`}
+
+
+ {features && features.length > 0 && (
+
+ {features.map((f, i) => (
+
+
+ {f}
+
+ ))}
+
+ )}
+
+
+
+ 我们将尽快为您带来 {title}
+
+
+
+
+ );
+}
diff --git a/frontend-v2/src/pages/admin/AdminDashboard.tsx b/frontend-v2/src/pages/admin/AdminDashboard.tsx
new file mode 100644
index 0000000..752316f
--- /dev/null
+++ b/frontend-v2/src/pages/admin/AdminDashboard.tsx
@@ -0,0 +1,180 @@
+import { useQuery } from '@tanstack/react-query';
+import { adminApi, type AdminDailyStat } from '@/api/client';
+import { Card, CardContent } from '@/components/ui/card';
+import { useCurrentUser } from '@/hooks/useApi';
+import { Users, Cpu, MessageSquare, BookOpen, Coins, Activity, TrendingUp } from 'lucide-react';
+
+function formatNumber(n: number | undefined): string {
+ if (n === undefined || n === null) return '—';
+ return n.toLocaleString('zh-CN');
+}
+
+function formatBjTime(iso: string | undefined): string {
+ if (!iso) return '—';
+ const d = new Date(iso);
+ if (isNaN(d.getTime())) return iso;
+ // 北京时间 = UTC+8
+ const bj = new Date(d.getTime() + 8 * 3600 * 1000);
+ const p = (x: number) => String(x).padStart(2, '0');
+ return `${bj.getUTCFullYear()}-${p(bj.getUTCMonth() + 1)}-${p(bj.getUTCDate())} ${p(bj.getUTCHours())}:${p(bj.getUTCMinutes())}:${p(bj.getUTCSeconds())}`;
+}
+
+function StatCard({
+ icon: Icon,
+ label,
+ value,
+ sub,
+}: {
+ icon: React.ReactNode;
+ label: string;
+ value: string;
+ sub?: string;
+}) {
+ return (
+
+
+
+ {value}
+ {sub && {sub}
}
+
+
+ );
+}
+
+// 简易柱状图(纯 SVG,amber 金暖白)
+function TokenBarChart({ data }: { data: AdminDailyStat[] }) {
+ if (!data || data.length === 0) {
+ return 暂无近 7 日 Token 数据
;
+ }
+ const max = Math.max(...data.map((d) => d.tokens), 1);
+ const width = 560;
+ const height = 180;
+ const gap = 16;
+ const barW = (width - gap * (data.length - 1)) / data.length;
+ return (
+
+ );
+}
+
+function MiniDonut({ enabled, total }: { enabled: number; total: number }) {
+ const pct = total > 0 ? enabled / total : 0;
+ const r = 26;
+ const c = 2 * Math.PI * r;
+ return (
+
+ );
+}
+
+export function AdminDashboard() {
+ const { data: user } = useCurrentUser();
+ const statsQuery = useQuery({
+ queryKey: ['admin', 'dashboard', 'stats'],
+ queryFn: adminApi.getStats,
+ });
+ const dailyQuery = useQuery({
+ queryKey: ['admin', 'dashboard', 'daily-stats'],
+ queryFn: adminApi.getDailyStats,
+ });
+
+ const stats = statsQuery.data;
+ const daily = dailyQuery.data?.data ?? [];
+
+ return (
+
+ {/* 页头 */}
+
+
+
数据看板
+
+ 欢迎回来,{user?.username || '管理员'}。平台实时运营概览。
+
+
+
+
服务器时间(北京)
+
{formatBjTime(stats?.server_time)}
+
+
+
+ {/* 错误态 */}
+ {statsQuery.isError && (
+
+ 看板数据加载失败:{(statsQuery.error as Error)?.message}
+
+ )}
+
+ {/* 统计卡片 6 张 */}
+
+ } label="总用户数" value={formatNumber(stats?.total_users)} sub={`活跃 ${formatNumber(stats?.active_users)}`} />
+ } label="模型总数" value={formatNumber(stats?.total_models)} sub={`启用 ${formatNumber(stats?.enabled_models)}`} />
+ } label="对话总数" value={formatNumber(stats?.total_conversations)} sub={`今日 ${formatNumber(stats?.today_conversations)}`} />
+ } label="知识库" value={formatNumber(stats?.total_knowledge_bases)} sub={`文档 ${formatNumber(stats?.total_documents)}`} />
+ } label="累计 Token" value={formatNumber(stats?.total_tokens)} sub={`今日 ${formatNumber(stats?.today_tokens)}`} />
+ } label="今日订单" value={formatNumber(stats?.today_orders)} sub={`订阅 ${formatNumber(stats?.today_subscription_orders)}`} />
+
+
+ {/* 图表区 */}
+
+
+
+
+
+
近 7 日 Token 消耗
+
+ {dailyQuery.isLoading ? (
+ 加载中…
+ ) : (
+
+ )}
+
+
+
+
+
+ 模型启用率
+
+
+
+ 启用 {formatNumber(stats?.enabled_models)} / 共 {formatNumber(stats?.total_models)}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend-v2/src/pages/admin/AdminLayout.tsx b/frontend-v2/src/pages/admin/AdminLayout.tsx
new file mode 100644
index 0000000..00b27fa
--- /dev/null
+++ b/frontend-v2/src/pages/admin/AdminLayout.tsx
@@ -0,0 +1,214 @@
+import { useState } from 'react';
+import { NavLink, Outlet, useNavigate } from 'react-router-dom';
+import { cn } from '@/lib/utils';
+import { useCurrentUser } from '@/hooks/useApi';
+import { Avatar, AvatarFallback } from '@/components/ui/avatar';
+import {
+ LayoutDashboard,
+ Users,
+ Cpu,
+ CreditCard,
+ Boxes,
+ Crown,
+ ScrollText,
+ Zap,
+ Bot,
+ Puzzle,
+ BookOpen,
+ FileBox,
+ HardDrive,
+ Workflow,
+ Code2,
+ AppWindow,
+ TerminalSquare,
+ Share2,
+ Megaphone,
+ ShoppingBag,
+ Receipt,
+ Building2,
+ BarChart3,
+ ChevronLeft,
+ ChevronRight,
+ ExternalLink,
+ LogOut,
+ ShieldCheck,
+} from 'lucide-react';
+
+interface AdminNavItem {
+ path: string;
+ label: string;
+ icon: React.ReactNode;
+ implemented?: boolean; // true = 真实数据页;false = ComingSoon 占位
+}
+
+interface AdminNavGroup {
+ title: string;
+ items: AdminNavItem[];
+}
+
+// 导航结构对照旧 admin(AdminLayout.tsx 6 分组 33 项),
+// 初期仅 Dashboard 为真实数据页,其余以 ComingSoon 占位,保持导航不出现断崖。
+const adminNavGroups: AdminNavGroup[] = [
+ {
+ title: '概览',
+ items: [
+ { path: '/admin', label: '数据看板', icon: , implemented: true },
+ ],
+ },
+ {
+ title: '用户与权限',
+ items: [
+ { path: '/admin/users', label: '用户管理', icon: },
+ { path: '/admin/roles', label: '角色权限', icon: },
+ { path: '/admin/tenant', label: '租户管理', icon: },
+ ],
+ },
+ {
+ title: '模型与资源',
+ items: [
+ { path: '/admin/models', label: '模型管理', icon: },
+ { path: '/admin/knowledge', label: '知识库', icon: },
+ { path: '/admin/assets', label: '素材管理', icon: },
+ { path: '/admin/storage', label: '存储管理', icon: },
+ ],
+ },
+ {
+ title: '智能体与工具',
+ items: [
+ { path: '/admin/agents', label: '智能体', icon: },
+ { path: '/admin/mcp', label: 'MCP 工具', icon: },
+ { path: '/admin/mcp-market', label: 'MCP 市场', icon: },
+ { path: '/admin/skills', label: '技能管理', icon: },
+ { path: '/admin/workflow', label: '工作流', icon: },
+ { path: '/admin/automation', label: '自动化', icon: },
+ ],
+ },
+ {
+ title: '应用与分发',
+ items: [
+ { path: '/admin/apps', label: '应用管理', icon: },
+ { path: '/admin/code-exec', label: '代码执行', icon: },
+ { path: '/admin/distributions', label: '分发渠道', icon: },
+ { path: '/admin/ads', label: '广告管理', icon: },
+ { path: '/admin/market', label: '应用市场', icon: },
+ ],
+ },
+ {
+ title: '运营与计费',
+ items: [
+ { path: '/admin/payment', label: '支付配置', icon: },
+ { path: '/admin/billing', label: '账单计费', icon: },
+ { path: '/admin/membership', label: '会员体系', icon: },
+ { path: '/admin/services', label: '增值服务', icon: },
+ { path: '/admin/logs', label: '日志审计', icon: },
+ { path: '/admin/developer', label: '开发者', icon: },
+ { path: '/admin/analytics', label: '数据分析', icon: },
+ ],
+ },
+];
+
+export function AdminLayout() {
+ const [collapsed, setCollapsed] = useState(false);
+ const navigate = useNavigate();
+ const { data: user } = useCurrentUser();
+
+ const handleLogout = () => {
+ localStorage.removeItem('auth_token');
+ navigate('/login');
+ };
+
+ return (
+
+ {/* 侧边栏 */}
+
+
+ {/* 主区域 */}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend-v2/src/pages/admin/AdminPlaceholders.tsx b/frontend-v2/src/pages/admin/AdminPlaceholders.tsx
new file mode 100644
index 0000000..716c03b
--- /dev/null
+++ b/frontend-v2/src/pages/admin/AdminPlaceholders.tsx
@@ -0,0 +1,308 @@
+import { AdminComingSoon } from './AdminComingSoon';
+import {
+ Users,
+ ShieldCheck,
+ Building2,
+ Cpu,
+ BookOpen,
+ FileBox,
+ HardDrive,
+ Bot,
+ Puzzle,
+ Zap,
+ Workflow,
+ TerminalSquare,
+ AppWindow,
+ Code2,
+ Share2,
+ Megaphone,
+ ShoppingBag,
+ CreditCard,
+ Receipt,
+ Crown,
+ Boxes,
+ ScrollText,
+ BarChart3,
+} from 'lucide-react';
+
+/**
+ * admin 后台占位页集中导出。
+ * 旧 AdminLayout 共 33 个二级模块,初期除 Dashboard(真实数据页)外,
+ * 其余 32 个模块统一用 AdminComingSoon 占位,待后续阶段逐个迁移真实数据。
+ * 每个导出对应一条侧边栏导航路由。
+ */
+
+export function UsersPage() {
+ return (
+
+ );
+}
+
+export function RolesPage() {
+ return (
+
+ );
+}
+
+export function TenantPage() {
+ return (
+
+ );
+}
+
+export function ModelsPage() {
+ return (
+
+ );
+}
+
+export function KnowledgeAdminPage() {
+ return (
+
+ );
+}
+
+export function AssetsPage() {
+ return (
+
+ );
+}
+
+export function StoragePage() {
+ return (
+
+ );
+}
+
+export function AgentsAdminPage() {
+ return (
+
+ );
+}
+
+export function McpPage() {
+ return (
+
+ );
+}
+
+export function McpMarketPage() {
+ return (
+
+ );
+}
+
+export function SkillsAdminPage() {
+ return (
+
+ );
+}
+
+export function WorkflowAdminPage() {
+ return (
+
+ );
+}
+
+export function AutomationPage() {
+ return (
+
+ );
+}
+
+export function AppsAdminPage() {
+ return (
+
+ );
+}
+
+export function CodeExecPage() {
+ return (
+
+ );
+}
+
+export function DistributionsPage() {
+ return (
+
+ );
+}
+
+export function AdsPage() {
+ return (
+
+ );
+}
+
+export function MarketAdminPage() {
+ return (
+
+ );
+}
+
+export function PaymentPage() {
+ return (
+
+ );
+}
+
+export function BillingPage() {
+ return (
+
+ );
+}
+
+export function MembershipPage() {
+ return (
+
+ );
+}
+
+export function ServicesPage() {
+ return (
+
+ );
+}
+
+export function LogsPage() {
+ return (
+
+ );
+}
+
+export function DeveloperPage() {
+ return (
+
+ );
+}
+
+export function AnalyticsPage() {
+ return (
+
+ );
+}