Phase 2.4 frontend: Knowledge admin page + API methods + routes + nav

This commit is contained in:
qyyfw committed 2026-07-02 23:03:53 +08:00
1 parent 855e1cb9c6
commit 8c2e5763ef
4 files changed
+900 -249

No files matched your search

+128 -123
View File
@@ -1,123 +1,128 @@
import React, { useState, useEffect } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import type { User } from '@/types';
import { AuthContext, AuthContextType } from '@/contexts/AuthContext';
import { authAPI } from '@/api';
// Layouts
import FrontLayout from '@/components/layout/FrontLayout';
import AdminLayout from '@/components/layout/AdminLayout';
// Front Pages
import LandingPage from '@/pages/front/LandingPage';
import LoginPage from '@/pages/front/LoginPage';
import ChatPage from '@/pages/front/ChatPage';
import ChannelPage from '@/pages/front/ChannelPage';
import KnowledgePage from '@/pages/front/KnowledgePage';
import MarketPage from '@/pages/front/MarketPage';
import TokenDashboardPage from '@/pages/front/TokenDashboardPage';
import PricingPage from '@/pages/front/PricingPage';
import ProfilePage from '@/pages/front/ProfilePage';
import NotificationPage from '@/pages/front/NotificationPage';
import AssetsPage from '@/pages/front/AssetsPage';
import NovelPreviewPage from '@/pages/front/NovelPreviewPage';
import EvolutionDashboard from '@/pages/front/EvolutionDashboard';
// Admin Pages
import AdminDashboard from '@/pages/admin/Dashboard';
import AdminUsers from '@/pages/admin/Users';
import AdminModels from '@/pages/admin/Models';
import AdminSiteSettings from '@/pages/admin/SiteSettings';
import AdminPayment from '@/pages/admin/Payment';
import AdminServices from '@/pages/admin/Services';
import AdminMembership from '@/pages/admin/Membership';
import AdminAds from '@/pages/admin/Ads';
import AdminMarket from '@/pages/admin/Market';
import AdminLogs from '@/pages/admin/Logs';
import AdminSkills from '@/pages/admin/Skills';
const App: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// 初始化:从localStorage恢复登录状态
useEffect(() => {
const token = localStorage.getItem('aiflow_token');
const cachedUser = localStorage.getItem('aiflow_user');
if (token && cachedUser) {
try {
setUser(JSON.parse(cachedUser));
} catch {
localStorage.removeItem('aiflow_token');
localStorage.removeItem('aiflow_user');
}
}
setLoading(false);
}, []);
const login = (u: User) => {
setUser(u);
localStorage.setItem('aiflow_user', JSON.stringify(u));
};
const logout = () => {
setUser(null);
localStorage.removeItem('aiflow_token');
localStorage.removeItem('aiflow_user');
};
const isAdmin = user?.role === 'admin';
if (loading) {
return <div className="loading-screen">加载中...</div>;
}
return (
<AuthContext.Provider value={{ user, login, logout, isAdmin }}>
<Routes>
{/* Login */}
<Route path="/login" element={
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage />
} />
{/* Root - Landing for guests, redirect for authenticated users */}
<Route path="/" element={
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage />
} />
{/* Front Routes (authenticated only) */}
<Route path="/" element={<FrontLayout />}>
<Route path="chat" element={<ChatPage />} />
<Route path="chat/:id" element={<ChatPage />} />
<Route path="channels" element={<ChannelPage />} />
<Route path="knowledge" element={<KnowledgePage />} />
<Route path="market" element={<MarketPage />} />
<Route path="token" element={<TokenDashboardPage />} />
<Route path="pricing" element={<PricingPage />} />
<Route path="profile" element={<ProfilePage />} />
<Route path="notifications" element={<NotificationPage />} />
<Route path="assets" element={<AssetsPage />} />
<Route path="assets/novel/:id" element={<NovelPreviewPage />} />
<Route path="evolution" element={<EvolutionDashboard />} />
</Route>
{/* Admin Routes */}
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<AdminUsers />} />
<Route path="models" element={<AdminModels />} />
<Route path="settings" element={<AdminSiteSettings />} />
<Route path="payment" element={<AdminPayment />} />
<Route path="services" element={<AdminServices />} />
<Route path="membership" element={<AdminMembership />} />
<Route path="ads" element={<AdminAds />} />
<Route path="market" element={<AdminMarket />} />
<Route path="logs" element={<AdminLogs />} />
<Route path="skills" element={<AdminSkills />} /> </Route>
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</AuthContext.Provider>
);
};
export default App;
import React, { useState, useEffect } from 'react';␍
import { Routes, Route, Navigate } from 'react-router-dom';␍
import type { User } from '@/types';␍
import { AuthContext, AuthContextType } from '@/contexts/AuthContext';␍
import { authAPI } from '@/api';␍
␍
// Layouts␍
import FrontLayout from '@/components/layout/FrontLayout';␍
import AdminLayout from '@/components/layout/AdminLayout';␍
␍
// Front Pages␍
import LandingPage from '@/pages/front/LandingPage';␍
import LoginPage from '@/pages/front/LoginPage';␍
import ChatPage from '@/pages/front/ChatPage';␍
import ChannelPage from '@/pages/front/ChannelPage';␍
import KnowledgePage from '@/pages/front/KnowledgePage';␍
import MarketPage from '@/pages/front/MarketPage';␍
import TokenDashboardPage from '@/pages/front/TokenDashboardPage';␍
import PricingPage from '@/pages/front/PricingPage';␍
import ProfilePage from '@/pages/front/ProfilePage';␍
import NotificationPage from '@/pages/front/NotificationPage';␍
import AssetsPage from '@/pages/front/AssetsPage';␍
import NovelPreviewPage from '@/pages/front/NovelPreviewPage';␍
import EvolutionDashboard from '@/pages/front/EvolutionDashboard';␍
␍
// Admin Pages␍
import AdminDashboard from '@/pages/admin/Dashboard';␍
import AdminUsers from '@/pages/admin/Users';␍
import AdminModels from '@/pages/admin/Models';␍
import AdminSiteSettings from '@/pages/admin/SiteSettings';␍
import AdminPayment from '@/pages/admin/Payment';␍
import AdminServices from '@/pages/admin/Services';␍
import AdminMembership from '@/pages/admin/Membership';␍
import AdminAds from '@/pages/admin/Ads';␍
import AdminMarket from '@/pages/admin/Market';␍
import AdminLogs from '@/pages/admin/Logs';␍
import AdminSkills from '@/pages/admin/Skills'
import AdminAgents from '@/pages/admin/Agents'
import AdminKnowledge from '@/pages/admin/Knowledge';;␍
␍
const App: React.FC = () => {␍
const [user, setUser] = useState<User | null>(null);␍
const [loading, setLoading] = useState(true);␍
␍
// 初始化:从localStorage恢复登录状态␍
useEffect(() => {␍
const token = localStorage.getItem('aiflow_token');␍
const cachedUser = localStorage.getItem('aiflow_user');␍
if (token && cachedUser) {␍
try {␍
setUser(JSON.parse(cachedUser));␍
} catch {␍
localStorage.removeItem('aiflow_token');␍
localStorage.removeItem('aiflow_user');␍
}␍
}␍
setLoading(false);␍
}, []);␍
␍
const login = (u: User) => {␍
setUser(u);␍
localStorage.setItem('aiflow_user', JSON.stringify(u));␍
};␍
const logout = () => {␍
setUser(null);␍
localStorage.removeItem('aiflow_token');␍
localStorage.removeItem('aiflow_user');␍
};␍
const isAdmin = user?.role === 'admin';␍
␍
if (loading) {␍
return <div className="loading-screen">加载中...</div>;␍
}␍
␍
return (␍
<AuthContext.Provider value={{ user, login, logout, isAdmin }}>␍
<Routes>␍
{/* Login */}␍
<Route path="/login" element={␍
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage />␍
} />␍
␍
{/* Root - Landing for guests, redirect for authenticated users */}␍
<Route path="/" element={␍
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage />␍
} />␍
␍
{/* Front Routes (authenticated only) */}␍
<Route path="/" element={<FrontLayout />}>␍
<Route path="chat" element={<ChatPage />} />␍
<Route path="chat/:id" element={<ChatPage />} />␍
<Route path="channels" element={<ChannelPage />} />␍
<Route path="knowledge" element={<KnowledgePage />} />␍
<Route path="market" element={<MarketPage />} />␍
<Route path="token" element={<TokenDashboardPage />} />␍
<Route path="pricing" element={<PricingPage />} />␍
<Route path="profile" element={<ProfilePage />} />␍
<Route path="notifications" element={<NotificationPage />} />␍
<Route path="assets" element={<AssetsPage />} />␍
<Route path="assets/novel/:id" element={<NovelPreviewPage />} />␍
<Route path="evolution" element={<EvolutionDashboard />} />␍
</Route>␍
␍
{/* Admin Routes */}␍
<Route path="/admin" element={<AdminLayout />}>␍
<Route index element={<AdminDashboard />} />␍
<Route path="users" element={<AdminUsers />} />␍
<Route path="models" element={<AdminModels />} />␍
<Route path="settings" element={<AdminSiteSettings />} />␍
<Route path="payment" element={<AdminPayment />} />␍
<Route path="services" element={<AdminServices />} />␍
<Route path="membership" element={<AdminMembership />} />␍
<Route path="ads" element={<AdminAds />} />␍
<Route path="market" element={<AdminMarket />} />␍
<Route path="logs" element={<AdminLogs />} />␍
␍
<Route path="skills" element={<AdminSkills />} />
<Route path="agents" element={<AdminAgents />} />
<Route path="knowledge" element={<AdminKnowledge />} />
</Route>␍
␍
<Route path="*" element={<Navigate to="/" />} />␍
</Routes>␍
</AuthContext.Provider>␍
);␍
};␍
␍
export default App;␍
+19
View File
@@ -455,6 +455,25 @@ export const adminAPI = {
publishAdminSkill: (id: string, data: any) => post<any>(`/admin/skills/${id}/publish`, data),
unpublishAdminSkill: (id: string) => post<any>(`/admin/skills/${id}/unpublish`),
getAdminSkillsStats: () => get<any>('/admin/skills/stats/overview'),
// Agent管理 (Phase 2.3)
getAdminAgents: (params?: any) => {
const sp = new URLSearchParams();
if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); });
return get<any>(`/admin/agents/${sp.toString() ? `?${sp.toString()}` : ''}`);
},
getAdminAgent: (id: string) => get<any>(`/admin/agents/${id}`),
createAdminAgent: (data: any) => post<any>('/admin/agents/', data),
updateAdminAgent: (id: string, data: any) => put<any>(`/admin/agents/${id}`, data),
deleteAdminAgent: (id: string) => del<any>(`/admin/agents/${id}`),
toggleAdminAgent: (id: string, isActive: boolean) => put<any>(`/admin/agents/${id}/toggle`, { is_active: isActive }),
getAdminAgentsStats: () => get<any>('/admin/agents/stats/overview'),
getAdminAgentReviews: (id: string) => get<any>(`/admin/agents/${id}/reviews`),
getAdminAvailableSkills: (params?: any) => {
const sp = new URLSearchParams();
if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); });
return get<any>(`/admin/agents/available/skills${sp.toString() ? `?${sp.toString()}` : ''}`);
},
getAdminAvailableModels: () => get<any>('/admin/agents/available/models'),
};
// ===== AI自进化API =====
export const evolutionAPI = {
+128 -126
View File
@@ -1,126 +1,128 @@
import React, { useState } from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import './AdminLayout.css';
const adminNavItems = [
{ path: '/admin', icon: 'dashboard', label: '平台驾驶舱', exact: true },
{ path: '/admin/users', icon: 'users', label: '用户管理' },
{ path: '/admin/models', icon: 'models', label: '大模型库管理' },
{ path: '/admin/settings', icon: 'settings', label: '网站基础设置' },
{ path: '/admin/payment', icon: 'payment', label: '支付渠道配置' },
{ path: '/admin/services', icon: 'services', label: '增值服务管理' },
{ path: '/admin/membership', icon: 'membership', label: '会员订阅体系' },
{ path: '/admin/ads', icon: 'ads', label: '广告位管理' },
{ path: '/admin/market', icon: 'market', label: '应用市场审核' },
{ path: '/admin/logs', icon: 'logs', label: '日志与风控' },
{ path: '/admin/skills', icon: 'skills', label: '技能管理' },];
const adminIconPaths: Record<string, JSX.Element> = {
dashboard: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>,
users: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
models: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>,
settings: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>,
payment: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>,
services: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>,
membership: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>,
ads: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>,
market: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></svg>,
logs: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>,
skills: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>,};
const AdminLayout: React.FC = () => {
const { user, logout } = useAuth();
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(false);
if (!user || user.role !== 'admin') {
navigate('/login');
return null;
}
return (
<div className="admin-layout">
{/* Admin Sidebar */}
<aside className={`admin-sidebar ${collapsed ? 'collapsed' : ''}`}>
<div className="admin-sidebar-header">
<div className="sidebar-logo">
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#1A1A2E"/>
<path d="M8 20V12l4 4 4-4v8" stroke="#6C63FF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
<path d="M6 22h20" stroke="#6C63FF" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
</svg>
{!collapsed && <span className="sidebar-brand admin-brand">Qihu-AIFlow 管理后台</span>}
</div>
<button className="collapse-btn" onClick={() => setCollapsed(!collapsed)}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
</div>
<nav className="admin-nav">
{adminNavItems.map(item => (
<NavLink
key={item.path}
to={item.path}
end={item.exact}
className={({ isActive }) => `admin-nav-item ${isActive ? 'active' : ''}`}
>
<span className="nav-icon">{adminIconPaths[item.icon]}</span>
{!collapsed && <span className="nav-label">{item.label}</span>}
</NavLink>
))}
</nav>
<div className="admin-sidebar-footer">
<NavLink to="/chat" className="admin-nav-item">
<span className="nav-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</span>
{!collapsed && <span className="nav-label">返回前台</span>}
</NavLink>
<button className="admin-nav-item logout-btn" onClick={() => { logout(); navigate('/login'); }}>
<span className="nav-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
</span>
{!collapsed && <span className="nav-label">退出登录</span>}
</button>
</div>
{!collapsed && (
<div className="admin-version-info">
Qihu-AIFlow v2.0.0
</div>
)}
</aside>
{/* Admin Main Content */}
<main className="admin-main">
<header className="admin-topbar">
<div className="admin-topbar-left">
<span className="admin-welcome">欢迎,{user.username}</span>
</div>
<div className="admin-topbar-right">
<a href="#/chat" className="admin-topbar-action">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<span>返回前台</span>
</a>
<div className="admin-user-tag">
<div className="user-avatar-sm admin-avatar">
{user.username.charAt(0).toUpperCase()}
</div>
<span>{user.username}</span>
</div>
</div>
</header>
<div className="admin-content">
<Outlet />
</div>
</main>
</div>
);
};
export default AdminLayout;
import React, { useState } from 'react';␍
import { Outlet, NavLink, useNavigate } from 'react-router-dom';␍
import { useAuth } from '@/contexts/AuthContext';␍
import './AdminLayout.css';␍
␍
const adminNavItems = [␍
{ path: '/admin', icon: 'dashboard', label: '平台驾驶舱', exact: true },␍
{ path: '/admin/users', icon: 'users', label: '用户管理' },␍
{ path: '/admin/models', icon: 'models', label: '大模型库管理' },␍
{ path: '/admin/settings', icon: 'settings', label: '网站基础设置' },␍
{ path: '/admin/payment', icon: 'payment', label: '支付渠道配置' },␍
{ path: '/admin/services', icon: 'services', label: '增值服务管理' },␍
{ path: '/admin/membership', icon: 'membership', label: '会员订阅体系' },␍
{ path: '/admin/ads', icon: 'ads', label: '广告位管理' },␍
{ path: '/admin/market', icon: 'market', label: '应用市场审核' },␍
{ path: '/admin/logs', icon: 'logs', label: '日志与风控' },␍
␍
{ path: '/admin/agents', icon: 'agents', label: 'Agent管理' },
{ path: '/admin/knowledge', icon: 'knowledge', label: '知识库管理' },
{ path: '/admin/skills', icon: 'skills', label: '技能管理' },];␍
␍
const adminIconPaths: Record<string, JSX.Element> = {␍
dashboard: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>,␍
users: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,␍
models: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>,␍
settings: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>,␍
payment: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>,␍
services: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>,␍
membership: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>,␍
ads: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>,␍
market: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></svg>,␍
logs: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>,␍
␍
skills: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>,};␍
␍
const AdminLayout: React.FC = () => {␍
const { user, logout } = useAuth();␍
const navigate = useNavigate();␍
const [collapsed, setCollapsed] = useState(false);␍
␍
if (!user || user.role !== 'admin') {␍
navigate('/login');␍
return null;␍
}␍
␍
return (␍
<div className="admin-layout">␍
{/* Admin Sidebar */}␍
<aside className={`admin-sidebar ${collapsed ? 'collapsed' : ''}`}>␍
<div className="admin-sidebar-header">␍
<div className="sidebar-logo">␍
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">␍
<rect width="32" height="32" rx="8" fill="#1A1A2E"/>␍
<path d="M8 20V12l4 4 4-4v8" stroke="#6C63FF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>␍
<path d="M6 22h20" stroke="#6C63FF" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>␍
</svg>␍
{!collapsed && <span className="sidebar-brand admin-brand">Qihu-AIFlow 管理后台</span>}␍
</div>␍
<button className="collapse-btn" onClick={() => setCollapsed(!collapsed)}>␍
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">␍
<line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/>␍
</svg>␍
</button>␍
</div>␍
␍
<nav className="admin-nav">␍
{adminNavItems.map(item => (␍
<NavLink␍
key={item.path}␍
to={item.path}␍
end={item.exact}␍
className={({ isActive }) => `admin-nav-item ${isActive ? 'active' : ''}`}␍
>␍
<span className="nav-icon">{adminIconPaths[item.icon]}</span>␍
{!collapsed && <span className="nav-label">{item.label}</span>}␍
</NavLink>␍
))}␍
</nav>␍
␍
<div className="admin-sidebar-footer">␍
<NavLink to="/chat" className="admin-nav-item">␍
<span className="nav-icon">␍
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>␍
</span>␍
{!collapsed && <span className="nav-label">返回前台</span>}␍
</NavLink>␍
<button className="admin-nav-item logout-btn" onClick={() => { logout(); navigate('/login'); }}>␍
<span className="nav-icon">␍
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>␍
</span>␍
{!collapsed && <span className="nav-label">退出登录</span>}␍
</button>␍
</div>␍
{!collapsed && (␍
<div className="admin-version-info">␍
Qihu-AIFlow v2.0.0␍
</div>␍
)}␍
</aside>␍
␍
{/* Admin Main Content */}␍
<main className="admin-main">␍
<header className="admin-topbar">␍
<div className="admin-topbar-left">␍
<span className="admin-welcome">欢迎,{user.username}</span>␍
</div>␍
<div className="admin-topbar-right">␍
<a href="#/chat" className="admin-topbar-action">␍
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>␍
<span>返回前台</span>␍
</a>␍
<div className="admin-user-tag">␍
<div className="user-avatar-sm admin-avatar">␍
{user.username.charAt(0).toUpperCase()}␍
</div>␍
<span>{user.username}</span>␍
</div>␍
</div>␍
</header>␍
<div className="admin-content">␍
<Outlet />␍
</div>␍
</main>␍
</div>␍
);␍
};␍
␍
export default AdminLayout;␍
+625
View File
@@ -0,0 +1,625 @@
import React, { useState, useEffect } from 'react';
import { adminAPI } from '@/api';
interface KnowledgeBaseItem {
id: string;
name: string;
user_id: string;
owner_name?: string;
description?: string;
directory_path: string;
document_count: number;
knowledge_graph_count: number;
is_official: boolean;
status: string;
external_service_id?: string;
external_id?: string;
external_sync_status: string;
external_config?: any;
total_storage_bytes?: number;
total_storage_mb?: number;
created_at: string;
updated_at: string;
doc_stats?: Record<string, { count: number; size: number }>;
graph_stats?: { nodes: number; edges: number };
}
interface ExternalService {
id: string;
name: string;
service_type: string;
api_url: string;
api_key?: string;
config?: any;
is_enabled: boolean;
status: string;
created_at?: string;
updated_at?: string;
}
interface Stats {
total_knowledge_bases: number;
total_documents: number;
total_storage_bytes: number;
total_storage_display: string;
official_kb: number;
user_kb: number;
active_kb: number;
kb_with_graph: number;
external_kb: number;
recent_kb_7d: number;
total_graph_nodes: number;
total_graph_edges: number;
doc_status_distribution: Record<string, number>;
file_type_distribution: Record<string, number>;
}
const Knowledge: React.FC = () => {
// Data
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBaseItem[]>([]);
const [stats, setStats] = useState<Stats | null>(null);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize] = useState(15);
// Filters
const [keyword, setKeyword] = useState('');
const [filterStatus, setFilterStatus] = useState('');
const [filterOfficial, setFilterOfficial] = useState('');
// External Services
const [extServices, setExtServices] = useState<ExternalService[]>([]);
const [showExtModal, setShowExtModal] = useState(false);
const [editingExtService, setEditingExtService] = useState<ExternalService | null>(null);
const [extForm, setExtForm] = useState({
name: '',
service_type: 'fastgpt',
api_url: '',
api_key: '',
});
// KB Modal
const [showModal, setShowModal] = useState(false);
const [editingKB, setEditingKB] = useState<KnowledgeBaseItem | null>(null);
const [kbForm, setKBForm] = useState({
name: '',
description: '',
is_official: false,
});
// Detail drawer
const [showDetail, setShowDetail] = useState(false);
const [selectedKB, setSelectedKB] = useState<KnowledgeBaseItem | null>(null);
const [detailDocs, setDetailDocs] = useState<any[]>([]);
const [detailDocTotal, setDetailDocTotal] = useState(0);
// Loading
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchStats();
fetchKBs();
fetchExtServices();
}, []);
useEffect(() => {
fetchKBs();
}, [page, keyword, filterStatus, filterOfficial]);
const fetchStats = async () => {
try {
const res = await adminAPI.getAdminKnowledgeStats();
if (res.data?.code === 0) setStats(res.data.data);
} catch (e) { console.error(e); }
};
const fetchKBs = async () => {
setLoading(true);
try {
const params: any = { page, page_size: pageSize };
if (keyword) params.keyword = keyword;
if (filterStatus) params.status = filterStatus;
if (filterOfficial) params.is_official = filterOfficial === 'true';
const res = await adminAPI.getAdminKnowledge(params);
if (res.data?.code === 0) {
setKnowledgeBases(res.data.data.items || []);
setTotal(res.data.data.total || 0);
}
} catch (e) { console.error(e); }
setLoading(false);
};
const fetchExtServices = async () => {
try {
const res = await adminAPI.getAdminKnowledgeExtServices();
if (res.data?.code === 0) setExtServices(res.data.data.items || []);
} catch (e) { console.error(e); }
};
const handleCreateKB = async () => {
try {
const res = await adminAPI.createAdminKnowledge(kbForm);
if (res.data?.code === 0) {
setShowModal(false);
setKBForm({ name: '', description: '', is_official: false });
fetchKBs();
fetchStats();
}
} catch (e) { console.error(e); }
};
const handleUpdateKB = async () => {
if (!editingKB) return;
try {
const res = await adminAPI.updateAdminKnowledge(editingKB.id, kbForm);
if (res.data?.code === 0) {
setShowModal(false);
fetchKBs();
fetchStats();
}
} catch (e) { console.error(e); }
};
const handleDeleteKB = async (id: string) => {
if (!confirm('确认删除该知识库及其所有文档?此操作不可恢复!')) return;
try {
const res = await adminAPI.deleteAdminKnowledge(id);
if (res.data?.code === 0) {
fetchKBs();
fetchStats();
}
} catch (e) { console.error(e); }
};
const handleToggleKB = async (id: string, isActive: boolean) => {
try {
const res = await adminAPI.toggleAdminKnowledge(id, isActive);
if (res.data?.code === 0) {
fetchKBs();
fetchStats();
}
} catch (e) { console.error(e); }
};
const handleViewDetail = async (kb: KnowledgeBaseItem) => {
setSelectedKB(kb);
setShowDetail(true);
try {
const res = await adminAPI.getAdminKnowledgeDetail(kb.id);
if (res.data?.code === 0) {
setSelectedKB(res.data.data);
}
const docRes = await adminAPI.getAdminKnowledgeDocs(kb.id, { page: 1, page_size: 20 });
if (docRes.data?.code === 0) {
setDetailDocs(docRes.data.data.items || []);
setDetailDocTotal(docRes.data.data.total || 0);
}
} catch (e) { console.error(e); }
};
const handleCreateExtService = async () => {
try {
const res = await adminAPI.createAdminKnowledgeExtService(extForm);
if (res.data?.code === 0) {
setShowExtModal(false);
setExtForm({ name: '', service_type: 'fastgpt', api_url: '', api_key: '' });
fetchExtServices();
fetchStats();
}
} catch (e) { console.error(e); }
};
const handleDeleteExtService = async (id: string) => {
if (!confirm('确认删除该外部知识库服务配置?')) return;
try {
const res = await adminAPI.deleteAdminKnowledgeExtService(id);
if (res.data?.code === 0) fetchExtServices();
} catch (e) { console.error(e); }
};
const handleCheckExtStatus = async (id: string) => {
try {
const res = await adminAPI.checkAdminKnowledgeExtStatus(id);
if (res.data?.code === 0) {
alert(`连接状态: ${res.data.data.connected ? '✅ 已连接' : '❌ 未连接'}\n详情: ${res.data.data.detail || '无'}`);
fetchExtServices();
}
} catch (e) { console.error(e); }
};
const handleDeleteDoc = async (kbId: string, docId: string) => {
if (!confirm('确认删除该文档?')) return;
try {
const res = await adminAPI.deleteAdminKnowledgeDoc(kbId, docId);
if (res.data?.code === 0) {
handleViewDetail(selectedKB!);
fetchStats();
}
} catch (e) { console.error(e); }
};
const openEditModal = (kb: KnowledgeBaseItem) => {
setEditingKB(kb);
setKBForm({
name: kb.name,
description: kb.description || '',
is_official: kb.is_official,
});
setShowModal(true);
};
const openCreateModal = () => {
setEditingKB(null);
setKBForm({ name: '', description: '', is_official: false });
setShowModal(true);
};
const totalPages = Math.ceil(total / pageSize);
// Status badge
const statusBadge = (status: string) => {
const map: Record<string, { color: string; text: string }> = {
active: { color: '#28a745', text: '活跃' },
inactive: { color: '#6c757d', text: '禁用' },
archived: { color: '#ffc107', text: '归档' },
published: { color: '#28a745', text: '已发布' },
pending: { color: '#ffc107', text: '待审核' },
};
const s = map[status] || { color: '#6c757d', text: status };
return <span style={{ color: s.color, fontWeight: 600 }}>{s.text}</span>;
};
const syncStatusBadge = (status: string) => {
const map: Record<string, { color: string; text: string }> = {
none: { color: '#adb5bd', text: '未同步' },
syncing: { color: '#17a2b8', text: '同步中' },
synced: { color: '#28a745', text: '已同步' },
error: { color: '#dc3545', text: '同步错误' },
};
const s = map[status] || { color: '#adb5bd', text: status };
return <span style={{ color: s.color, fontSize: '0.85em' }}>{s.text}</span>;
};
const extTypeLabel = (type: string) => {
const map: Record<string, string> = { fastgpt: 'FastGPT', dify: 'Dify', ragflow: 'RAGFlow' };
return map[type] || type;
};
return (
<div style={{ padding: '24px', maxWidth: '1400px' }}>
<h2 style={{ marginBottom: '24px', fontSize: '1.5em' }}>📚 知识库管理</h2>
{/* Stats Dashboard */}
{stats && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: '12px', marginBottom: '24px' }}>
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
<div style={{ fontSize: '2em', fontWeight: 700, color: '#1890ff' }}>{stats.total_knowledge_bases}</div>
<div style={{ fontSize: '0.85em', color: '#666' }}>知识库总数</div>
</div>
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
<div style={{ fontSize: '2em', fontWeight: 700, color: '#52c41a' }}>{stats.total_documents}</div>
<div style={{ fontSize: '0.85em', color: '#666' }}>文档总数</div>
</div>
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
<div style={{ fontSize: '1.3em', fontWeight: 700, color: '#faad14' }}>{stats.total_storage_display}</div>
<div style={{ fontSize: '0.85em', color: '#666' }}>存储占用</div>
</div>
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
<div style={{ fontSize: '2em', fontWeight: 700, color: '#722ed1' }}>{stats.official_kb}</div>
<div style={{ fontSize: '0.85em', color: '#666' }}>官方知识库</div>
</div>
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
<div style={{ fontSize: '2em', fontWeight: 700, color: '#13c2c2' }}>{stats.kb_with_graph}</div>
<div style={{ fontSize: '0.85em', color: '#666' }}>含图谱KB</div>
</div>
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
<div style={{ fontSize: '2em', fontWeight: 700, color: '#eb2f96' }}>{stats.external_kb}</div>
<div style={{ fontSize: '0.85em', color: '#666' }}>外部集成</div>
</div>
</div>
)}
{/* Toolbar */}
<div style={{ display: 'flex', gap: '12px', marginBottom: '16px', alignItems: 'center' }}>
<input
type="text"
placeholder="搜索知识库名称/描述..."
value={keyword}
onChange={e => setKeyword(e.target.value)}
style={{ padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: '4px', width: '200px' }}
/>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)}
style={{ padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}>
<option value="">全部状态</option>
<option value="active">活跃</option>
<option value="inactive">禁用</option>
<option value="archived">归档</option>
</select>
<select value={filterOfficial} onChange={e => setFilterOfficial(e.target.value)}
style={{ padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}>
<option value="">全部来源</option>
<option value="true">官方</option>
<option value="false">用户</option>
</select>
<button onClick={openCreateModal}
style={{ padding: '8px 16px', background: '#1890ff', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
+ 创建知识库
</button>
<button onClick={() => setShowExtModal(true)}
style={{ padding: '8px 16px', background: '#722ed1', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
🔗 外部服务配置
</button>
</div>
{/* Knowledge Base Table */}
<table style={{ width: '100%', borderCollapse: 'collapse', background: '#fff', borderRadius: '8px' }}>
<thead>
<tr style={{ background: '#f5f5f5', borderBottom: '2px solid #e0e0e0' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>名称</th>
<th style={{ padding: '12px', textAlign: 'left' }}>来源</th>
<th style={{ padding: '12px', textAlign: 'left' }}>文档数</th>
<th style={{ padding: '12px', textAlign: 'left' }}>存储</th>
<th style={{ padding: '12px', textAlign: 'left' }}>状态</th>
<th style={{ padding: '12px', textAlign: 'left' }}>同步</th>
<th style={{ padding: '12px', textAlign: 'left' }}>创建时间</th>
<th style={{ padding: '12px', textAlign: 'center' }}>操作</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={8} style={{ textAlign: 'center', padding: '24px' }}>加载中...</td></tr>
) : knowledgeBases.length === 0 ? (
<tr><td colSpan={8} style={{ textAlign: 'center', padding: '24px', color: '#999' }}>暂无知识库数据</td></tr>
) : knowledgeBases.map(kb => (
<tr key={kb.id} style={{ borderBottom: '1px solid #f0f0f0' }}>
<td style={{ padding: '12px' }}>
<div style={{ fontWeight: 600 }}>{kb.name}</div>
<div style={{ fontSize: '0.8em', color: '#999' }}>{kb.description?.substring(0, 50)}</div>
</td>
<td style={{ padding: '12px' }}>
{kb.is_official ? (
<span style={{ color: '#722ed1', fontWeight: 600 }}>⭐ 官方</span>
) : (
<span style={{ color: '#666' }}>👤 {kb.owner_name || '用户'}</span>
)}
</td>
<td style={{ padding: '12px' }}>{kb.document_count}</td>
<td style={{ padding: '12px' }}>{kb.total_storage_mb || 0} MB</td>
<td style={{ padding: '12px' }}>{statusBadge(kb.status)}</td>
<td style={{ padding: '12px' }}>{syncStatusBadge(kb.external_sync_status)}</td>
<td style={{ padding: '12px', fontSize: '0.85em' }}>{kb.created_at?.substring(0, 10)}</td>
<td style={{ padding: '12px', textAlign: 'center' }}>
<button onClick={() => handleViewDetail(kb)}
style={{ padding: '4px 8px', margin: '2px', background: '#1890ff', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
详情
</button>
<button onClick={() => openEditModal(kb)}
style={{ padding: '4px 8px', margin: '2px', background: '#faad14', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
编辑
</button>
<button onClick={() => handleToggleKB(kb.id, kb.status !== 'active')}
style={{ padding: '4px 8px', margin: '2px', background: kb.status === 'active' ? '#ff4d4f' : '#52c41a', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
{kb.status === 'active' ? '禁用' : '启用'}
</button>
<button onClick={() => handleDeleteKB(kb.id)}
style={{ padding: '4px 8px', margin: '2px', background: '#ff4d4f', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
删除
</button>
</td>
</tr>
))}
</tbody>
</table>
{/* Pagination */}
{totalPages > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', gap: '8px', marginTop: '16px' }}>
<button disabled={page <= 1} onClick={() => setPage(page - 1)}
style={{ padding: '6px 12px', border: '1px solid #d9d9d9', borderRadius: '4px', cursor: page <= 1 ? 'not-allowed' : 'pointer' }}>
上一页
</button>
<span style={{ padding: '6px 12px' }}>第 {page} / {totalPages} 页 (共 {total} 条)</span>
<button disabled={page >= totalPages} onClick={() => setPage(page + 1)}
style={{ padding: '6px 12px', border: '1px solid #d9d9d9', borderRadius: '4px', cursor: page >= totalPages ? 'not-allowed' : 'pointer' }}>
下一页
</button>
</div>
)}
{/* KB Create/Edit Modal */}
{showModal && (
<div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', background: 'rgba(0,0,0,0.5)', zIndex: 1000, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<div style={{ background: '#fff', borderRadius: '8px', padding: '24px', width: '480px', maxWidth: '90vw' }}>
<h3>{editingKB ? '编辑知识库' : '创建知识库'}</h3>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>名称</label>
<input value={kbForm.name} onChange={e => setKBForm({ ...kbForm, name: e.target.value })}
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>描述</label>
<textarea value={kbForm.description} onChange={e => setKBForm({ ...kbForm, description: e.target.value })}
rows={3} style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
</div>
<div style={{ marginBottom: '16px' }}>
<label>
<input type="checkbox" checked={kbForm.is_official} onChange={e => setKBForm({ ...kbForm, is_official: e.target.checked })} />
<span style={{ marginLeft: '8px' }}>官方平台知识库</span>
</label>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={editingKB ? handleUpdateKB : handleCreateKB}
style={{ padding: '8px 24px', background: '#1890ff', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
{editingKB ? '保存' : '创建'}
</button>
<button onClick={() => setShowModal(false)}
style={{ padding: '8px 24px', background: '#f5f5f5', border: '1px solid #d9d9d9', borderRadius: '4px', cursor: 'pointer' }}>
取消
</button>
</div>
</div>
</div>
)}
{/* Detail Drawer */}
{showDetail && selectedKB && (
<div style={{ position: 'fixed', top: 0, right: 0, width: '480px', height: '100%', background: '#fff', boxShadow: '-2px 0 8px rgba(0,0,0,0.1)', zIndex: 1000, padding: '24px', overflowY: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
<h3>{selectedKB.name}</h3>
<button onClick={() => setShowDetail(false)} style={{ background: 'none', border: 'none', fontSize: '1.5em', cursor: 'pointer' }}>✕</button>
</div>
<div style={{ marginBottom: '16px', padding: '12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div>来源: {selectedKB.is_official ? '⭐ 官方' : `👤 ${selectedKB.owner_name || '用户'}`}</div>
<div>描述: {selectedKB.description || '无'}</div>
<div>状态: {statusBadge(selectedKB.status)}</div>
<div>文档数: {selectedKB.document_count}</div>
<div>存储: {selectedKB.total_storage_mb || 0} MB</div>
</div>
{/* Graph Stats */}
{selectedKB.graph_stats && (
<div style={{ marginBottom: '16px', padding: '12px', background: '#e6f7ff', borderRadius: '8px' }}>
<h4 style={{ margin: '0 0 8px 0' }}>知识图谱</h4>
<div>节点数: {selectedKB.graph_stats.nodes}</div>
<div>边数: {selectedKB.graph_stats.edges}</div>
</div>
)}
{/* External Integration */}
{selectedKB.external_service_id && (
<div style={{ marginBottom: '16px', padding: '12px', background: '#f9f0ff', borderRadius: '8px' }}>
<h4 style={{ margin: '0 0 8px 0' }}>外部集成</h4>
<div>同步状态: {syncStatusBadge(selectedKB.external_sync_status)}</div>
<div>外部ID: {selectedKB.external_id || '未关联'}</div>
</div>
)}
{/* Documents */}
<h4 style={{ margin: '16px 0 8px 0' }}>文档列表 ({detailDocTotal})</h4>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#f5f5f5' }}>
<th style={{ padding: '8px', textAlign: 'left' }}>文件名</th>
<th style={{ padding: '8px', textAlign: 'left' }}>类型</th>
<th style={{ padding: '8px', textAlign: 'left' }}>大小</th>
<th style={{ padding: '8px', textAlign: 'left' }}>状态</th>
<th style={{ padding: '8px', textAlign: 'center' }}>操作</th>
</tr>
</thead>
<tbody>
{detailDocs.map(doc => (
<tr key={doc.id} style={{ borderBottom: '1px solid #f0f0f0' }}>
<td style={{ padding: '8px' }}>{doc.filename}</td>
<td style={{ padding: '8px' }}>{doc.file_type}</td>
<td style={{ padding: '8px' }}>{doc.file_size > 1024 * 1024 ? `${(doc.file_size / 1024 / 1024).toFixed(1)} MB` : `${(doc.file_size / 1024).toFixed(1)} KB`}</td>
<td style={{ padding: '8px' }}>{statusBadge(doc.status)}</td>
<td style={{ padding: '8px', textAlign: 'center' }}>
<button onClick={() => handleDeleteDoc(selectedKB.id, doc.id)}
style={{ padding: '4px 8px', background: '#ff4d4f', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
删除
</button>
</td>
</tr>
))}
{detailDocs.length === 0 && (
<tr><td colSpan={5} style={{ textAlign: 'center', padding: '12px', color: '#999' }}>暂无文档</td></tr>
)}
</tbody>
</table>
</div>
)}
{/* External Service Modal */}
{showExtModal && (
<div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', background: 'rgba(0,0,0,0.5)', zIndex: 1000, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<div style={{ background: '#fff', borderRadius: '8px', padding: '24px', width: '600px', maxWidth: '90vw' }}>
<h3>🔗 外部知识库服务配置</h3>
{/* Existing Services */}
{extServices.length > 0 && (
<div style={{ marginBottom: '16px' }}>
<h4>已配置的服务</h4>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#f5f5f5' }}>
<th style={{ padding: '8px' }}>名称</th>
<th style={{ padding: '8px' }}>类型</th>
<th style={{ padding: '8px' }}>URL</th>
<th style={{ padding: '8px' }}>状态</th>
<th style={{ padding: '8px' }}>操作</th>
</tr>
</thead>
<tbody>
{extServices.map(svc => (
<tr key={svc.id}>
<td style={{ padding: '8px' }}>{svc.name}</td>
<td style={{ padding: '8px' }}>{extTypeLabel(svc.service_type)}</td>
<td style={{ padding: '8px', fontSize: '0.8em' }}>{svc.api_url}</td>
<td style={{ padding: '8px' }}>{statusBadge(svc.status)}</td>
<td style={{ padding: '8px' }}>
<button onClick={() => handleCheckExtStatus(svc.id)}
style={{ padding: '4px 8px', background: '#1890ff', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
检测
</button>
<button onClick={() => handleDeleteExtService(svc.id)}
style={{ padding: '4px 8px', background: '#ff4d4f', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
删除
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* New Service Form */}
<div style={{ borderTop: '1px solid #e0e0e0', paddingTop: '16px' }}>
<h4>添加新服务</h4>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>服务名称</label>
<input value={extForm.name} onChange={e => setExtForm({ ...extForm, name: e.target.value })}
placeholder="如: 生产环境 FastGPT"
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>服务类型</label>
<select value={extForm.service_type} onChange={e => setExtForm({ ...extForm, service_type: e.target.value })}
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}>
<option value="fastgpt">FastGPT - 开源 AI 知识库平台</option>
<option value="dify">Dify - LLM 应用开发平台</option>
<option value="ragflow">RAGFlow - 基于深度文档理解的 RAG 引擎</option>
</select>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>API URL</label>
<input value={extForm.api_url} onChange={e => setExtForm({ ...extForm, api_url: e.target.value })}
placeholder="https://your-fastgpt-instance.com/api/v1"
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>API Key</label>
<input type="password" value={extForm.api_key} onChange={e => setExtForm({ ...extForm, api_key: e.target.value })}
placeholder="Bearer Token 或 API Key"
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={handleCreateExtService}
style={{ padding: '8px 24px', background: '#722ed1', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
添加服务
</button>
<button onClick={() => setShowExtModal(false)}
style={{ padding: '8px 24px', background: '#f5f5f5', border: '1px solid #d9d9d9', borderRadius: '4px', cursor: 'pointer' }}>
关闭
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default Knowledge;