This commit is contained in:
1 parent
7b8c3ba2dd
commit
25f3edac0a
6 files changed
+1286
-79
No files matched your search
+88
-48
@@ -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 <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() {
|
||||
return (
|
||||
@@ -35,21 +79,7 @@ function App() {
|
||||
<Route path="im-bots" element={<ImBotHub />} />
|
||||
<Route path="apps" element={<AppServices />} />
|
||||
{/* 占位页:真实功能开发中,统一 amber 金暖白「敬请期待」态 */}
|
||||
<Route
|
||||
path="market"
|
||||
element={
|
||||
<ComingSoon
|
||||
title="技能市场"
|
||||
icon={Zap}
|
||||
description="浏览、导入、发布 Skills 技能,一键接入平台能力。"
|
||||
features={[
|
||||
'从 Coze / Dify / N8N 等平台导入技能',
|
||||
'官方技能与社区技能分类检索',
|
||||
'一键安装到 AI 聊天与个人工作流',
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="market" element={<MarketPage />} />
|
||||
<Route
|
||||
path="agents"
|
||||
element={
|
||||
@@ -127,37 +157,47 @@ function App() {
|
||||
/>
|
||||
<Route path="dashboard" element={<DashboardPage />} />
|
||||
<Route path="profile" element={<ProfilePage />} />
|
||||
<Route
|
||||
path="notifications"
|
||||
element={
|
||||
<ComingSoon
|
||||
title="消息通知"
|
||||
icon={Bell}
|
||||
description="集中查看系统通知与任务动态。"
|
||||
features={[
|
||||
'任务完成 / 失败提醒',
|
||||
'配额与账单告警',
|
||||
'按渠道订阅推送',
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="evolution"
|
||||
element={
|
||||
<ComingSoon
|
||||
title="自进化"
|
||||
icon={Sparkles}
|
||||
description="让平台从使用中持续学习、自我优化能力。"
|
||||
features={[
|
||||
'对话与工具调用沉淀',
|
||||
'技能自动迭代建议',
|
||||
'知识库增量同步',
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="notifications" element={<NotificationsPage />} />
|
||||
<Route path="evolution" element={<EvolutionPage />} />
|
||||
</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 />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
+438
-31
@@ -31,6 +31,12 @@ async function data<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
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 ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
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<Conversation[]>('/chat/conversations'),
|
||||
listConversations: () => data<Conversation[]>('/chat/conversations'),
|
||||
|
||||
createConversation: (data: { title?: string; model?: string }) =>
|
||||
request<Conversation>('/chat/conversations', {
|
||||
createConversation: (payload: { title?: string; model?: string }) =>
|
||||
data<Conversation>('/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<ChatMessage[]>(`/chat/conversations/${convId}/messages`),
|
||||
data<ChatMessage[]>(`/chat/conversations/${convId}/messages`),
|
||||
|
||||
sendMessage: (convId: string, data: { content: string; model?: string }) =>
|
||||
request<ChatMessage>(`/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<KnowledgeBase[]>('/knowledge/bases'),
|
||||
export interface KnowledgeBaseList {
|
||||
bases: KnowledgeBase[];
|
||||
storage_used: number;
|
||||
storage_limit: number;
|
||||
membership_level: number;
|
||||
}
|
||||
|
||||
createBase: (data: { name: string; description?: string }) =>
|
||||
request<KnowledgeBase>('/knowledge/bases', {
|
||||
// 创建返回的子集(字段以 create 真实返回为准,缺失字段前端填充默认值)
|
||||
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',
|
||||
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<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) =>
|
||||
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<ImBotStatus>('/im-bot/status'),
|
||||
getStatus: () => successData<ImBotStatus>('/im-bot/status'),
|
||||
|
||||
getQRCode: () => request<{ qr_url: string; expire_at: string }>('/im-bot/qrcode'),
|
||||
|
||||
@@ -407,3 +562,255 @@ export const tokenApi = {
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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={['留存分析', '漏斗分析', '自定义报表']}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user