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 { 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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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