From 25f3edac0a46f180456084f7a56c4e3441da0733 Mon Sep 17 00:00:00 2001 From: agent Date: Mon, 24 Aug 2026 11:32:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=90=8E=E5=8F=B0=E8=84=9A?= =?UTF-8?q?=E6=89=8B=E6=9E=B6-=E5=B8=83=E5=B1=80/=E8=B7=AF=E7=94=B1/?= =?UTF-8?q?=E5=AE=88=E5=8D=AB=20+=20Dashboard=20=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=A1=B5=20(#1013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-v2/src/App.tsx | 136 +++-- frontend-v2/src/api/client.ts | 469 ++++++++++++++++-- .../src/pages/admin/AdminComingSoon.tsx | 58 +++ .../src/pages/admin/AdminDashboard.tsx | 180 +++++++ frontend-v2/src/pages/admin/AdminLayout.tsx | 214 ++++++++ .../src/pages/admin/AdminPlaceholders.tsx | 308 ++++++++++++ 6 files changed, 1286 insertions(+), 79 deletions(-) create mode 100644 frontend-v2/src/pages/admin/AdminComingSoon.tsx create mode 100644 frontend-v2/src/pages/admin/AdminDashboard.tsx create mode 100644 frontend-v2/src/pages/admin/AdminLayout.tsx create mode 100644 frontend-v2/src/pages/admin/AdminPlaceholders.tsx diff --git a/frontend-v2/src/App.tsx b/frontend-v2/src/App.tsx index 73a295c..278f5a5 100644 --- a/frontend-v2/src/App.tsx +++ b/frontend-v2/src/App.tsx @@ -10,17 +10,61 @@ import { ImBotHub } from '@/pages/front/ImBotHub'; import { AppServices } from '@/pages/front/AppServices'; import { DashboardPage } from '@/pages/front/DashboardPage'; import { ProfilePage } from '@/pages/front/ProfilePage'; +import { MarketPage } from '@/pages/front/MarketPage'; +import { NotificationsPage } from '@/pages/front/NotificationsPage'; +import { EvolutionPage } from '@/pages/front/EvolutionPage'; import { ComingSoon } from '@/components/common/ComingSoon'; import { - Zap, Bot, Puzzle, Workflow, Shield, Radio, - Bell, - Sparkles, } from 'lucide-react'; +import { useCurrentUser } from '@/hooks/useApi'; +import { AdminLayout } from '@/pages/admin/AdminLayout'; +import { AdminDashboard } from '@/pages/admin/AdminDashboard'; +import { + UsersPage, + RolesPage, + TenantPage, + ModelsPage, + KnowledgeAdminPage, + AssetsPage, + StoragePage, + AgentsAdminPage, + McpPage, + McpMarketPage, + SkillsAdminPage, + WorkflowAdminPage, + AutomationPage, + AppsAdminPage, + CodeExecPage, + DistributionsPage, + AdsPage, + MarketAdminPage, + PaymentPage, + BillingPage, + MembershipPage, + ServicesPage, + LogsPage, + DeveloperPage, + AnalyticsPage, +} from '@/pages/admin/AdminPlaceholders'; + +// 管理员守卫:复用 frontend-v2 的 auth_token 认证链,role=admin|super_admin 放行。 +function AdminGuard({ children }: { children: React.ReactNode }) { + const { data: user, isLoading } = useCurrentUser(); + const token = localStorage.getItem('auth_token'); + + if (!token) return ; + if (isLoading) { + return
加载中…
; + } + const isAdmin = user?.role === 'admin' || user?.role === 'super_admin'; + if (!isAdmin) return ; + return <>{children}; +} function App() { return ( @@ -35,21 +79,7 @@ function App() { } /> } /> {/* 占位页:真实功能开发中,统一 amber 金暖白「敬请期待」态 */} - - } - /> + } /> } /> } /> - - } - /> - - } - /> + } /> + } /> + + {/* Admin 后台:管理员守卫 + AdminLayout 外壳 + 嵌套二级路由 */} + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> diff --git a/frontend-v2/src/api/client.ts b/frontend-v2/src/api/client.ts index 05174c2..e4eaa65 100644 --- a/frontend-v2/src/api/client.ts +++ b/frontend-v2/src/api/client.ts @@ -31,6 +31,12 @@ async function data(path: string, options: RequestInit = {}): Promise { return env.data; } +// im_bot_user.py 专用 envelope:`{success, data, ...}`(无 code 字段)。 +async function successData(path: string, options: RequestInit = {}): Promise { + const env = await request<{ success: boolean; data: T }>(path, options); + return env.data; +} + // ─── Auth ─────────────────────────────────────────────────────────────────────── export const authApi = { @@ -46,7 +52,7 @@ export const authApi = { body: JSON.stringify(data), }), - me: () => request<{ id: string; username: string; email: string; avatar_url: string }>('/auth/me'), + me: () => request<{ id: string; username: string; email: string; avatar_url: string; role?: string }>('/auth/me'), logout: () => request('/auth/logout', { method: 'POST' }), @@ -56,13 +62,19 @@ export const authApi = { // ─── Chat ──────────────────────────────────────────────────────────────────────── +// 后端契约 (chat.py):list/messages/create 均返回 {code,data:[...]}/{code,data:{...}} +// 列表字段:id,title,model_id,status,pinned,source,created_at,updated_at(无 message_count) +// 消息字段:id,conversation_id,role,content,model_id,created_at,token_used export interface Conversation { id: string; title: string; - model: string; + model_id: string; + status?: string; + pinned?: boolean; + source?: string; created_at: string; updated_at: string; - message_count: number; + message_count?: number; // 后端未返回,前端可选兼容 } export interface ChatMessage { @@ -70,24 +82,25 @@ export interface ChatMessage { conversation_id: string; role: 'user' | 'assistant'; content: string; - model: string; + model_id: string; created_at: string; + token_used?: number; } export const chatApi = { - listConversations: () => request('/chat/conversations'), + listConversations: () => data('/chat/conversations'), - createConversation: (data: { title?: string; model?: string }) => - request('/chat/conversations', { + createConversation: (payload: { title?: string; model?: string }) => + data('/chat/conversations', { method: 'POST', - body: JSON.stringify(data), + body: JSON.stringify(payload), }), deleteConversation: (id: string) => request(`/chat/conversations/${id}`, { method: 'DELETE' }), getMessages: (convId: string) => - request(`/chat/conversations/${convId}/messages`), + data(`/chat/conversations/${convId}/messages`), sendMessage: (convId: string, data: { content: string; model?: string }) => request(`/chat/conversations/${convId}/messages`, { @@ -130,23 +143,41 @@ export const chatApi = { // ─── Knowledge ─────────────────────────────────────────────────────────────────── +// 后端契约 (knowledge.py): +// list 返回 {code,data:{bases:[...],storage_used,storage_limit,membership_level}} +// create 返回 {code,data:{id,name,description,created_at}}(缺 document_count/status/updated_at) export interface KnowledgeBase { id: string; name: string; description: string; document_count: number; - status: 'active' | 'building' | 'error'; + status: 'active' | 'building' | 'error' | string; created_at: string; updated_at: string; } -export const knowledgeApi = { - listBases: () => request('/knowledge/bases'), +export interface KnowledgeBaseList { + bases: KnowledgeBase[]; + storage_used: number; + storage_limit: number; + membership_level: number; +} - createBase: (data: { name: string; description?: string }) => - request('/knowledge/bases', { +// 创建返回的子集(字段以 create 真实返回为准,缺失字段前端填充默认值) +export interface KnowledgeBaseCreated { + id: string; + name: string; + description: string; + created_at: string; +} + +export const knowledgeApi = { + listBases: () => data('/knowledge/bases'), + + createBase: (payload: { name: string; description?: string }) => + data('/knowledge/bases', { method: 'POST', - body: JSON.stringify(data), + body: JSON.stringify(payload), }), deleteBase: (id: string) => @@ -164,45 +195,169 @@ export const knowledgeApi = { }; // ─── Market / Apps ────────────────────────────────────────────────────────────── +// 后端契约:app/api/market.py → GET /apps (list_apps) 返回 +// { code:0, data:{ total, page, page_size, items:[MarketApp] } } +// 字段以 list_apps 真实 item 为准(见 #993 重写)。 + +export type MarketAppType = 'standalone' | 'embedded' | 'skill' | 'agent' | 'workflow'; +export type MarketAppSource = 'official' | 'community' | 'third_party'; +export type MarketAppStatus = 'draft' | 'pending' | 'published' | 'rejected' | 'offline'; export interface MarketApp { id: string; name: string; description: string; - icon: string; - category: string; - status: 'enabled' | 'disabled'; - type: 'standalone' | 'embedded'; - downloads: number; + icon: string | null; + type: MarketAppType; + source: MarketAppSource; + publisher_id: string; + publisher_name: string; + is_member_free: boolean; + join_promotion: boolean; + status: MarketAppStatus; + price: number; + install_count: number; + rating_avg: number; + review_count: number; + created_at: string; + capability: string | null; + category: string | null; + tags: string[]; + installed: boolean; + version: string | null; + security_scan: string | null; + verified: boolean; + dependencies: string[]; +} + +// 详情页用:list 字段 + 可能更丰富的描述/截图/长文 +export interface MarketAppDetail extends MarketApp { + long_description?: string; + screenshots?: string[]; + homepage_url?: string | null; + updated_at?: string | null; +} + +export interface MarketReview { + id: string; + app_id: string; + user_id: string; + user_name: string; rating: number; + comment: string; + created_at: string; +} + +export interface MarketReviewList { + total: number; + page: number; + page_size: number; + items: MarketReview[]; +} + +export interface MarketAd { + id: string; + title: string; + image_url: string; + url: string; +} + +export interface MarketAppList { + total: number; + page: number; + page_size: number; + items: MarketApp[]; +} + +export type MarketSort = 'latest' | 'popular' | 'rating'; + +// 后端契约 (market.py /my-apps):{code,data:[{id,app_id,name,icon,type,price,purchased_at}]} +// 与 MarketApp 字段差距大(无 installed/status/description/version),单独建模。 +export interface MyApp { + id: string; + app_id: string; + name: string; + icon: string | null; + type: MarketAppType; + price: number; + purchased_at: string | null; } export const marketApi = { - listApps: () => request('/market/apps'), + // 分页 + 筛选 + 排序 + 搜索,解包 {data:{total,page,page_size,items}} + listApps: (params: { + type?: MarketAppType; + source?: MarketAppSource; + capability?: string; + search?: string; + sort?: MarketSort; + page?: number; + page_size?: number; + } = {}) => { + const qs = new URLSearchParams(); + if (params.type) qs.set('type', params.type); + if (params.source) qs.set('source', params.source); + if (params.capability) qs.set('capability', params.capability); + if (params.search) qs.set('search', params.search); + if (params.sort) qs.set('sort', params.sort); + if (params.page) qs.set('page', String(params.page)); + if (params.page_size) qs.set('page_size', String(params.page_size)); + const q = qs.toString(); + return data(`/apps${q ? `?${q}` : ''}`); + }, - getMyApps: () => request('/market/my-apps'), + getApp: (appId: string) => + data(`/apps/${appId}`), + getMyApps: () => + data('/my-apps'), + + getReviews: (appId: string, params: { page?: number; page_size?: number } = {}) => { + const qs = new URLSearchParams(); + if (params.page) qs.set('page', String(params.page)); + if (params.page_size) qs.set('page_size', String(params.page_size)); + const q = qs.toString(); + return data(`/reviews/${appId}${q ? `?${q}` : ''}`); + }, + + getAds: () => + data('/ads'), + + // ─── 写操作(后续迭代启用)─── installApp: (appId: string) => - request(`/market/apps/${appId}/install`, { method: 'POST' }), + request(`/apps/${appId}/install`, { method: 'POST' }), uninstallApp: (appId: string) => - request(`/market/apps/${appId}/uninstall`, { method: 'DELETE' }), + request(`/apps/${appId}/uninstall`, { method: 'DELETE' }), - getAds: () => request<{ id: string; title: string; image_url: string; url: string }[]>('/market/ads'), + purchaseApp: (appId: string) => + request(`/apps/${appId}/purchase`, { method: 'POST' }), + + reviewApp: (appId: string, payload: { rating: number; comment: string }) => + request(`/apps/${appId}/review`, { + method: 'POST', + body: JSON.stringify(payload), + }), }; // ─── IM Bot ────────────────────────────────────────────────────────────────────── +// 后端契约 (im_bot_user.py /status):{success,data:{connected,status,configured,baseurl,ilink_bot_id,username,connected_at}} +// 注意:data 内无 message_count / last_active(前端页面已用可选默认值兼容)。 export interface ImBotStatus { - status: 'connected' | 'disconnected' | 'expired'; - ilink_bot_id: string; - connected_at: string; - last_active: string; - message_count: number; + status: 'connected' | 'disconnected' | 'expired' | string; + ilink_bot_id: string | null; + connected_at: string | null; + last_active?: string | null; + message_count?: number; + connected?: boolean; + configured?: boolean; + username?: string | null; + baseurl?: string | null; } export const imBotApi = { - getStatus: () => request('/im-bot/status'), + getStatus: () => successData('/im-bot/status'), getQRCode: () => request<{ qr_url: string; expire_at: string }>('/im-bot/qrcode'), @@ -407,3 +562,255 @@ export const tokenApi = { getBalance: () => data('/token/balance'), }; + +// ─── Notifications 消息通知 ────────────────────────────────────────────────────── +// 后端契约 (notifications.py):全部 {code,data,message} +// GET / → { total, page, page_size, items:[NotificationItem] } +// PUT /{id}/read → {code,message}; PUT /read-all?type= → {code,data:{count},message} +// GET /unread-count → {code,data:{count},message} + +export interface NotificationItem { + id: string; + type: string | null; + title: string; + content: string; + is_read: boolean; + related_id: string | null; + related_type: string | null; + created_at: string | null; +} + +export interface NotificationList { + total: number; + page: number; + page_size: number; + items: NotificationItem[]; +} + +export const notificationsApi = { + list: (params: { type?: string; is_read?: boolean; page?: number; page_size?: number } = {}) => { + const qs = new URLSearchParams(); + if (params.type) qs.set('type', params.type); + if (params.is_read !== undefined) qs.set('is_read', String(params.is_read)); + if (params.page) qs.set('page', String(params.page)); + if (params.page_size) qs.set('page_size', String(params.page_size)); + const q = qs.toString(); + return data(`/notifications/${q ? `?${q}` : ''}`); + }, + + markRead: (id: string) => + request(`/notifications/${id}/read`, { method: 'PUT' }), + + markAllRead: (type?: string) => + request(`/notifications/read-all${type ? `?type=${encodeURIComponent(type)}` : ''}`, { method: 'PUT' }), + + unreadCount: () => + data<{ count: number }>('/notifications/unread-count'), +}; + +// ─── Admin 后台 ────────────────────────────────────────────────────────────────── +// 后端契约 (app/api/admin/*):绝大多数端点返回统一信封 {code,data,message}, +// 但 dashboard.py 的 `/admin/dashboard/stats` 返回**裸 dict**(无 code/data 包裹), +// `/admin/dashboard/daily-stats` 返回 {code,data:{days,data}}。 +// 因此 adminApi 不复用 data(),改用 rawAdmin() 解包并按端点分别处理 envelope。 + +export interface AdminDashboardStats { + total_users: number; + active_users: number; + total_models: number; + enabled_models: number; + total_conversations: number; + active_conversations: number; + valid_conversations: number; + total_knowledge_bases: number; + total_documents: number; + today_orders: number; + today_subscription_orders: number; + today_tokens: number; + week_tokens: number; + total_tokens: number; + today_conversations: number; + server_time: string; +} + +export interface AdminDailyStat { + date: string; + tokens: number; +} + +export interface AdminDailyStats { + days: string[]; + data: AdminDailyStat[]; +} + +// 统一信封解包(与 data() 相同语义,但显式标记 admin 域,便于后续差异演进) +async function adminData(path: string, options: RequestInit = {}): Promise { + const env = await request<{ code: number; data: T; message: string }>(path, options); + return env.data; +} + +export const adminApi = { + // /stats 裸 dict(无 code/data 包裹)→ 走 raw request,直接返回 json + getStats: () => request('/admin/dashboard/stats'), + + // /daily-stats 标准信封 {code,data:{days,data}} + getDailyStats: () => adminData('/admin/dashboard/daily-stats'), +}; + +// ─── Evolution AI 自进化 ──────────────────────────────────────────────────────── +// 后端契约 (evolution.py + services/evolution_service.py):全部 {code,data,...} +// 技能状态 state: main | staging | baby | frozen +// 事件类型 event_type: created | edited | promoted | frozen | ...(字符串透传) + +export type EvolutionSkillState = 'main' | 'staging' | 'baby' | 'frozen'; + +export interface EvolutionSkill { + id: string; + skill_name: string; + description: string; + tags: string[]; + category: string; + state: EvolutionSkillState; + use_count: number; + version: number; + last_used_at: string | null; + created_at: string | null; + updated_at: string | null; + skill_md_content?: string | null; // 仅 detail 返回 +} + +export interface EvolutionEvent { + id: string; + skill_name: string; + event_type: string; + description: string; + version_before: number | null; + version_after: number | null; + created_at: string | null; +} + +export interface EvolutionTrajectory { + id: string; + conversation_id: string; + file_path: string | null; + status: string; + atom_count: number | null; + token_used: number | null; + created_at: string | null; + processed_at: string | null; + weightscore?: number; + atom_id?: string | null; +} + +export interface EvolutionSkillTree { + categories: { name: string; skills: EvolutionSkill[] }[]; + total_count: number; + main_count: number; + baby_count: number; + staging_count: number; + frozen_count: number; +} + +export interface EvolutionStatistics { + ranking: { skill_name: string; category: string; use_count: number; state: string; last_used_at: string | null }[]; + total_use_count: number; + category_distribution: { category: string; count: number }[]; + daily_trend: { date: string; count: number }[]; + low_use_skills: EvolutionSkill[]; +} + +export interface EvolutionDimension { + label: string; + value: number; + maxValue: number; +} + +export interface EvolutionProfile { + level: number; + level_title: string; + total_skills: number; + active_skills: number; + staging_skills: number; + baby_skills: number; + total_use_count: number; + dimensions: EvolutionDimension[]; + tags: string[]; + growth_suggestions: string[]; +} + +export interface EvolutionPipelineStatus { + scheduler_running: boolean; + watcher_status: string; + pipeline_status: string; + discovered_count: number; + done_count: number; + total_skills: number; + total_trajectories: number; +} + +export interface EvolutionWatcherStatus { + user_id: string; + scheduler_running: boolean; + watcher_status: string; + pipeline_status: string; + pipeline_processed: number; + watcher_error: string | null; + skill_count: number; + trajectory_count: number; +} + +export interface EvolutionReport { + id: string; + report_type: string; + period_start: string | null; + period_end: string | null; + created_at: string | null; +} + +export interface EvolutionReportDetail extends EvolutionReport { + user_id: string; + report_data: Record; +} + +export const evolutionApi = { + // 技能管理 + listSkills: (params: { state?: string; keyword?: string; sort_by?: string; order?: string } = {}) => { + const qs = new URLSearchParams(); + if (params.state) qs.set('state', params.state); + if (params.keyword) qs.set('keyword', params.keyword); + if (params.sort_by) qs.set('sort_by', params.sort_by); + if (params.order) qs.set('order', params.order); + const q = qs.toString(); + return data(`/evolution/skills${q ? `?${q}` : ''}`); + }, + + getSkillDetail: (skillName: string) => + data(`/evolution/skills/${encodeURIComponent(skillName)}`), + + // 进化事件 + listEvents: (params: { event_type?: string; skill_name?: string; limit?: number } = {}) => { + const qs = new URLSearchParams(); + if (params.event_type) qs.set('event_type', params.event_type); + if (params.skill_name) qs.set('skill_name', params.skill_name); + if (params.limit) qs.set('limit', String(params.limit)); + const q = qs.toString(); + return data(`/evolution/events${q ? `?${q}` : ''}`); + }, + + // 驾驶舱 + getSkillTree: () => data('/evolution/dashboard/skill-tree'), + getTimeline: (event_type?: string) => + data(`/evolution/dashboard/timeline${event_type ? `?event_type=${encodeURIComponent(event_type)}` : ''}`), + getStatistics: () => data('/evolution/dashboard/statistics'), + getProfile: () => data('/evolution/dashboard/profile'), + + // 流水线 / Watcher + getPipelineStatus: () => data('/evolution/pipeline/status'), + getWatcherStatus: () => data('/evolution/watcher/status'), + + // 报告 + listReports: (report_type?: string) => + data(`/evolution/reports${report_type ? `?report_type=${encodeURIComponent(report_type)}` : ''}`), + getReportDetail: (reportId: string) => + data(`/evolution/reports/${reportId}`), +}; diff --git a/frontend-v2/src/pages/admin/AdminComingSoon.tsx b/frontend-v2/src/pages/admin/AdminComingSoon.tsx new file mode 100644 index 0000000..7e6bf50 --- /dev/null +++ b/frontend-v2/src/pages/admin/AdminComingSoon.tsx @@ -0,0 +1,58 @@ +import type { LucideIcon } from 'lucide-react'; +import { Sparkles, Construction } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; + +interface AdminComingSoonProps { + title: string; + icon: LucideIcon; + description?: string; + features?: string[]; +} + +/** + * admin 后台统一「模块开发中」占位页(amber 金暖白)。 + * 旧 admin 共 33 个二级模块,初期仅 Dashboard 为真实数据页, + * 其余模块用此占位,保证后台导航点击不出现空白断崖。 + */ +export function AdminComingSoon({ title, icon: Icon, description, features }: AdminComingSoonProps) { + return ( +
+ + +
+ +
+ +
+

