feat(im): mirror WeChat bot chats into AI chat as WeXin Bot conversation; add conversations.source; migrate history; remove redundant history page
This commit is contained in:
1 parent
7c71d193fc
commit
7b7871a14d
5 files changed
+1044
-955
No files matched your search
@@ -691,6 +691,7 @@ def list_conversations(
|
|||||||
"status": conv.status,
|
"status": conv.status,
|
||||||
"pinned": conv.pinned,
|
"pinned": conv.pinned,
|
||||||
"knowledge_graph_count": conv.knowledge_graph_count or 0,
|
"knowledge_graph_count": conv.knowledge_graph_count or 0,
|
||||||
|
"source": getattr(conv, "source", None),
|
||||||
"created_at": str(conv.created_at) if conv.created_at else None,
|
"created_at": str(conv.created_at) if conv.created_at else None,
|
||||||
"updated_at": str(conv.updated_at) if conv.updated_at else None,
|
"updated_at": str(conv.updated_at) if conv.updated_at else None,
|
||||||
})
|
})
|
||||||
@@ -1140,69 +1141,8 @@ def clear_conversation(
|
|||||||
return {"code": 0, "message": "对话上下文已清空"}
|
return {"code": 0, "message": "对话上下文已清空"}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/conversations/{conv_id}/to-knowledge-graph")
|
def _build_knowledge_graph(db: Session, kb: KnowledgeBase) -> dict:
|
||||||
def convert_to_knowledge_graph(
|
"""从知识库文档构建知识图谱节点和边,返回 graph_data。"""
|
||||||
conv_id: str,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
):
|
|
||||||
"""将对话内容转换为知识图谱"""
|
|
||||||
conv = db.query(Conversation).filter(
|
|
||||||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
|
||||||
).first()
|
|
||||||
if not conv:
|
|
||||||
raise HTTPException(status_code=404, detail="对话不存在")
|
|
||||||
|
|
||||||
messages = (
|
|
||||||
db.query(Message)
|
|
||||||
.filter(Message.conversation_id == conv_id)
|
|
||||||
.order_by(asc(Message.created_at))
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not messages:
|
|
||||||
raise HTTPException(status_code=400, detail="对话中没有消息, 无法生成知识图谱")
|
|
||||||
|
|
||||||
kb_name = f"对话图谱-{conv.title}"
|
|
||||||
kb = KnowledgeBase(
|
|
||||||
id=gen_uuid(),
|
|
||||||
user_id=current_user.id,
|
|
||||||
name=kb_name,
|
|
||||||
description=f"从对话「{conv.title}」自动生成的知识图谱",
|
|
||||||
directory_path="/",
|
|
||||||
document_count=0,
|
|
||||||
created_at=cst_now(),
|
|
||||||
updated_at=cst_now(),
|
|
||||||
)
|
|
||||||
db.add(kb)
|
|
||||||
db.flush()
|
|
||||||
|
|
||||||
doc_count = 0
|
|
||||||
for idx, msg in enumerate(messages):
|
|
||||||
role_label = "用户" if msg.role == "user" else "AI"
|
|
||||||
preview = re.sub(r'[\\n\\r\\t]+', ' ', msg.content[:40]).strip()
|
|
||||||
filename = f"msg_{idx+1:03d}_{role_label}_{preview}.txt"
|
|
||||||
|
|
||||||
doc = KnowledgeDocument(
|
|
||||||
id=gen_uuid(),
|
|
||||||
knowledge_base_id=kb.id,
|
|
||||||
name=filename,
|
|
||||||
filename=filename,
|
|
||||||
file_type="txt",
|
|
||||||
file_size=len(msg.content.encode('utf-8')),
|
|
||||||
content=msg.content,
|
|
||||||
status="completed",
|
|
||||||
chunk_count=max(1, len(msg.content) // 500 + 1),
|
|
||||||
created_at=cst_now(),
|
|
||||||
updated_at=cst_now(),
|
|
||||||
)
|
|
||||||
db.add(doc)
|
|
||||||
doc_count += 1
|
|
||||||
|
|
||||||
kb.document_count = doc_count
|
|
||||||
db.flush()
|
|
||||||
|
|
||||||
# 构建知识图谱节点和边
|
|
||||||
documents = db.query(KnowledgeDocument).filter(
|
documents = db.query(KnowledgeDocument).filter(
|
||||||
and_(
|
and_(
|
||||||
KnowledgeDocument.knowledge_base_id == kb.id,
|
KnowledgeDocument.knowledge_base_id == kb.id,
|
||||||
@@ -1268,7 +1208,96 @@ def convert_to_knowledge_graph(
|
|||||||
|
|
||||||
kb.knowledge_graph_count = len(graph_nodes)
|
kb.knowledge_graph_count = len(graph_nodes)
|
||||||
kb.updated_at = cst_now()
|
kb.updated_at = cst_now()
|
||||||
conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1
|
return graph_data
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/conversations/{conv_id}/to-knowledge-graph")
|
||||||
|
def convert_to_knowledge_graph(
|
||||||
|
conv_id: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""将对话内容转换为知识图谱(幂等:同一对话仅生成一次知识库)"""
|
||||||
|
conv = db.query(Conversation).filter(
|
||||||
|
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||||||
|
).first()
|
||||||
|
if not conv:
|
||||||
|
raise HTTPException(status_code=404, detail="对话不存在")
|
||||||
|
|
||||||
|
messages = (
|
||||||
|
db.query(Message)
|
||||||
|
.filter(Message.conversation_id == conv_id)
|
||||||
|
.order_by(asc(Message.created_at))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not messages:
|
||||||
|
raise HTTPException(status_code=400, detail="对话中没有消息, 无法生成知识图谱")
|
||||||
|
|
||||||
|
# 幂等:若该对话已生成过知识图谱且知识库仍存在,则直接刷新返回
|
||||||
|
if conv.knowledge_graph_base_id:
|
||||||
|
existing_kb = db.query(KnowledgeBase).filter(
|
||||||
|
KnowledgeBase.id == conv.knowledge_graph_base_id
|
||||||
|
).first()
|
||||||
|
if existing_kb:
|
||||||
|
graph_data = _build_knowledge_graph(db, existing_kb)
|
||||||
|
conv.knowledge_graph_count = 1
|
||||||
|
conv.updated_at = cst_now()
|
||||||
|
db.commit()
|
||||||
|
return {
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"knowledge_base_id": existing_kb.id,
|
||||||
|
"knowledge_base_name": existing_kb.name,
|
||||||
|
"graph": graph_data,
|
||||||
|
"knowledge_graph_count": 1,
|
||||||
|
"already_exists": True,
|
||||||
|
},
|
||||||
|
"message": "知识图谱已存在, 已刷新展示",
|
||||||
|
}
|
||||||
|
|
||||||
|
kb_name = f"对话图谱-{conv.title}"
|
||||||
|
kb = KnowledgeBase(
|
||||||
|
id=gen_uuid(),
|
||||||
|
user_id=current_user.id,
|
||||||
|
name=kb_name,
|
||||||
|
description=f"从对话「{conv.title}」自动生成的知识图谱",
|
||||||
|
directory_path="/",
|
||||||
|
document_count=0,
|
||||||
|
created_at=cst_now(),
|
||||||
|
updated_at=cst_now(),
|
||||||
|
)
|
||||||
|
db.add(kb)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
doc_count = 0
|
||||||
|
for idx, msg in enumerate(messages):
|
||||||
|
role_label = "用户" if msg.role == "user" else "AI"
|
||||||
|
preview = re.sub(r'[\\n\\r\\t]+', ' ', msg.content[:40]).strip()
|
||||||
|
filename = f"msg_{idx+1:03d}_{role_label}_{preview}.txt"
|
||||||
|
|
||||||
|
doc = KnowledgeDocument(
|
||||||
|
id=gen_uuid(),
|
||||||
|
knowledge_base_id=kb.id,
|
||||||
|
filename=filename,
|
||||||
|
file_type="txt",
|
||||||
|
file_size=len(msg.content.encode('utf-8')),
|
||||||
|
content=msg.content,
|
||||||
|
status="completed",
|
||||||
|
chunk_count=max(1, len(msg.content) // 500 + 1),
|
||||||
|
created_at=cst_now(),
|
||||||
|
updated_at=cst_now(),
|
||||||
|
)
|
||||||
|
db.add(doc)
|
||||||
|
doc_count += 1
|
||||||
|
|
||||||
|
kb.document_count = doc_count
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
# 构建知识图谱节点和边
|
||||||
|
graph_data = _build_knowledge_graph(db, kb)
|
||||||
|
conv.knowledge_graph_base_id = kb.id
|
||||||
|
conv.knowledge_graph_count = 1
|
||||||
conv.updated_at = cst_now()
|
conv.updated_at = cst_now()
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -1285,6 +1314,8 @@ def convert_to_knowledge_graph(
|
|||||||
"knowledge_base_id": kb.id,
|
"knowledge_base_id": kb.id,
|
||||||
"knowledge_base_name": kb.name,
|
"knowledge_base_name": kb.name,
|
||||||
"graph": graph_data,
|
"graph": graph_data,
|
||||||
|
"knowledge_graph_count": 1,
|
||||||
|
"already_exists": False,
|
||||||
},
|
},
|
||||||
"message": f"知识图谱构建完成",
|
"message": f"知识图谱构建完成",
|
||||||
}
|
}
|
||||||
@@ -1299,6 +1330,7 @@ def get_models(db: Session = Depends(get_db)):
|
|||||||
result.append({
|
result.append({
|
||||||
"id": m.id,
|
"id": m.id,
|
||||||
"name": m.name,
|
"name": m.name,
|
||||||
|
"display_name": getattr(m, "display_name", None) or m.name,
|
||||||
"provider_id": m.provider_id,
|
"provider_id": m.provider_id,
|
||||||
"provider_name": provider.name if provider else "",
|
"provider_name": provider.name if provider else "",
|
||||||
"status": m.status,
|
"status": m.status,
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -85,6 +85,7 @@ class Conversation(Base):
|
|||||||
pinned = Column(Boolean, default=False)
|
pinned = Column(Boolean, default=False)
|
||||||
knowledge_graph_count = Column(Integer, default=0)
|
knowledge_graph_count = Column(Integer, default=0)
|
||||||
knowledge_graph_base_id = Column(String(64), default=None, comment="已生成的知识图谱知识库ID")
|
knowledge_graph_base_id = Column(String(64), default=None, comment="已生成的知识图谱知识库ID")
|
||||||
|
source = Column(String(32), default=None, comment="对话来源,如 wechat_bot")
|
||||||
created_at = Column(DateTime, default=datetime.now)
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
|||||||
+166
-168
@@ -1,168 +1,166 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import type { User } from '@/types';
|
import type { User } from '@/types';
|
||||||
import { AuthContext, AuthContextType } from '@/contexts/AuthContext';
|
import { AuthContext, AuthContextType } from '@/contexts/AuthContext';
|
||||||
import { authAPI } from '@/api';
|
import { authAPI } from '@/api';
|
||||||
|
|
||||||
// Layouts
|
// Layouts
|
||||||
import FrontLayout from '@/components/layout/FrontLayout';
|
import FrontLayout from '@/components/layout/FrontLayout';
|
||||||
import AdminLayout from '@/components/layout/AdminLayout';
|
import AdminLayout from '@/components/layout/AdminLayout';
|
||||||
|
|
||||||
// Front Pages
|
// Front Pages
|
||||||
import LandingPage from '@/pages/front/LandingPage';
|
import LandingPage from '@/pages/front/LandingPage';
|
||||||
import LoginPage from '@/pages/front/LoginPage';
|
import LoginPage from '@/pages/front/LoginPage';
|
||||||
import ChatPage from '@/pages/front/ChatPage';
|
import ChatPage from '@/pages/front/ChatPage';
|
||||||
import ChannelPage from '@/pages/front/ChannelPage';
|
import ChannelPage from '@/pages/front/ChannelPage';
|
||||||
import KnowledgePage from '@/pages/front/KnowledgePage';
|
import KnowledgePage from '@/pages/front/KnowledgePage';
|
||||||
import MarketPage from '@/pages/front/MarketPage';
|
import MarketPage from '@/pages/front/MarketPage';
|
||||||
import TokenDashboardPage from '@/pages/front/TokenDashboardPage';
|
import TokenDashboardPage from '@/pages/front/TokenDashboardPage';
|
||||||
import PricingPage from '@/pages/front/PricingPage';
|
import PricingPage from '@/pages/front/PricingPage';
|
||||||
import ProfilePage from '@/pages/front/ProfilePage';
|
import ProfilePage from '@/pages/front/ProfilePage';
|
||||||
import NotificationPage from '@/pages/front/NotificationPage';
|
import NotificationPage from '@/pages/front/NotificationPage';
|
||||||
import AssetsPage from '@/pages/front/AssetsPage';
|
import AssetsPage from '@/pages/front/AssetsPage';
|
||||||
import NovelPreviewPage from '@/pages/front/NovelPreviewPage';
|
import NovelPreviewPage from '@/pages/front/NovelPreviewPage';
|
||||||
import EvolutionDashboard from '@/pages/front/EvolutionDashboard';
|
import EvolutionDashboard from '@/pages/front/EvolutionDashboard';
|
||||||
import WechatBot from '@/pages/front/WechatBot';
|
import WechatBot from '@/pages/front/WechatBot';
|
||||||
import WechatHistory from '@/pages/front/WechatHistory';
|
|
||||||
|
// Admin Pages
|
||||||
// Admin Pages
|
import AdminDashboard from '@/pages/admin/Dashboard';
|
||||||
import AdminDashboard from '@/pages/admin/Dashboard';
|
import AdminUsers from '@/pages/admin/Users';
|
||||||
import AdminUsers from '@/pages/admin/Users';
|
import AdminModels from '@/pages/admin/Models';
|
||||||
import AdminModels from '@/pages/admin/Models';
|
import AdminSiteSettings from '@/pages/admin/SiteSettings';
|
||||||
import AdminSiteSettings from '@/pages/admin/SiteSettings';
|
import AdminPayment from '@/pages/admin/Payment';
|
||||||
import AdminPayment from '@/pages/admin/Payment';
|
import AdminServices from '@/pages/admin/Services';
|
||||||
import AdminServices from '@/pages/admin/Services';
|
import AdminMembership from '@/pages/admin/Membership';
|
||||||
import AdminMembership from '@/pages/admin/Membership';
|
import AdminAds from '@/pages/admin/Ads';
|
||||||
import AdminAds from '@/pages/admin/Ads';
|
import AdminMarket from '@/pages/admin/Market';
|
||||||
import AdminMarket from '@/pages/admin/Market';
|
import AdminLogs from '@/pages/admin/Logs';
|
||||||
import AdminLogs from '@/pages/admin/Logs';
|
import AdminSkills from '@/pages/admin/Skills'
|
||||||
import AdminSkills from '@/pages/admin/Skills'
|
import AdminAgents from '@/pages/admin/Agents';
|
||||||
import AdminAgents from '@/pages/admin/Agents';
|
import AdminCodeExec from '@/pages/admin/CodeExec';;
|
||||||
import AdminCodeExec from '@/pages/admin/CodeExec';;
|
import DistributionBuilder from '@/pages/admin/DistributionBuilder';
|
||||||
import DistributionBuilder from '@/pages/admin/DistributionBuilder';
|
import PrivacySettings from '@/pages/admin/PrivacySettings';
|
||||||
import PrivacySettings from '@/pages/admin/PrivacySettings';
|
import ImBots from '@/pages/admin/ImBots';
|
||||||
import ImBots from '@/pages/admin/ImBots';
|
import Promotion from '@/pages/admin/Promotion';
|
||||||
import Promotion from '@/pages/admin/Promotion';
|
import Achievement from '@/pages/admin/Achievement';
|
||||||
import Achievement from '@/pages/admin/Achievement';
|
import Tenant from '@/pages/admin/Tenant';
|
||||||
import Tenant from '@/pages/admin/Tenant';
|
import Billing from '@/pages/admin/Billing';
|
||||||
import Billing from '@/pages/admin/Billing';
|
import Analytics from '@/pages/admin/Analytics';
|
||||||
import Analytics from '@/pages/admin/Analytics';
|
import UserPortal from '@/pages/UserPortal';
|
||||||
import UserPortal from '@/pages/UserPortal';
|
import AdminKnowledge from '@/pages/admin/Knowledge';
|
||||||
import AdminKnowledge from '@/pages/admin/Knowledge';
|
import AdminWorkflow from '@/pages/admin/Workflow';
|
||||||
import AdminWorkflow from '@/pages/admin/Workflow';
|
import AdminStorage from '@/pages/admin/Storage';
|
||||||
import AdminStorage from '@/pages/admin/Storage';
|
import AdminDeveloper from '@/pages/admin/Developer';
|
||||||
import AdminDeveloper from '@/pages/admin/Developer';
|
import AdminDeveloperTools from '@/pages/admin/DeveloperTools';
|
||||||
import AdminDeveloperTools from '@/pages/admin/DeveloperTools';
|
import AdminAutomation from '@/pages/admin/Automation';
|
||||||
import AdminAutomation from '@/pages/admin/Automation';
|
import AdminMcp from '@/pages/admin/Mcp';
|
||||||
import AdminMcp from '@/pages/admin/Mcp';
|
import AdminApps from '@/pages/admin/Apps';
|
||||||
import AdminApps from '@/pages/admin/Apps';
|
import AssetCenter from '@/pages/admin/AssetCenter';
|
||||||
import AssetCenter from '@/pages/admin/AssetCenter';
|
|
||||||
|
const App: React.FC = () => {
|
||||||
const App: React.FC = () => {
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [loading, setLoading] = useState(true);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
// 初始化:从localStorage恢复登录状态
|
||||||
// 初始化:从localStorage恢复登录状态
|
useEffect(() => {
|
||||||
useEffect(() => {
|
const token = localStorage.getItem('aiflow_token');
|
||||||
const token = localStorage.getItem('aiflow_token');
|
const cachedUser = localStorage.getItem('aiflow_user');
|
||||||
const cachedUser = localStorage.getItem('aiflow_user');
|
if (token && cachedUser) {
|
||||||
if (token && cachedUser) {
|
try {
|
||||||
try {
|
setUser(JSON.parse(cachedUser));
|
||||||
setUser(JSON.parse(cachedUser));
|
} catch {
|
||||||
} catch {
|
localStorage.removeItem('aiflow_token');
|
||||||
localStorage.removeItem('aiflow_token');
|
localStorage.removeItem('aiflow_user');
|
||||||
localStorage.removeItem('aiflow_user');
|
}
|
||||||
}
|
}
|
||||||
}
|
setLoading(false);
|
||||||
setLoading(false);
|
}, []);
|
||||||
}, []);
|
|
||||||
|
const login = (u: User) => {
|
||||||
const login = (u: User) => {
|
setUser(u);
|
||||||
setUser(u);
|
localStorage.setItem('aiflow_user', JSON.stringify(u));
|
||||||
localStorage.setItem('aiflow_user', JSON.stringify(u));
|
};
|
||||||
};
|
const logout = () => {
|
||||||
const logout = () => {
|
setUser(null);
|
||||||
setUser(null);
|
localStorage.removeItem('aiflow_token');
|
||||||
localStorage.removeItem('aiflow_token');
|
localStorage.removeItem('aiflow_user');
|
||||||
localStorage.removeItem('aiflow_user');
|
};
|
||||||
};
|
const isAdmin = user?.role === 'admin';
|
||||||
const isAdmin = user?.role === 'admin';
|
|
||||||
|
if (loading) {
|
||||||
if (loading) {
|
return <div className="loading-screen">加载中...</div>;
|
||||||
return <div className="loading-screen">加载中...</div>;
|
}
|
||||||
}
|
|
||||||
|
return (
|
||||||
return (
|
<AuthContext.Provider value={{ user, login, logout, isAdmin }}>
|
||||||
<AuthContext.Provider value={{ user, login, logout, isAdmin }}>
|
<Routes>
|
||||||
<Routes>
|
{/* Login */}
|
||||||
{/* Login */}
|
<Route path="/login" element={
|
||||||
<Route path="/login" element={
|
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage />
|
||||||
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LoginPage />
|
} />
|
||||||
} />
|
|
||||||
|
{/* Root - Landing for guests, redirect for authenticated users */}
|
||||||
{/* Root - Landing for guests, redirect for authenticated users */}
|
<Route path="/" element={
|
||||||
<Route path="/" element={
|
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage />
|
||||||
user ? (isAdmin ? <Navigate to="/admin" /> : <Navigate to="/chat" />) : <LandingPage />
|
} />
|
||||||
} />
|
|
||||||
|
{/* Front Routes (authenticated only) */}
|
||||||
{/* Front Routes (authenticated only) */}
|
<Route path="/" element={<FrontLayout />}>
|
||||||
<Route path="/" element={<FrontLayout />}>
|
<Route path="chat" element={<ChatPage />} />
|
||||||
<Route path="chat" element={<ChatPage />} />
|
<Route path="chat/:id" element={<ChatPage />} />
|
||||||
<Route path="chat/:id" element={<ChatPage />} />
|
<Route path="channels" element={<ChannelPage />} />
|
||||||
<Route path="channels" element={<ChannelPage />} />
|
<Route path="knowledge" element={<KnowledgePage />} />
|
||||||
<Route path="knowledge" element={<KnowledgePage />} />
|
<Route path="market" element={<MarketPage />} />
|
||||||
<Route path="market" element={<MarketPage />} />
|
<Route path="token" element={<TokenDashboardPage />} />
|
||||||
<Route path="token" element={<TokenDashboardPage />} />
|
<Route path="pricing" element={<PricingPage />} />
|
||||||
<Route path="pricing" element={<PricingPage />} />
|
<Route path="profile" element={<ProfilePage />} />
|
||||||
<Route path="profile" element={<ProfilePage />} />
|
<Route path="notifications" element={<NotificationPage />} />
|
||||||
<Route path="notifications" element={<NotificationPage />} />
|
<Route path="assets" element={<AssetsPage />} />
|
||||||
<Route path="assets" element={<AssetsPage />} />
|
<Route path="assets/novel/:id" element={<NovelPreviewPage />} />
|
||||||
<Route path="assets/novel/:id" element={<NovelPreviewPage />} />
|
<Route path="evolution" element={<EvolutionDashboard />} />
|
||||||
<Route path="evolution" element={<EvolutionDashboard />} />
|
<Route path="wechat-bot" element={<WechatBot />} />
|
||||||
<Route path="wechat-bot" element={<WechatBot />} />
|
</Route>
|
||||||
<Route path="wechat-history" element={<WechatHistory />} />
|
|
||||||
</Route>
|
{/* Admin Routes */}
|
||||||
|
<Route path="/admin" element={<AdminLayout />}>
|
||||||
{/* Admin Routes */}
|
<Route index element={<AdminDashboard />} />
|
||||||
<Route path="/admin" element={<AdminLayout />}>
|
<Route path="users" element={<AdminUsers />} />
|
||||||
<Route index element={<AdminDashboard />} />
|
<Route path="models" element={<AdminModels />} />
|
||||||
<Route path="users" element={<AdminUsers />} />
|
<Route path="settings" element={<AdminSiteSettings />} />
|
||||||
<Route path="models" element={<AdminModels />} />
|
<Route path="payment" element={<AdminPayment />} />
|
||||||
<Route path="settings" element={<AdminSiteSettings />} />
|
<Route path="services" element={<AdminServices />} />
|
||||||
<Route path="payment" element={<AdminPayment />} />
|
<Route path="membership" element={<AdminMembership />} />
|
||||||
<Route path="services" element={<AdminServices />} />
|
<Route path="ads" element={<AdminAds />} />
|
||||||
<Route path="membership" element={<AdminMembership />} />
|
<Route path="market" element={<AdminMarket />} />
|
||||||
<Route path="ads" element={<AdminAds />} />
|
<Route path="logs" element={<AdminLogs />} />
|
||||||
<Route path="market" element={<AdminMarket />} />
|
|
||||||
<Route path="logs" element={<AdminLogs />} />
|
<Route path="skills" element={<AdminSkills />} />
|
||||||
|
<Route path="agents" element={<AdminAgents />} />
|
||||||
<Route path="skills" element={<AdminSkills />} />
|
<Route path="code-exec" element={<AdminCodeExec />} />
|
||||||
<Route path="agents" element={<AdminAgents />} />
|
<Route path="distributions" element={<DistributionBuilder />} />
|
||||||
<Route path="code-exec" element={<AdminCodeExec />} />
|
<Route path="im-bots" element={<ImBots />} />
|
||||||
<Route path="distributions" element={<DistributionBuilder />} />
|
<Route path="privacy" element={<PrivacySettings />} />
|
||||||
<Route path="im-bots" element={<ImBots />} />
|
<Route path="promotion" element={<Promotion />} />
|
||||||
<Route path="privacy" element={<PrivacySettings />} />
|
<Route path="achievement" element={<Achievement />} />
|
||||||
<Route path="promotion" element={<Promotion />} />
|
<Route path="tenant" element={<Tenant />} />
|
||||||
<Route path="achievement" element={<Achievement />} />
|
<Route path="billing" element={<Billing />} />
|
||||||
<Route path="tenant" element={<Tenant />} />
|
<Route path="analytics" element={<Analytics />} />
|
||||||
<Route path="billing" element={<Billing />} />
|
<Route path="knowledge" element={<AdminKnowledge />} />
|
||||||
<Route path="analytics" element={<Analytics />} />
|
<Route path="workflow" element={<AdminWorkflow />} />
|
||||||
<Route path="knowledge" element={<AdminKnowledge />} />
|
<Route path="storage" element={<AdminStorage />} />
|
||||||
<Route path="workflow" element={<AdminWorkflow />} />
|
<Route path="developer" element={<AdminDeveloper />} />
|
||||||
<Route path="storage" element={<AdminStorage />} />
|
<Route path="developer-tools" element={<AdminDeveloperTools />} />
|
||||||
<Route path="developer" element={<AdminDeveloper />} />
|
<Route path="automation" element={<AdminAutomation />} />
|
||||||
<Route path="developer-tools" element={<AdminDeveloperTools />} />
|
<Route path="mcp" element={<AdminMcp />} />
|
||||||
<Route path="automation" element={<AdminAutomation />} />
|
<Route path="apps" element={<AdminApps />} />
|
||||||
<Route path="mcp" element={<AdminMcp />} />
|
<Route path="assets" element={<AssetCenter />} />
|
||||||
<Route path="apps" element={<AdminApps />} />
|
<Route path="portal" element={<UserPortal />} />
|
||||||
<Route path="assets" element={<AssetCenter />} />
|
</Route>
|
||||||
<Route path="portal" element={<UserPortal />} />
|
|
||||||
</Route>
|
<Route path="*" element={<Navigate to="/" />} />
|
||||||
|
</Routes>
|
||||||
<Route path="*" element={<Navigate to="/" />} />
|
</AuthContext.Provider>
|
||||||
</Routes>
|
);
|
||||||
</AuthContext.Provider>
|
};
|
||||||
);
|
|
||||||
};
|
export default App;
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,184 +1,183 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import './FrontLayout.css';
|
import './FrontLayout.css';
|
||||||
|
|
||||||
const navGroups = [
|
const navGroups = [
|
||||||
{
|
{
|
||||||
title: '智能体',
|
title: '智能体',
|
||||||
items: [
|
items: [
|
||||||
{ path: '/chat', icon: 'chat', label: 'AI聊天' },
|
{ path: '/chat', icon: 'chat', label: 'AI聊天' },
|
||||||
{ path: '/knowledge', icon: 'knowledge', label: '知识库' },
|
{ path: '/knowledge', icon: 'knowledge', label: '知识库' },
|
||||||
{ path: '/market', icon: 'market', label: '技能市场' },
|
{ path: '/market', icon: 'market', label: '技能市场' },
|
||||||
{ path: '/evolution', icon: 'evolution', label: 'AI自进化' },
|
{ path: '/evolution', icon: 'evolution', label: 'AI自进化' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'IM Bot',
|
title: 'IM Bot',
|
||||||
items: [
|
items: [
|
||||||
{ path: '/channels', icon: 'channel', label: '频道管理' },
|
{ path: '/channels', icon: 'channel', label: '频道管理' },
|
||||||
{ path: '/wechat-bot', icon: 'wechat', label: '微信机器人' },
|
{ path: '/wechat-bot', icon: 'wechat', label: '微信机器人' },
|
||||||
{ path: '/wechat-history', icon: 'chat', label: '对话记录' },
|
],
|
||||||
],
|
},
|
||||||
},
|
{
|
||||||
{
|
title: '资源中心',
|
||||||
title: '资源中心',
|
items: [
|
||||||
items: [
|
{ path: '/assets', icon: 'assets', label: '资产文件' },
|
||||||
{ path: '/assets', icon: 'assets', label: '资产文件' },
|
{ path: '/token', icon: 'token', label: 'Token驾驶舱' },
|
||||||
{ path: '/token', icon: 'token', label: 'Token驾驶舱' },
|
{ path: '/pricing', icon: 'pricing', label: '付费中心' },
|
||||||
{ path: '/pricing', icon: 'pricing', label: '付费中心' },
|
],
|
||||||
],
|
},
|
||||||
},
|
];
|
||||||
];
|
|
||||||
|
const bottomItems = [
|
||||||
const bottomItems = [
|
{ path: '/profile', icon: 'profile', label: '个人中心' },
|
||||||
{ path: '/profile', icon: 'profile', label: '个人中心' },
|
{ path: '/notifications', icon: 'notify', label: '消息通知' },
|
||||||
{ path: '/notifications', icon: 'notify', label: '消息通知' },
|
];
|
||||||
];
|
|
||||||
|
const iconPaths: Record<string, JSX.Element> = {
|
||||||
const iconPaths: Record<string, JSX.Element> = {
|
chat: <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>,
|
||||||
chat: <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>,
|
channel: <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>,
|
||||||
channel: <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>,
|
knowledge: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>,
|
||||||
knowledge: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></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>,
|
||||||
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>,
|
apps: <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="2" width="9" height="9" rx="2"/><rect x="13" y="2" width="9" height="9" rx="2"/><rect x="2" y="13" width="9" height="9" rx="2"/><rect x="13" y="13" width="5" height="5" rx="1.5"/><path d="M18 18v3"/><path d="M19.5 16.5v6"/></svg>,
|
||||||
apps: <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="2" width="9" height="9" rx="2"/><rect x="13" y="2" width="9" height="9" rx="2"/><rect x="2" y="13" width="9" height="9" rx="2"/><rect x="13" y="13" width="5" height="5" rx="1.5"/><path d="M18 18v3"/><path d="M19.5 16.5v6"/></svg>,
|
token: <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="10"/><path d="M12 6v6l4 2"/></svg>,
|
||||||
token: <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="10"/><path d="M12 6v6l4 2"/></svg>,
|
pricing: <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="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>,
|
||||||
pricing: <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="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>,
|
profile: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>,
|
||||||
profile: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>,
|
notify: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>,
|
||||||
notify: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>,
|
assets: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>,
|
||||||
assets: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>,
|
evolution: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>,
|
||||||
evolution: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>,
|
wechat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/><circle cx="9" cy="11" r="1"/><circle cx="15" cy="11" r="1"/></svg>,
|
||||||
wechat: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/><circle cx="9" cy="11" r="1"/><circle cx="15" cy="11" r="1"/></svg>,
|
};
|
||||||
};
|
|
||||||
|
const FrontLayout: React.FC = () => {
|
||||||
const FrontLayout: React.FC = () => {
|
const { user, logout } = useAuth();
|
||||||
const { user, logout } = useAuth();
|
const navigate = useNavigate();
|
||||||
const navigate = useNavigate();
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
|
if (!user) {
|
||||||
if (!user) {
|
navigate('/login');
|
||||||
navigate('/login');
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
return (
|
||||||
return (
|
<div className="front-layout">
|
||||||
<div className="front-layout">
|
{/* Sidebar */}
|
||||||
{/* Sidebar */}
|
<aside className={`front-sidebar ${collapsed ? 'collapsed' : ''}`}>
|
||||||
<aside className={`front-sidebar ${collapsed ? 'collapsed' : ''}`}>
|
{/* Header */}
|
||||||
{/* Header */}
|
<div className="sidebar-header">
|
||||||
<div className="sidebar-header">
|
<div className="sidebar-logo">
|
||||||
<div className="sidebar-logo">
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
|
<defs>
|
||||||
<defs>
|
<linearGradient id="flGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
<linearGradient id="flGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
<stop offset="0%" stopColor="#6C63FF"/>
|
||||||
<stop offset="0%" stopColor="#6C63FF"/>
|
<stop offset="100%" stopColor="#A78BFA"/>
|
||||||
<stop offset="100%" stopColor="#A78BFA"/>
|
</linearGradient>
|
||||||
</linearGradient>
|
</defs>
|
||||||
</defs>
|
<rect width="32" height="32" rx="8" fill="url(#flGrad)"/>
|
||||||
<rect width="32" height="32" rx="8" fill="url(#flGrad)"/>
|
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
||||||
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
|
||||||
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
|
</svg>
|
||||||
</svg>
|
{!collapsed && <span className="sidebar-brand">七虎AI</span>}
|
||||||
{!collapsed && <span className="sidebar-brand">七虎AI</span>}
|
</div>
|
||||||
</div>
|
<button className="collapse-btn" onClick={() => setCollapsed(!collapsed)}>
|
||||||
<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">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
{collapsed ? <><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></> : <><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></>}
|
||||||
{collapsed ? <><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></> : <><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></>}
|
</svg>
|
||||||
</svg>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Navigation */}
|
||||||
{/* Navigation */}
|
<nav className="sidebar-nav">
|
||||||
<nav className="sidebar-nav">
|
<NavLink to="/" className={({ isActive }) => `nav-item nav-home-link`} end>
|
||||||
<NavLink to="/" className={({ isActive }) => `nav-item nav-home-link`} end>
|
<span className="nav-item-icon">
|
||||||
<span className="nav-item-icon">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
</span>
|
||||||
</span>
|
{!collapsed && <span className="nav-item-label">返回首页</span>}
|
||||||
{!collapsed && <span className="nav-item-label">返回首页</span>}
|
</NavLink>
|
||||||
</NavLink>
|
{navGroups.map(group => (
|
||||||
{navGroups.map(group => (
|
<div className="nav-section" key={group.title}>
|
||||||
<div className="nav-section" key={group.title}>
|
{!collapsed && <div className="nav-group-title">{group.title}</div>}
|
||||||
{!collapsed && <div className="nav-group-title">{group.title}</div>}
|
{group.items.map(item => (
|
||||||
{group.items.map(item => (
|
<NavLink
|
||||||
<NavLink
|
key={item.path}
|
||||||
key={item.path}
|
to={item.path}
|
||||||
to={item.path}
|
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
||||||
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
title={collapsed ? item.label : undefined}
|
||||||
title={collapsed ? item.label : undefined}
|
>
|
||||||
>
|
<span className="nav-icon">{iconPaths[item.icon]}</span>
|
||||||
<span className="nav-icon">{iconPaths[item.icon]}</span>
|
{!collapsed && <span className="nav-label">{item.label}</span>}
|
||||||
{!collapsed && <span className="nav-label">{item.label}</span>}
|
{!collapsed && <span className="nav-active-dot" />}
|
||||||
{!collapsed && <span className="nav-active-dot" />}
|
</NavLink>
|
||||||
</NavLink>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
<div className="nav-section nav-bottom">
|
||||||
<div className="nav-section nav-bottom">
|
{bottomItems.map(item => (
|
||||||
{bottomItems.map(item => (
|
<NavLink
|
||||||
<NavLink
|
key={item.path}
|
||||||
key={item.path}
|
to={item.path}
|
||||||
to={item.path}
|
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
||||||
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
title={collapsed ? item.label : undefined}
|
||||||
title={collapsed ? item.label : undefined}
|
>
|
||||||
>
|
<span className="nav-icon">{iconPaths[item.icon]}</span>
|
||||||
<span className="nav-icon">{iconPaths[item.icon]}</span>
|
{!collapsed && <span className="nav-label">{item.label}</span>}
|
||||||
{!collapsed && <span className="nav-label">{item.label}</span>}
|
{!collapsed && <span className="nav-active-dot" />}
|
||||||
{!collapsed && <span className="nav-active-dot" />}
|
</NavLink>
|
||||||
</NavLink>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</nav>
|
|
||||||
|
{/* Footer */}
|
||||||
{/* Footer */}
|
<div className="sidebar-footer">
|
||||||
<div className="sidebar-footer">
|
<div className="user-info-mini">
|
||||||
<div className="user-info-mini">
|
<div className="user-avatar-sm">
|
||||||
<div className="user-avatar-sm">
|
{user.username.charAt(0).toUpperCase()}
|
||||||
{user.username.charAt(0).toUpperCase()}
|
</div>
|
||||||
</div>
|
{!collapsed && (
|
||||||
{!collapsed && (
|
<div className="user-details">
|
||||||
<div className="user-details">
|
<span className="user-name">{user.username}</span>
|
||||||
<span className="user-name">{user.username}</span>
|
<span className="user-plan">{user.membership?.planName || '免费版'}</span>
|
||||||
<span className="user-plan">{user.membership?.planName || '免费版'}</span>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
{user.role === 'admin' && (
|
||||||
{user.role === 'admin' && (
|
<NavLink
|
||||||
<NavLink
|
to="/admin"
|
||||||
to="/admin"
|
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
||||||
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
title="管理后台"
|
||||||
title="管理后台"
|
>
|
||||||
>
|
<span className="nav-icon">
|
||||||
<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">
|
||||||
<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"/>
|
||||||
<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>
|
||||||
</svg>
|
</span>
|
||||||
</span>
|
{!collapsed && <span className="nav-label">管理后台</span>}
|
||||||
{!collapsed && <span className="nav-label">管理后台</span>}
|
</NavLink>
|
||||||
</NavLink>
|
)}
|
||||||
)}
|
<button
|
||||||
<button
|
className="nav-item logout-btn"
|
||||||
className="nav-item logout-btn"
|
onClick={() => { logout(); navigate('/login'); }}
|
||||||
onClick={() => { logout(); navigate('/login'); }}
|
title="退出登录"
|
||||||
title="退出登录"
|
>
|
||||||
>
|
<span className="nav-icon">
|
||||||
<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">
|
||||||
<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"/>
|
||||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
<polyline points="16 17 21 12 16 7"/>
|
||||||
<polyline points="16 17 21 12 16 7"/>
|
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
</svg>
|
||||||
</svg>
|
</span>
|
||||||
</span>
|
{!collapsed && <span className="nav-label">退出登录</span>}
|
||||||
{!collapsed && <span className="nav-label">退出登录</span>}
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
</aside>
|
|
||||||
|
{/* Main Content */}
|
||||||
{/* Main Content */}
|
<main className="front-main">
|
||||||
<main className="front-main">
|
<Outlet />
|
||||||
<Outlet />
|
</main>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
export default FrontLayout;
|
||||||
export default FrontLayout;
|
|
||||||
Reference in new issue
Block a user