feat(admin): 后台脚手架-布局/路由/守卫 + Dashboard 真实数据页 (#1013)
CI / gate (push) Has been cancelled

This commit is contained in:
agent committed 2026-08-24 11:32:18 +08:00
1 parent 7b8c3ba2dd
commit 25f3edac0a
6 files changed
+1286 -79

No files matched your search

+88 -48
View File
@@ -10,17 +10,61 @@ import { ImBotHub } from '@/pages/front/ImBotHub';
import { AppServices } from '@/pages/front/AppServices'; import { AppServices } from '@/pages/front/AppServices';
import { DashboardPage } from '@/pages/front/DashboardPage'; import { DashboardPage } from '@/pages/front/DashboardPage';
import { ProfilePage } from '@/pages/front/ProfilePage'; 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 { ComingSoon } from '@/components/common/ComingSoon';
import { import {
Zap,
Bot, Bot,
Puzzle, Puzzle,
Workflow, Workflow,
Shield, Shield,
Radio, Radio,
Bell,
Sparkles,
} from 'lucide-react'; } 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 <Navigate to="/login" replace />;
if (isLoading) {
return <div className="min-h-screen flex items-center justify-center text-muted-foreground">加载中…</div>;
}
const isAdmin = user?.role === 'admin' || user?.role === 'super_admin';
if (!isAdmin) return <Navigate to="/" replace />;
return <>{children}</>;
}
function App() { function App() {
return ( return (
@@ -35,21 +79,7 @@ function App() {
<Route path="im-bots" element={<ImBotHub />} /> <Route path="im-bots" element={<ImBotHub />} />
<Route path="apps" element={<AppServices />} /> <Route path="apps" element={<AppServices />} />
{/* 占位页:真实功能开发中,统一 amber 金暖白「敬请期待」态 */} {/* 占位页:真实功能开发中,统一 amber 金暖白「敬请期待」态 */}
<Route <Route path="market" element={<MarketPage />} />
path="market"
element={
<ComingSoon
title="技能市场"
icon={Zap}
description="浏览、导入、发布 Skills 技能,一键接入平台能力。"
features={[
'从 Coze / Dify / N8N 等平台导入技能',
'官方技能与社区技能分类检索',
'一键安装到 AI 聊天与个人工作流',
]}
/>
}
/>
<Route <Route
path="agents" path="agents"
element={ element={
@@ -127,37 +157,47 @@ function App() {
/> />
<Route path="dashboard" element={<DashboardPage />} /> <Route path="dashboard" element={<DashboardPage />} />
<Route path="profile" element={<ProfilePage />} /> <Route path="profile" element={<ProfilePage />} />
<Route <Route path="notifications" element={<NotificationsPage />} />
path="notifications" <Route path="evolution" element={<EvolutionPage />} />
element={
<ComingSoon
title="消息通知"
icon={Bell}
description="集中查看系统通知与任务动态。"
features={[
'任务完成 / 失败提醒',
'配额与账单告警',
'按渠道订阅推送',
]}
/>
}
/>
<Route
path="evolution"
element={
<ComingSoon
title="自进化"
icon={Sparkles}
description="让平台从使用中持续学习、自我优化能力。"
features={[
'对话与工具调用沉淀',
'技能自动迭代建议',
'知识库增量同步',
]}
/>
}
/>
</Route> </Route>
{/* Admin 后台:管理员守卫 + AdminLayout 外壳 + 嵌套二级路由 */}
<Route
path="/admin"
element={
<AdminGuard>
<AdminLayout />
</AdminGuard>
}
>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<UsersPage />} />
<Route path="roles" element={<RolesPage />} />
<Route path="tenant" element={<TenantPage />} />
<Route path="models" element={<ModelsPage />} />
<Route path="knowledge" element={<KnowledgeAdminPage />} />
<Route path="assets" element={<AssetsPage />} />
<Route path="storage" element={<StoragePage />} />
<Route path="agents" element={<AgentsAdminPage />} />
<Route path="mcp" element={<McpPage />} />
<Route path="mcp-market" element={<McpMarketPage />} />
<Route path="skills" element={<SkillsAdminPage />} />
<Route path="workflow" element={<WorkflowAdminPage />} />
<Route path="automation" element={<AutomationPage />} />
<Route path="apps" element={<AppsAdminPage />} />
<Route path="code-exec" element={<CodeExecPage />} />
<Route path="distributions" element={<DistributionsPage />} />
<Route path="ads" element={<AdsPage />} />
<Route path="market" element={<MarketAdminPage />} />
<Route path="payment" element={<PaymentPage />} />
<Route path="billing" element={<BillingPage />} />
<Route path="membership" element={<MembershipPage />} />
<Route path="services" element={<ServicesPage />} />
<Route path="logs" element={<LogsPage />} />
<Route path="developer" element={<DeveloperPage />} />
<Route path="analytics" element={<AnalyticsPage />} />
</Route>
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
+438 -31
View File
@@ -31,6 +31,12 @@ async function data<T>(path: string, options: RequestInit = {}): Promise<T> {
return env.data; return env.data;
} }
// im_bot_user.py 专用 envelope:`{success, data, ...}`(无 code 字段)。
async function successData<T>(path: string, options: RequestInit = {}): Promise<T> {
const env = await request<{ success: boolean; data: T }>(path, options);
return env.data;
}
// ─── Auth ─────────────────────────────────────────────────────────────────────── // ─── Auth ───────────────────────────────────────────────────────────────────────
export const authApi = { export const authApi = {
@@ -46,7 +52,7 @@ export const authApi = {
body: JSON.stringify(data), 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' }), logout: () => request('/auth/logout', { method: 'POST' }),
@@ -56,13 +62,19 @@ export const authApi = {
// ─── Chat ──────────────────────────────────────────────────────────────────────── // ─── 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 { export interface Conversation {
id: string; id: string;
title: string; title: string;
model: string; model_id: string;
status?: string;
pinned?: boolean;
source?: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
message_count: number; message_count?: number; // 后端未返回,前端可选兼容
} }
export interface ChatMessage { export interface ChatMessage {
@@ -70,24 +82,25 @@ export interface ChatMessage {
conversation_id: string; conversation_id: string;
role: 'user' | 'assistant'; role: 'user' | 'assistant';
content: string; content: string;
model: string; model_id: string;
created_at: string; created_at: string;
token_used?: number;
} }
export const chatApi = { export const chatApi = {
listConversations: () => request<Conversation[]>('/chat/conversations'), listConversations: () => data<Conversation[]>('/chat/conversations'),
createConversation: (data: { title?: string; model?: string }) => createConversation: (payload: { title?: string; model?: string }) =>
request<Conversation>('/chat/conversations', { data<Conversation>('/chat/conversations', {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(payload),
}), }),
deleteConversation: (id: string) => deleteConversation: (id: string) =>
request(`/chat/conversations/${id}`, { method: 'DELETE' }), request(`/chat/conversations/${id}`, { method: 'DELETE' }),
getMessages: (convId: string) => getMessages: (convId: string) =>
request<ChatMessage[]>(`/chat/conversations/${convId}/messages`), data<ChatMessage[]>(`/chat/conversations/${convId}/messages`),
sendMessage: (convId: string, data: { content: string; model?: string }) => sendMessage: (convId: string, data: { content: string; model?: string }) =>
request<ChatMessage>(`/chat/conversations/${convId}/messages`, { request<ChatMessage>(`/chat/conversations/${convId}/messages`, {
@@ -130,23 +143,41 @@ export const chatApi = {
// ─── Knowledge ─────────────────────────────────────────────────────────────────── // ─── 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 { export interface KnowledgeBase {
id: string; id: string;
name: string; name: string;
description: string; description: string;
document_count: number; document_count: number;
status: 'active' | 'building' | 'error'; status: 'active' | 'building' | 'error' | string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
export const knowledgeApi = { export interface KnowledgeBaseList {
listBases: () => request<KnowledgeBase[]>('/knowledge/bases'), bases: KnowledgeBase[];
storage_used: number;
storage_limit: number;
membership_level: number;
}
createBase: (data: { name: string; description?: string }) => // 创建返回的子集(字段以 create 真实返回为准,缺失字段前端填充默认值)
request<KnowledgeBase>('/knowledge/bases', { export interface KnowledgeBaseCreated {
id: string;
name: string;
description: string;
created_at: string;
}
export const knowledgeApi = {
listBases: () => data<KnowledgeBaseList>('/knowledge/bases'),
createBase: (payload: { name: string; description?: string }) =>
data<KnowledgeBaseCreated>('/knowledge/bases', {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(payload),
}), }),
deleteBase: (id: string) => deleteBase: (id: string) =>
@@ -164,45 +195,169 @@ export const knowledgeApi = {
}; };
// ─── Market / Apps ────────────────────────────────────────────────────────────── // ─── 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 { export interface MarketApp {
id: string; id: string;
name: string; name: string;
description: string; description: string;
icon: string; icon: string | null;
category: string; type: MarketAppType;
status: 'enabled' | 'disabled'; source: MarketAppSource;
type: 'standalone' | 'embedded'; publisher_id: string;
downloads: number; 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; 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 = { export const marketApi = {
listApps: () => request<MarketApp[]>('/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<MarketAppList>(`/apps${q ? `?${q}` : ''}`);
},
getMyApps: () => request<MarketApp[]>('/market/my-apps'), getApp: (appId: string) =>
data<MarketAppDetail>(`/apps/${appId}`),
getMyApps: () =>
data<MyApp[]>('/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<MarketReviewList>(`/reviews/${appId}${q ? `?${q}` : ''}`);
},
getAds: () =>
data<MarketAd[]>('/ads'),
// ─── 写操作(后续迭代启用)───
installApp: (appId: string) => installApp: (appId: string) =>
request(`/market/apps/${appId}/install`, { method: 'POST' }), request(`/apps/${appId}/install`, { method: 'POST' }),
uninstallApp: (appId: string) => 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 ──────────────────────────────────────────────────────────────────────
// 后端契约 (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 { export interface ImBotStatus {
status: 'connected' | 'disconnected' | 'expired'; status: 'connected' | 'disconnected' | 'expired' | string;
ilink_bot_id: string; ilink_bot_id: string | null;
connected_at: string; connected_at: string | null;
last_active: string; last_active?: string | null;
message_count: number; message_count?: number;
connected?: boolean;
configured?: boolean;
username?: string | null;
baseurl?: string | null;
} }
export const imBotApi = { export const imBotApi = {
getStatus: () => request<ImBotStatus>('/im-bot/status'), getStatus: () => successData<ImBotStatus>('/im-bot/status'),
getQRCode: () => request<{ qr_url: string; expire_at: string }>('/im-bot/qrcode'), getQRCode: () => request<{ qr_url: string; expire_at: string }>('/im-bot/qrcode'),
@@ -407,3 +562,255 @@ export const tokenApi = {
getBalance: () => data<TokenBalance>('/token/balance'), getBalance: () => data<TokenBalance>('/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<NotificationList>(`/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<T>(),改用 rawAdmin<T>() 解包并按端点分别处理 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<T>() 相同语义,但显式标记 admin 域,便于后续差异演进)
async function adminData<T>(path: string, options: RequestInit = {}): Promise<T> {
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<AdminDashboardStats>('/admin/dashboard/stats'),
// /daily-stats 标准信封 {code,data:{days,data}}
getDailyStats: () => adminData<AdminDailyStats>('/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<string, unknown>;
}
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<EvolutionSkill[]>(`/evolution/skills${q ? `?${q}` : ''}`);
},
getSkillDetail: (skillName: string) =>
data<EvolutionSkill>(`/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<EvolutionEvent[]>(`/evolution/events${q ? `?${q}` : ''}`);
},
// 驾驶舱
getSkillTree: () => data<EvolutionSkillTree>('/evolution/dashboard/skill-tree'),
getTimeline: (event_type?: string) =>
data<EvolutionEvent[]>(`/evolution/dashboard/timeline${event_type ? `?event_type=${encodeURIComponent(event_type)}` : ''}`),
getStatistics: () => data<EvolutionStatistics>('/evolution/dashboard/statistics'),
getProfile: () => data<EvolutionProfile>('/evolution/dashboard/profile'),
// 流水线 / Watcher
getPipelineStatus: () => data<EvolutionPipelineStatus>('/evolution/pipeline/status'),
getWatcherStatus: () => data<EvolutionWatcherStatus>('/evolution/watcher/status'),
// 报告
listReports: (report_type?: string) =>
data<EvolutionReport[]>(`/evolution/reports${report_type ? `?report_type=${encodeURIComponent(report_type)}` : ''}`),
getReportDetail: (reportId: string) =>
data<EvolutionReportDetail>(`/evolution/reports/${reportId}`),
};
@@ -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 (
<div className="flex items-center justify-center min-h-[60vh] px-4">
<Card className="w-full max-w-lg text-center border-border">
<CardContent className="p-10">
<div className="mx-auto mb-6 w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center">
<Icon size={30} className="text-primary" />
</div>
<div className="flex items-center justify-center gap-2 mb-3">
<h2 className="text-2xl font-bold text-foreground">{title}</h2>
<Badge className="bg-primary/10 text-primary border-primary/20 hover:bg-primary/10">
<Construction size={12} className="mr-1" />
建设中
</Badge>
</div>
<p className="text-muted-foreground leading-relaxed mb-6">
{description ?? `${title} 模块正在开发中,敬请期待。上线后这里将提供完整的后台管理能力。`}
</p>
{features && features.length > 0 && (
<div className="text-left space-y-2 mb-6">
{features.map((f, i) => (
<div key={i} className="flex items-start gap-2 text-sm text-muted-foreground">
<Sparkles size={15} className="text-primary mt-0.5 shrink-0" />
<span>{f}</span>
</div>
))}
</div>
)}
<div className="inline-flex items-center gap-2 text-xs text-muted-foreground/70 bg-muted rounded-full px-3 py-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
我们将尽快为您带来 {title}
</div>
</CardContent>
</Card>
</div>
);
}
@@ -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 (
<Card className="border-border">
<CardContent className="p-5">
<div className="flex items-center justify-between mb-3">
<span className="text-sm text-muted-foreground">{label}</span>
<div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
{Icon}
</div>
</div>
<div className="text-2xl font-bold text-foreground">{value}</div>
{sub && <div className="text-xs text-muted-foreground mt-1">{sub}</div>}
</CardContent>
</Card>
);
}
// 简易柱状图(纯 SVG,amber 金暖白)
function TokenBarChart({ data }: { data: AdminDailyStat[] }) {
if (!data || data.length === 0) {
return <div className="text-sm text-muted-foreground py-8 text-center">暂无近 7 日 Token 数据</div>;
}
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 (
<svg viewBox={`0 0 ${width} ${height}`} className="w-full h-auto">
{data.map((d, i) => {
const h = Math.max((d.tokens / max) * (height - 30), 2);
const x = i * (barW + gap);
const y = height - h - 20;
return (
<g key={d.date}>
<rect x={x} y={y} width={barW} height={h} rx={4} fill="var(--primary)" opacity={0.85} />
<text x={x + barW / 2} y={height - 6} textAnchor="middle" fontSize="10" fill="var(--muted-foreground)">
{d.date.slice(5)}
</text>
</g>
);
})}
</svg>
);
}
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 (
<svg viewBox="0 0 72 72" className="w-16 h-16">
<circle cx="36" cy="36" r={r} fill="none" stroke="var(--border)" strokeWidth="8" />
<circle
cx="36"
cy="36"
r={r}
fill="none"
stroke="var(--primary)"
strokeWidth="8"
strokeDasharray={`${c * pct} ${c}`}
strokeLinecap="round"
transform="rotate(-90 36 36)"
/>
<text x="36" y="40" textAnchor="middle" fontSize="14" fontWeight="700" fill="var(--foreground)">
{Math.round(pct * 100)}%
</text>
</svg>
);
}
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 (
<div className="space-y-6">
{/* 页头 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground">数据看板</h1>
<p className="text-sm text-muted-foreground mt-1">
欢迎回来,{user?.username || '管理员'}。平台实时运营概览。
</p>
</div>
<div className="text-right">
<div className="text-xs text-muted-foreground">服务器时间(北京)</div>
<div className="text-sm font-medium text-foreground">{formatBjTime(stats?.server_time)}</div>
</div>
</div>
{/* 错误态 */}
{statsQuery.isError && (
<div className="rounded-lg border border-destructive/30 bg-destructive/10 text-destructive px-4 py-3 text-sm">
看板数据加载失败:{(statsQuery.error as Error)?.message}
</div>
)}
{/* 统计卡片 6 张 */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<StatCard icon={<Users size={18} />} label="总用户数" value={formatNumber(stats?.total_users)} sub={`活跃 ${formatNumber(stats?.active_users)}`} />
<StatCard icon={<Cpu size={18} />} label="模型总数" value={formatNumber(stats?.total_models)} sub={`启用 ${formatNumber(stats?.enabled_models)}`} />
<StatCard icon={<MessageSquare size={18} />} label="对话总数" value={formatNumber(stats?.total_conversations)} sub={`今日 ${formatNumber(stats?.today_conversations)}`} />
<StatCard icon={<BookOpen size={18} />} label="知识库" value={formatNumber(stats?.total_knowledge_bases)} sub={`文档 ${formatNumber(stats?.total_documents)}`} />
<StatCard icon={<Coins size={18} />} label="累计 Token" value={formatNumber(stats?.total_tokens)} sub={`今日 ${formatNumber(stats?.today_tokens)}`} />
<StatCard icon={<Activity size={18} />} label="今日订单" value={formatNumber(stats?.today_orders)} sub={`订阅 ${formatNumber(stats?.today_subscription_orders)}`} />
</div>
{/* 图表区 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<Card className="border-border lg:col-span-2">
<CardContent className="p-5">
<div className="flex items-center gap-2 mb-4">
<TrendingUp size={18} className="text-primary" />
<h2 className="text-base font-semibold text-foreground">近 7 日 Token 消耗</h2>
</div>
{dailyQuery.isLoading ? (
<div className="text-sm text-muted-foreground py-8 text-center">加载中…</div>
) : (
<TokenBarChart data={daily} />
)}
</CardContent>
</Card>
<Card className="border-border">
<CardContent className="p-5">
<h2 className="text-base font-semibold text-foreground mb-4">模型启用率</h2>
<div className="flex flex-col items-center gap-3">
<MiniDonut enabled={stats?.enabled_models ?? 0} total={stats?.total_models ?? 0} />
<div className="text-sm text-muted-foreground text-center">
启用 {formatNumber(stats?.enabled_models)} / 共 {formatNumber(stats?.total_models)}
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}
+214
View File
@@ -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: <LayoutDashboard size={20} />, implemented: true },
],
},
{
title: '用户与权限',
items: [
{ path: '/admin/users', label: '用户管理', icon: <Users size={20} /> },
{ path: '/admin/roles', label: '角色权限', icon: <ShieldCheck size={20} /> },
{ path: '/admin/tenant', label: '租户管理', icon: <Building2 size={20} /> },
],
},
{
title: '模型与资源',
items: [
{ path: '/admin/models', label: '模型管理', icon: <Cpu size={20} /> },
{ path: '/admin/knowledge', label: '知识库', icon: <BookOpen size={20} /> },
{ path: '/admin/assets', label: '素材管理', icon: <FileBox size={20} /> },
{ path: '/admin/storage', label: '存储管理', icon: <HardDrive size={20} /> },
],
},
{
title: '智能体与工具',
items: [
{ path: '/admin/agents', label: '智能体', icon: <Bot size={20} /> },
{ path: '/admin/mcp', label: 'MCP 工具', icon: <Puzzle size={20} /> },
{ path: '/admin/mcp-market', label: 'MCP 市场', icon: <Puzzle size={20} /> },
{ path: '/admin/skills', label: '技能管理', icon: <Zap size={20} /> },
{ path: '/admin/workflow', label: '工作流', icon: <Workflow size={20} /> },
{ path: '/admin/automation', label: '自动化', icon: <TerminalSquare size={20} /> },
],
},
{
title: '应用与分发',
items: [
{ path: '/admin/apps', label: '应用管理', icon: <AppWindow size={20} /> },
{ path: '/admin/code-exec', label: '代码执行', icon: <Code2 size={20} /> },
{ path: '/admin/distributions', label: '分发渠道', icon: <Share2 size={20} /> },
{ path: '/admin/ads', label: '广告管理', icon: <Megaphone size={20} /> },
{ path: '/admin/market', label: '应用市场', icon: <ShoppingBag size={20} /> },
],
},
{
title: '运营与计费',
items: [
{ path: '/admin/payment', label: '支付配置', icon: <CreditCard size={20} /> },
{ path: '/admin/billing', label: '账单计费', icon: <Receipt size={20} /> },
{ path: '/admin/membership', label: '会员体系', icon: <Crown size={20} /> },
{ path: '/admin/services', label: '增值服务', icon: <Boxes size={20} /> },
{ path: '/admin/logs', label: '日志审计', icon: <ScrollText size={20} /> },
{ path: '/admin/developer', label: '开发者', icon: <Code2 size={20} /> },
{ path: '/admin/analytics', label: '数据分析', icon: <BarChart3 size={20} /> },
],
},
];
export function AdminLayout() {
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const { data: user } = useCurrentUser();
const handleLogout = () => {
localStorage.removeItem('auth_token');
navigate('/login');
};
return (
<div className="min-h-screen bg-background flex">
{/* 侧边栏 */}
<aside className={cn(
'fixed left-0 top-0 z-40 h-screen border-r border-border bg-white dark:bg-gray-950 transition-all duration-300 flex flex-col',
collapsed ? 'w-[68px]' : 'w-[248px]'
)}>
{/* Logo + 访问前台 */}
<div className="h-16 flex items-center justify-between px-4 border-b border-border">
{!collapsed && (
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
<span className="text-white font-bold text-sm">七</span>
</div>
<span className="text-base font-bold text-foreground">七虎AI · 后台</span>
</div>
)}
<button
onClick={() => setCollapsed(!collapsed)}
className="p-1.5 rounded-lg hover:bg-muted text-muted-foreground transition-colors"
>
{collapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
</button>
</div>
{/* 导航 */}
<div className="flex-1 overflow-y-auto py-4 px-2">
{adminNavGroups.map((group, i) => (
<div key={i} className="mb-4">
{!collapsed && (
<div className="px-3 mb-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">
{group.title}
</div>
)}
{group.items.map((item) => (
<NavLink
key={item.path}
to={item.path}
end={item.path === '/admin'}
className={({ isActive }) => cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
)}
title={collapsed ? item.label : undefined}
>
{item.icon}
{!collapsed && <span>{item.label}</span>}
</NavLink>
))}
</div>
))}
</div>
{/* 底部:访问前台 + 用户信息 + 退出 */}
<div className="border-t border-border py-2 px-2 space-y-1">
<button
onClick={() => navigate('/')}
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground transition-colors w-full"
title={collapsed ? '访问前台' : undefined}
>
<ExternalLink size={20} />
{!collapsed && <span>访问前台</span>}
</button>
{!collapsed && (
<div className="flex items-center gap-2 px-3 py-2">
<Avatar className="h-8 w-8">
<AvatarFallback>{(user?.username || 'A').slice(0, 1).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground truncate">{user?.username || '管理员'}</div>
<div className="text-xs text-muted-foreground truncate">{user?.role || 'admin'}</div>
</div>
<button
onClick={handleLogout}
className="p-1.5 rounded-lg hover:bg-muted text-muted-foreground transition-colors"
title="退出登录"
>
<LogOut size={18} />
</button>
</div>
)}
</div>
</aside>
{/* 主区域 */}
<div className={cn('flex-1 flex flex-col min-h-screen', collapsed ? 'ml-[68px]' : 'ml-[248px]')}>
<main className="flex-1 p-6 overflow-y-auto">
<Outlet />
</main>
</div>
</div>
);
}
@@ -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 (
<AdminComingSoon
title="用户管理"
icon={Users}
description="管理全平台注册用户,查看活跃度、封禁状态与归属租户。"
features={['用户列表与搜索', '角色与状态管理', '登录日志审计']}
/>
);
}
export function RolesPage() {
return (
<AdminComingSoon
title="角色权限"
icon={ShieldCheck}
description="配置 RBAC 角色与细粒度权限策略,管控后台访问边界。"
features={['角色 CRUD', '权限点分配', '资源级授权']}
/>
);
}
export function TenantPage() {
return (
<AdminComingSoon
title="租户管理"
icon={Building2}
description="管理多租户隔离、配额与租户级配置。"
features={['租户列表', '配额调整', '隔离策略']}
/>
);
}
export function ModelsPage() {
return (
<AdminComingSoon
title="模型管理"
icon={Cpu}
description="接入与编排 LLM 供应商、模型上下线与计费路由。"
features={['供应商配置', '模型启停', '路由权重']}
/>
);
}
export function KnowledgeAdminPage() {
return (
<AdminComingSoon
title="知识库管理"
icon={BookOpen}
description="平台级知识库与向量检索策略管理。"
features={['知识库列表', '检索调优', '容量监控']}
/>
);
}
export function AssetsPage() {
return (
<AdminComingSoon
title="素材管理"
icon={FileBox}
description="管理上传的文档、图片与多模态素材资源。"
features={['素材库', '分类标签', '占用统计']}
/>
);
}
export function StoragePage() {
return (
<AdminComingSoon
title="存储管理"
icon={HardDrive}
description="对象存储桶、配额与冷热分层策略。"
features={['存储桶', '配额告警', '生命周期']}
/>
);
}
export function AgentsAdminPage() {
return (
<AdminComingSoon
title="智能体管理"
icon={Bot}
description="平台级 Agent 编排模板与市场审核。"
features={['Agent 模板', '发布审核', '调用统计']}
/>
);
}
export function McpPage() {
return (
<AdminComingSoon
title="MCP 工具"
icon={Puzzle}
description="管理 Model Context Protocol 工具接入与连通性。"
features={['工具配置', '连通测试', '权限治理']}
/>
);
}
export function McpMarketPage() {
return (
<AdminComingSoon
title="MCP 市场"
icon={Puzzle}
description="官方与社区 MCP 应用市场运营。"
features={['上架审核', '分类运营', '下载统计']}
/>
);
}
export function SkillsAdminPage() {
return (
<AdminComingSoon
title="技能管理"
icon={Zap}
description="AI 自进化技能库的状态与质量治理。"
features={['技能状态', '质量评分', '冻结/解冻']}
/>
);
}
export function WorkflowAdminPage() {
return (
<AdminComingSoon
title="工作流管理"
icon={Workflow}
description="可视化 DAG 自动化流程的模板与运行监控。"
features={['流程模板', '运行记录', '失败重试']}
/>
);
}
export function AutomationPage() {
return (
<AdminComingSoon
title="自动化"
icon={TerminalSquare}
description="定时任务、触发器与事件驱动自动化。"
features={['任务编排', '触发器', '执行日志']}
/>
);
}
export function AppsAdminPage() {
return (
<AdminComingSoon
title="应用管理"
icon={AppWindow}
description="平台内置应用的上架、配置与下线。"
features={['应用列表', '配置项', '灰度发布']}
/>
);
}
export function CodeExecPage() {
return (
<AdminComingSoon
title="代码执行"
icon={Code2}
description="沙箱代码执行环境的安全与配额管控。"
features={['沙箱配置', '资源限额', '审计日志']}
/>
);
}
export function DistributionsPage() {
return (
<AdminComingSoon
title="分发渠道"
icon={Share2}
description="管理多渠道分发与推广链接。"
features={['渠道列表', '推广码', '转化统计']}
/>
);
}
export function AdsPage() {
return (
<AdminComingSoon
title="广告管理"
icon={Megaphone}
description="运营广告位与投放策略。"
features={['广告位', '创意管理', '曝光统计']}
/>
);
}
export function MarketAdminPage() {
return (
<AdminComingSoon
title="应用市场"
icon={ShoppingBag}
description="技能/应用市场的审核、分类与运营。"
features={['上架审核', '分类运营', '交易对账']}
/>
);
}
export function PaymentPage() {
return (
<AdminComingSoon
title="支付配置"
icon={CreditCard}
description="支付网关、回调与对账配置。"
features={['网关配置', '回调校验', '对账文件']}
/>
);
}
export function BillingPage() {
return (
<AdminComingSoon
title="账单计费"
icon={Receipt}
description="用量计费、账单生成与发票管理。"
features={['账单列表', '用量明细', '发票管理']}
/>
);
}
export function MembershipPage() {
return (
<AdminComingSoon
title="会员体系"
icon={Crown}
description="会员等级、权益与续费策略。"
features={['等级配置', '权益包', '续费统计']}
/>
);
}
export function ServicesPage() {
return (
<AdminComingSoon
title="增值服务"
icon={Boxes}
description="管理付费增值服务与套餐。"
features={['服务列表', '套餐配置', '售卖统计']}
/>
);
}
export function LogsPage() {
return (
<AdminComingSoon
title="日志审计"
icon={ScrollText}
description="操作日志与审计轨迹,支持 CSV 导出。"
features={['操作日志', '审计追踪', '导出']}
/>
);
}
export function DeveloperPage() {
return (
<AdminComingSoon
title="开发者"
icon={Code2}
description="开放 API、密钥与开发者生态管理。"
features={['API 密钥', 'SDK 下载', '调用配额']}
/>
);
}
export function AnalyticsPage() {
return (
<AdminComingSoon
title="数据分析"
icon={BarChart3}
description="全平台运营数据看板与多维分析。"
features={['留存分析', '漏斗分析', '自定义报表']}
/>
);
}