{title}

+ + + 建设中 + +
+ +

+ {description ?? `${title} 模块正在开发中,敬请期待。上线后这里将提供完整的后台管理能力。`} +

+ + {features && features.length > 0 && ( +
+ {features.map((f, i) => ( +
+ + {f} +
+ ))} +
+ )} + +
+ + 我们将尽快为您带来 {title} +
+
+
+
+ ); +} diff --git a/frontend-v2/src/pages/admin/AdminDashboard.tsx b/frontend-v2/src/pages/admin/AdminDashboard.tsx new file mode 100644 index 0000000..752316f --- /dev/null +++ b/frontend-v2/src/pages/admin/AdminDashboard.tsx @@ -0,0 +1,180 @@ +import { useQuery } from '@tanstack/react-query'; +import { adminApi, type AdminDailyStat } from '@/api/client'; +import { Card, CardContent } from '@/components/ui/card'; +import { useCurrentUser } from '@/hooks/useApi'; +import { Users, Cpu, MessageSquare, BookOpen, Coins, Activity, TrendingUp } from 'lucide-react'; + +function formatNumber(n: number | undefined): string { + if (n === undefined || n === null) return '—'; + return n.toLocaleString('zh-CN'); +} + +function formatBjTime(iso: string | undefined): string { + if (!iso) return '—'; + const d = new Date(iso); + if (isNaN(d.getTime())) return iso; + // 北京时间 = UTC+8 + const bj = new Date(d.getTime() + 8 * 3600 * 1000); + const p = (x: number) => String(x).padStart(2, '0'); + return `${bj.getUTCFullYear()}-${p(bj.getUTCMonth() + 1)}-${p(bj.getUTCDate())} ${p(bj.getUTCHours())}:${p(bj.getUTCMinutes())}:${p(bj.getUTCSeconds())}`; +} + +function StatCard({ + icon: Icon, + label, + value, + sub, +}: { + icon: React.ReactNode; + label: string; + value: string; + sub?: string; +}) { + return ( + + +
+ {label} +
+ {Icon} +
+
+
{value}
+ {sub &&
{sub}
} +
+
+ ); +} + +// 简易柱状图(纯 SVG,amber 金暖白) +function TokenBarChart({ data }: { data: AdminDailyStat[] }) { + if (!data || data.length === 0) { + return
暂无近 7 日 Token 数据
; + } + const max = Math.max(...data.map((d) => d.tokens), 1); + const width = 560; + const height = 180; + const gap = 16; + const barW = (width - gap * (data.length - 1)) / data.length; + return ( + + {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 ( + + + + {d.date.slice(5)} + + + ); + })} + + ); +} + +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 ( + + + + + {Math.round(pct * 100)}% + + + ); +} + +export function AdminDashboard() { + const { data: user } = useCurrentUser(); + const statsQuery = useQuery({ + queryKey: ['admin', 'dashboard', 'stats'], + queryFn: adminApi.getStats, + }); + const dailyQuery = useQuery({ + queryKey: ['admin', 'dashboard', 'daily-stats'], + queryFn: adminApi.getDailyStats, + }); + + const stats = statsQuery.data; + const daily = dailyQuery.data?.data ?? []; + + return ( +
+ {/* 页头 */} +
+
+

数据看板

+

+ 欢迎回来,{user?.username || '管理员'}。平台实时运营概览。 +

+
+
+
服务器时间(北京)
+
{formatBjTime(stats?.server_time)}
+
+
+ + {/* 错误态 */} + {statsQuery.isError && ( +
+ 看板数据加载失败:{(statsQuery.error as Error)?.message} +
+ )} + + {/* 统计卡片 6 张 */} +
+ } label="总用户数" value={formatNumber(stats?.total_users)} sub={`活跃 ${formatNumber(stats?.active_users)}`} /> + } label="模型总数" value={formatNumber(stats?.total_models)} sub={`启用 ${formatNumber(stats?.enabled_models)}`} /> + } label="对话总数" value={formatNumber(stats?.total_conversations)} sub={`今日 ${formatNumber(stats?.today_conversations)}`} /> + } label="知识库" value={formatNumber(stats?.total_knowledge_bases)} sub={`文档 ${formatNumber(stats?.total_documents)}`} /> + } label="累计 Token" value={formatNumber(stats?.total_tokens)} sub={`今日 ${formatNumber(stats?.today_tokens)}`} /> + } label="今日订单" value={formatNumber(stats?.today_orders)} sub={`订阅 ${formatNumber(stats?.today_subscription_orders)}`} /> +
+ + {/* 图表区 */} +
+ + +
+ +

近 7 日 Token 消耗

+
+ {dailyQuery.isLoading ? ( +
加载中…
+ ) : ( + + )} +
+
+ + + +

模型启用率

+
+ +
+ 启用 {formatNumber(stats?.enabled_models)} / 共 {formatNumber(stats?.total_models)} +
+
+
+
+
+
+ ); +} diff --git a/frontend-v2/src/pages/admin/AdminLayout.tsx b/frontend-v2/src/pages/admin/AdminLayout.tsx new file mode 100644 index 0000000..00b27fa --- /dev/null +++ b/frontend-v2/src/pages/admin/AdminLayout.tsx @@ -0,0 +1,214 @@ +import { useState } from 'react'; +import { NavLink, Outlet, useNavigate } from 'react-router-dom'; +import { cn } from '@/lib/utils'; +import { useCurrentUser } from '@/hooks/useApi'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { + LayoutDashboard, + Users, + Cpu, + CreditCard, + Boxes, + Crown, + ScrollText, + Zap, + Bot, + Puzzle, + BookOpen, + FileBox, + HardDrive, + Workflow, + Code2, + AppWindow, + TerminalSquare, + Share2, + Megaphone, + ShoppingBag, + Receipt, + Building2, + BarChart3, + ChevronLeft, + ChevronRight, + ExternalLink, + LogOut, + ShieldCheck, +} from 'lucide-react'; + +interface AdminNavItem { + path: string; + label: string; + icon: React.ReactNode; + implemented?: boolean; // true = 真实数据页;false = ComingSoon 占位 +} + +interface AdminNavGroup { + title: string; + items: AdminNavItem[]; +} + +// 导航结构对照旧 admin(AdminLayout.tsx 6 分组 33 项), +// 初期仅 Dashboard 为真实数据页,其余以 ComingSoon 占位,保持导航不出现断崖。 +const adminNavGroups: AdminNavGroup[] = [ + { + title: '概览', + items: [ + { path: '/admin', label: '数据看板', icon: , implemented: true }, + ], + }, + { + title: '用户与权限', + items: [ + { path: '/admin/users', label: '用户管理', icon: }, + { path: '/admin/roles', label: '角色权限', icon: }, + { path: '/admin/tenant', label: '租户管理', icon: }, + ], + }, + { + title: '模型与资源', + items: [ + { path: '/admin/models', label: '模型管理', icon: }, + { path: '/admin/knowledge', label: '知识库', icon: }, + { path: '/admin/assets', label: '素材管理', icon: }, + { path: '/admin/storage', label: '存储管理', icon: }, + ], + }, + { + title: '智能体与工具', + items: [ + { path: '/admin/agents', label: '智能体', icon: }, + { path: '/admin/mcp', label: 'MCP 工具', icon: }, + { path: '/admin/mcp-market', label: 'MCP 市场', icon: }, + { path: '/admin/skills', label: '技能管理', icon: }, + { path: '/admin/workflow', label: '工作流', icon: }, + { path: '/admin/automation', label: '自动化', icon: }, + ], + }, + { + title: '应用与分发', + items: [ + { path: '/admin/apps', label: '应用管理', icon: }, + { path: '/admin/code-exec', label: '代码执行', icon: }, + { path: '/admin/distributions', label: '分发渠道', icon: }, + { path: '/admin/ads', label: '广告管理', icon: }, + { path: '/admin/market', label: '应用市场', icon: }, + ], + }, + { + title: '运营与计费', + items: [ + { path: '/admin/payment', label: '支付配置', icon: }, + { path: '/admin/billing', label: '账单计费', icon: }, + { path: '/admin/membership', label: '会员体系', icon: }, + { path: '/admin/services', label: '增值服务', icon: }, + { path: '/admin/logs', label: '日志审计', icon: }, + { path: '/admin/developer', label: '开发者', icon: }, + { path: '/admin/analytics', label: '数据分析', icon: }, + ], + }, +]; + +export function AdminLayout() { + const [collapsed, setCollapsed] = useState(false); + const navigate = useNavigate(); + const { data: user } = useCurrentUser(); + + const handleLogout = () => { + localStorage.removeItem('auth_token'); + navigate('/login'); + }; + + return ( +
+ {/* 侧边栏 */} + + + {/* 主区域 */} +
+
+ +
+
+
+ ); +} diff --git a/frontend-v2/src/pages/admin/AdminPlaceholders.tsx b/frontend-v2/src/pages/admin/AdminPlaceholders.tsx new file mode 100644 index 0000000..716c03b --- /dev/null +++ b/frontend-v2/src/pages/admin/AdminPlaceholders.tsx @@ -0,0 +1,308 @@ +import { AdminComingSoon } from './AdminComingSoon'; +import { + Users, + ShieldCheck, + Building2, + Cpu, + BookOpen, + FileBox, + HardDrive, + Bot, + Puzzle, + Zap, + Workflow, + TerminalSquare, + AppWindow, + Code2, + Share2, + Megaphone, + ShoppingBag, + CreditCard, + Receipt, + Crown, + Boxes, + ScrollText, + BarChart3, +} from 'lucide-react'; + +/** + * admin 后台占位页集中导出。 + * 旧 AdminLayout 共 33 个二级模块,初期除 Dashboard(真实数据页)外, + * 其余 32 个模块统一用 AdminComingSoon 占位,待后续阶段逐个迁移真实数据。 + * 每个导出对应一条侧边栏导航路由。 + */ + +export function UsersPage() { + return ( + + ); +} + +export function RolesPage() { + return ( + + ); +} + +export function TenantPage() { + return ( + + ); +} + +export function ModelsPage() { + return ( + + ); +} + +export function KnowledgeAdminPage() { + return ( + + ); +} + +export function AssetsPage() { + return ( + + ); +} + +export function StoragePage() { + return ( + + ); +} + +export function AgentsAdminPage() { + return ( + + ); +} + +export function McpPage() { + return ( + + ); +} + +export function McpMarketPage() { + return ( + + ); +} + +export function SkillsAdminPage() { + return ( + + ); +} + +export function WorkflowAdminPage() { + return ( + + ); +} + +export function AutomationPage() { + return ( + + ); +} + +export function AppsAdminPage() { + return ( + + ); +} + +export function CodeExecPage() { + return ( + + ); +} + +export function DistributionsPage() { + return ( + + ); +} + +export function AdsPage() { + return ( + + ); +} + +export function MarketAdminPage() { + return ( + + ); +} + +export function PaymentPage() { + return ( + + ); +} + +export function BillingPage() { + return ( + + ); +} + +export function MembershipPage() { + return ( + + ); +} + +export function ServicesPage() { + return ( + + ); +} + +export function LogsPage() { + return ( + + ); +} + +export function DeveloperPage() { + return ( + + ); +} + +export function AnalyticsPage() { + return ( + + ); +}