前言前面六篇完成了后端核心功能用户系统、知识库管理、文档处理、RAG 问答。从今天开始进入前端开发阶段把后端能力转化为用户可以使用的界面。本篇搭建前端页面框架完成仪表盘、登录、知识库管理等核心页面的开发。1. 前端项目结构frontend/src/ ├── main.tsx # 入口 ├── App.tsx # 路由配置 ├── index.css # Tailwind 样式 ├── lib/ │ ├── api.ts # Axios 实例 拦截器 │ └── utils.ts # 工具函数 ├── hooks/ │ ├── useAuth.tsx # 认证状态 │ └── useChat.ts # 流式对话 ├── api/ │ ├── auth.ts # 认证 API │ ├── knowledgeBase.ts # 知识库 API │ └── chat.ts # 对话 API ├── components/ │ ├── Layout.tsx # 布局组件 │ ├── ProtectedRoute.tsx # 路由保护 │ └── ui/ # shadcn/ui 组件 ├── pages/ │ ├── Login.tsx # 登录页 │ ├── Register.tsx # 注册页 │ ├── Dashboard.tsx # 仪表盘 │ ├── KnowledgeBaseDetail.tsx # 知识库详情 │ └── Chat.tsx # 对话页 └── types/ └── index.ts # 类型定义2. 布局组件// frontend/src/components/Layout.tsx import { Outlet, Link, useLocation, useNavigate } from react-router-dom; import { useAuth } from /hooks/useAuth; import { Button } from /components/ui/button; export default function Layout() { const { user, logout } useAuth(); const location useLocation(); const navigate useNavigate(); const navLinks [ { path: /dashboard, label: 知识库, icon: }, { path: /chat, label: 对话, icon: }, ]; return ( div classNamemin-h-screen bg-gray-50 {/* Top Nav */} header classNamebg-white border-b sticky top-0 z-50 div classNamemax-w-7xl mx-auto px-4 h-14 flex items-center justify-between div classNameflex items-center gap-6 Link to/dashboard classNamefont-bold text-blue-600 text-lg KNow /Link nav classNamehidden md:flex items-center gap-1 {navLinks.map((link) ( Link key{link.path} to{link.path} className{px-3 py-1.5 rounded-md text-sm transition ${ location.pathname.startsWith(link.path) ? bg-blue-50 text-blue-600 font-medium : text-gray-600 hover:bg-gray-100 }} {link.icon} {link.label} /Link ))} /nav /div div classNameflex items-center gap-3 Link to/bookmarks classNametext-sm text-gray-500 hover:text-gray-700 收藏 /Link {user ? ( div classNameflex items-center gap-2 span classNametext-sm text-gray-600{user.nickname}/span Button variantoutline sizesm onClick{logout} 退出 /Button /div ) : ( Button sizesm onClick{() navigate(/login)} 登录 /Button )} /div /div /header {/* Mobile Nav */} div classNamemd:hidden fixed bottom-0 left-0 right-0 bg-white border-t z-50 div classNameflex justify-around py-2 {navLinks.map((link) ( Link key{link.path} to{link.path} className{flex flex-col items-center px-3 py-1 text-xs ${ location.pathname.startsWith(link.path) ? text-blue-600 : text-gray-500 }} span classNametext-lg{link.icon}/span span{link.label}/span /Link ))} /div /div {/* Main Content */} main classNamepb-16 md:pb-0 Outlet / /main /div ); }3. 路由保护// frontend/src/components/ProtectedRoute.tsx import { Navigate } from react-router-dom; import { useAuth } from /hooks/useAuth; export function ProtectedRoute({ children }: { children: React.ReactNode }) { const { user, isLoading } useAuth(); if (isLoading) { return ( div classNamemin-h-screen flex items-center justify-center div classNameanimate-spin h-8 w-8 border-4 border-blue-600 border-t-transparent rounded-full / /div ); } if (!user) { return Navigate to/login replace /; } return {children}/; }4. 仪表盘页面// frontend/src/pages/Dashboard.tsx import { useState, useEffect } from react; import { useNavigate } from react-router-dom; import { useAuth } from /hooks/useAuth; import { listKnowledgeBases, createKnowledgeBase, deleteKnowledgeBase, KnowledgeBase, } from /api/knowledgeBase; import { Button } from /components/ui/button; import { Card, CardContent } from /components/ui/card; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from /components/ui/dialog; import { Input } from /components/ui/input; import { Textarea } from /components/ui/textarea; export default function Dashboard() { const { user } useAuth(); const navigate useNavigate(); const [kbs, setKbs] useStateKnowledgeBase[]([]); const [loading, setLoading] useState(true); const [open, setOpen] useState(false); const [name, setName] useState(); const [desc, setDesc] useState(); const load async () { setLoading(true); try { const res await listKnowledgeBases(); setKbs(res.items); } catch (e) { console.error(e); } setLoading(false); }; useEffect(() { load(); }, []); const handleCreate async () { if (!name.trim()) return; await createKnowledgeBase({ name, description: desc }); setOpen(false); setName(); setDesc(); load(); }; const handleDelete async (id: string) { if (!confirm(确定删除文档也会被删除。)) return; await deleteKnowledgeBase(id); load(); }; return ( div classNamemax-w-6xl mx-auto px-4 py-8 div classNameflex items-center justify-between mb-8 div h1 classNametext-2xl font-bold我的知识库/h1 p classNametext-sm text-gray-500 mt-1 欢迎回来{user?.nickname} /p /div Dialog open{open} onOpenChange{setOpen} DialogTrigger asChild Button新建知识库/Button /DialogTrigger DialogContent DialogHeader DialogTitle新建知识库/DialogTitle /DialogHeader div classNamespace-y-4 pt-4 Input placeholder知识库名称 value{name} onChange{(e) setName(e.target.value)} / Textarea placeholder描述可选 value{desc} onChange{(e) setDesc(e.target.value)} / Button onClick{handleCreate} classNamew-full 创建 /Button /div /DialogContent /Dialog /div {loading ? ( div classNametext-center py-20 text-gray-400加载中.../div ) : kbs.length 0 ? ( div classNametext-center py-20 border-2 border-dashed rounded-xl div classNametext-5xl mb-4/div h3 classNametext-lg font-medium text-gray-600 还没有知识库 /h3 p classNametext-sm text-gray-400 mt-1 创建一个知识库开始上传文档 /p Button classNamemt-4 onClick{() setOpen(true)} 新建知识库 /Button /div ) : ( div classNamegrid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 {kbs.map((kb) ( Card key{kb.id} classNamecursor-pointer hover:shadow-md transition onClick{() navigate(/knowledge-bases/${kb.id})} CardContent classNamep-5 h3 classNamefont-semibold text-gray-900{kb.name}/h3 p classNametext-sm text-gray-500 mt-1 line-clamp-2 {kb.description || 暂无描述} /p div classNameflex items-center justify-between mt-4 text-xs text-gray-400 span{kb.document_count} 个文档/span Button variantghost sizesm onClick{(e) { e.stopPropagation(); handleDelete(kb.id); }} 删除 /Button /div /CardContent /Card ))} /div )} /div ); }5. 知识库详情页// frontend/src/pages/KnowledgeBaseDetail.tsx import { useState, useEffect, useRef } from react; import { useParams, useNavigate } from react-router-dom; import { listDocuments, uploadDocument, deleteDocument, Document, } from /api/knowledgeBase; import { Button } from /components/ui/button; import { Card, CardContent } from /components/ui/card; const STATUS_LABEL: Recordstring, string { pending: 等待处理, processing: 处理中, ready: 已完成, failed: 处理失败, }; const STATUS_COLOR: Recordstring, string { pending: bg-yellow-100 text-yellow-700, processing: bg-blue-100 text-blue-700, ready: bg-green-100 text-green-700, failed: bg-red-100 text-red-700, }; export default function KnowledgeBaseDetail() { const { id } useParams(); const navigate useNavigate(); const [docs, setDocs] useStateDocument[]([]); const [loading, setLoading] useState(true); const [uploading, setUploading] useState(false); const fileRef useRefHTMLInputElement(null); const load async () { if (!id) return; setLoading(true); try { const res await listDocuments(id); setDocs(res.items); } catch (e) { console.error(e); } setLoading(false); }; useEffect(() { load(); }, [id]); const handleUpload async (e: React.ChangeEventHTMLInputElement) { const files e.target.files; if (!files?.length || !id) return; setUploading(true); for (const file of Array.from(files)) { try { await uploadDocument(id, file); } catch (err) { console.error(Upload failed:, file.name, err); } } setUploading(false); load(); if (fileRef.current) fileRef.current.value ; }; const handleDelete async (docId: string) { if (!id || !confirm(确定删除)) return; await deleteDocument(id, docId); load(); }; const formatSize (bytes: number) { if (bytes 1024) return bytes B; if (bytes 1024 * 1024) return (bytes / 1024).toFixed(1) KB; return (bytes / 1024 / 1024).toFixed(1) MB; }; return ( div classNamemax-w-4xl mx-auto px-4 py-8 div classNameflex items-center justify-between mb-6 div button onClick{() navigate(/dashboard)} classNametext-sm text-gray-400 hover:text-gray-600 mb-1 block ← 返回 /button h1 classNametext-2xl font-bold文档管理/h1 /div div classNameflex gap-3 Button variantoutline onClick{() navigate(/chat?kb${id})} 开始问答 /Button Button disabled{uploading} onClick{() fileRef.current?.click()} {uploading ? 上传中... : 上传文档} /Button input typefile ref{fileRef} classNamehidden multiple accept.pdf,.txt,.md,.docx onChange{handleUpload} / /div /div {loading ? ( div classNametext-center py-20 text-gray-400加载中.../div ) : docs.length 0 ? ( div classNametext-center py-20 border-2 border-dashed rounded-xl div classNametext-5xl mb-4/div h3 classNametext-lg font-medium text-gray-600还没有文档/h3 p classNametext-sm text-gray-400 mt-1 上传 PDF、TXT、MD 或 DOCX 文件 /p Button classNamemt-4 onClick{() fileRef.current?.click()} 上传第一个文档 /Button /div ) : ( div classNamespace-y-2 {docs.map((doc) ( Card key{doc.id} CardContent classNameflex items-center justify-between py-3 px-4 div classNameflex items-center gap-3 span classNametext-xl {doc.file_type pdf ? : doc.file_type md ? : } /span div p classNametext-sm font-medium{doc.filename}/p p classNametext-xs text-gray-400 {formatSize(doc.file_size)} · {doc.chunk_count} 个片段 /p /div /div div classNameflex items-center gap-2 span className{text-xs px-2 py-1 rounded-full ${ STATUS_COLOR[doc.status] }} {STATUS_LABEL[doc.status]} /span Button variantghost sizesm onClick{() handleDelete(doc.id)} 删除 /Button /div /CardContent /Card ))} /div )} /div ); }6. 路由配置// frontend/src/App.tsx import { BrowserRouter, Routes, Route, Navigate } from react-router-dom; import { AuthProvider } from /hooks/useAuth; import { QueryClient, QueryClientProvider } from tanstack/react-query; import Layout from /components/Layout; import ProtectedRoute from /components/ProtectedRoute; import Login from /pages/Login; import Register from /pages/Register; import Dashboard from /pages/Dashboard; import KnowledgeBaseDetail from /pages/KnowledgeBaseDetail; import Chat from /pages/Chat; const queryClient new QueryClient(); function App() { return ( QueryClientProvider client{queryClient} AuthProvider BrowserRouter Routes Route path/login element{Login /} / Route path/register element{Register /} / Route element{Layout /} Route path/dashboard element{ ProtectedRoute Dashboard / /ProtectedRoute } / Route path/knowledge-bases/:id element{ ProtectedRoute KnowledgeBaseDetail / /ProtectedRoute } / Route path/chat element{ ProtectedRoute Chat / /ProtectedRoute } / /Route Route path* element{Navigate to/dashboard /} / /Routes /BrowserRouter /AuthProvider /QueryClientProvider ); } export default App;7. 入口文件// frontend/src/main.tsx import React from react; import ReactDOM from react-dom/client; import App from ./App; import ./index.css; ReactDOM.createRoot(document.getElementById(root)!).render( React.StrictMode App / /React.StrictMode );8. 样式文件/* frontend/src/index.css */importtailwindcss;layerbase{body{applybg-gray-50 text-gray-900 antialiased;}}9. 前后端联调配置 Vite 代理让前端开发时能调用后端 API// frontend/vite.config.tsimport{defineConfig}fromvite;importreactfromvitejs/plugin-react;importtailwindcssfromtailwindcss/vite;importpathfrompath;exportdefaultdefineConfig({plugins:[react(),tailwindcss()],resolve:{alias:{:path.resolve(__dirname,./src),},},server:{port:3000,proxy:{/api:{target:http://localhost:8000,changeOrigin:true,},},},});Docker 环境下代理指向后端容器proxy:{/api:{target:http://backend:8000,changeOrigin:true,},}10. 验证# 启动前端开发服务器cdfrontendnpmrun dev# 访问openhttp://localhost:3000# 验证流程# 1. 访问 /dashboard → 自动跳转到 /login# 2. 注册一个新账号# 3. 创建知识库 → 看到卡片列表# 4. 点击知识库 → 进入文档管理# 5. 上传文档 → 看到状态变化总结今天完成了前端页面框架的搭建组件功能Layout顶部导航 底部移动端导航 响应式布局ProtectedRoute未登录自动跳转登录页Dashboard知识库列表 新建/删除KnowledgeBaseDetail文档管理 上传/删除/状态App路由配置 Auth/Query Provider下一篇我们继续前端开发——打造流式对话界面实现打字机效果和完整的对话体验。本文是《AI 全栈开发实战——做一个真正的产品》系列的第 7 篇。系列目录1-6. ✅ 后端核心功能7. ✅ 前端开发一——页面框架 ← 你在这里8. 前端开发二——对话界面…本文由 Zyentor智元界 原创发布本文发布于 Zyentor智元界 —— AI 开发者社区原文链接https://www.zyentor.com/news/3790
AI 全栈开发实战(7):前端开发(一)——搭建 KNow 页面框架与核心页面
前言前面六篇完成了后端核心功能用户系统、知识库管理、文档处理、RAG 问答。从今天开始进入前端开发阶段把后端能力转化为用户可以使用的界面。本篇搭建前端页面框架完成仪表盘、登录、知识库管理等核心页面的开发。1. 前端项目结构frontend/src/ ├── main.tsx # 入口 ├── App.tsx # 路由配置 ├── index.css # Tailwind 样式 ├── lib/ │ ├── api.ts # Axios 实例 拦截器 │ └── utils.ts # 工具函数 ├── hooks/ │ ├── useAuth.tsx # 认证状态 │ └── useChat.ts # 流式对话 ├── api/ │ ├── auth.ts # 认证 API │ ├── knowledgeBase.ts # 知识库 API │ └── chat.ts # 对话 API ├── components/ │ ├── Layout.tsx # 布局组件 │ ├── ProtectedRoute.tsx # 路由保护 │ └── ui/ # shadcn/ui 组件 ├── pages/ │ ├── Login.tsx # 登录页 │ ├── Register.tsx # 注册页 │ ├── Dashboard.tsx # 仪表盘 │ ├── KnowledgeBaseDetail.tsx # 知识库详情 │ └── Chat.tsx # 对话页 └── types/ └── index.ts # 类型定义2. 布局组件// frontend/src/components/Layout.tsx import { Outlet, Link, useLocation, useNavigate } from react-router-dom; import { useAuth } from /hooks/useAuth; import { Button } from /components/ui/button; export default function Layout() { const { user, logout } useAuth(); const location useLocation(); const navigate useNavigate(); const navLinks [ { path: /dashboard, label: 知识库, icon: }, { path: /chat, label: 对话, icon: }, ]; return ( div classNamemin-h-screen bg-gray-50 {/* Top Nav */} header classNamebg-white border-b sticky top-0 z-50 div classNamemax-w-7xl mx-auto px-4 h-14 flex items-center justify-between div classNameflex items-center gap-6 Link to/dashboard classNamefont-bold text-blue-600 text-lg KNow /Link nav classNamehidden md:flex items-center gap-1 {navLinks.map((link) ( Link key{link.path} to{link.path} className{px-3 py-1.5 rounded-md text-sm transition ${ location.pathname.startsWith(link.path) ? bg-blue-50 text-blue-600 font-medium : text-gray-600 hover:bg-gray-100 }} {link.icon} {link.label} /Link ))} /nav /div div classNameflex items-center gap-3 Link to/bookmarks classNametext-sm text-gray-500 hover:text-gray-700 收藏 /Link {user ? ( div classNameflex items-center gap-2 span classNametext-sm text-gray-600{user.nickname}/span Button variantoutline sizesm onClick{logout} 退出 /Button /div ) : ( Button sizesm onClick{() navigate(/login)} 登录 /Button )} /div /div /header {/* Mobile Nav */} div classNamemd:hidden fixed bottom-0 left-0 right-0 bg-white border-t z-50 div classNameflex justify-around py-2 {navLinks.map((link) ( Link key{link.path} to{link.path} className{flex flex-col items-center px-3 py-1 text-xs ${ location.pathname.startsWith(link.path) ? text-blue-600 : text-gray-500 }} span classNametext-lg{link.icon}/span span{link.label}/span /Link ))} /div /div {/* Main Content */} main classNamepb-16 md:pb-0 Outlet / /main /div ); }3. 路由保护// frontend/src/components/ProtectedRoute.tsx import { Navigate } from react-router-dom; import { useAuth } from /hooks/useAuth; export function ProtectedRoute({ children }: { children: React.ReactNode }) { const { user, isLoading } useAuth(); if (isLoading) { return ( div classNamemin-h-screen flex items-center justify-center div classNameanimate-spin h-8 w-8 border-4 border-blue-600 border-t-transparent rounded-full / /div ); } if (!user) { return Navigate to/login replace /; } return {children}/; }4. 仪表盘页面// frontend/src/pages/Dashboard.tsx import { useState, useEffect } from react; import { useNavigate } from react-router-dom; import { useAuth } from /hooks/useAuth; import { listKnowledgeBases, createKnowledgeBase, deleteKnowledgeBase, KnowledgeBase, } from /api/knowledgeBase; import { Button } from /components/ui/button; import { Card, CardContent } from /components/ui/card; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from /components/ui/dialog; import { Input } from /components/ui/input; import { Textarea } from /components/ui/textarea; export default function Dashboard() { const { user } useAuth(); const navigate useNavigate(); const [kbs, setKbs] useStateKnowledgeBase[]([]); const [loading, setLoading] useState(true); const [open, setOpen] useState(false); const [name, setName] useState(); const [desc, setDesc] useState(); const load async () { setLoading(true); try { const res await listKnowledgeBases(); setKbs(res.items); } catch (e) { console.error(e); } setLoading(false); }; useEffect(() { load(); }, []); const handleCreate async () { if (!name.trim()) return; await createKnowledgeBase({ name, description: desc }); setOpen(false); setName(); setDesc(); load(); }; const handleDelete async (id: string) { if (!confirm(确定删除文档也会被删除。)) return; await deleteKnowledgeBase(id); load(); }; return ( div classNamemax-w-6xl mx-auto px-4 py-8 div classNameflex items-center justify-between mb-8 div h1 classNametext-2xl font-bold我的知识库/h1 p classNametext-sm text-gray-500 mt-1 欢迎回来{user?.nickname} /p /div Dialog open{open} onOpenChange{setOpen} DialogTrigger asChild Button新建知识库/Button /DialogTrigger DialogContent DialogHeader DialogTitle新建知识库/DialogTitle /DialogHeader div classNamespace-y-4 pt-4 Input placeholder知识库名称 value{name} onChange{(e) setName(e.target.value)} / Textarea placeholder描述可选 value{desc} onChange{(e) setDesc(e.target.value)} / Button onClick{handleCreate} classNamew-full 创建 /Button /div /DialogContent /Dialog /div {loading ? ( div classNametext-center py-20 text-gray-400加载中.../div ) : kbs.length 0 ? ( div classNametext-center py-20 border-2 border-dashed rounded-xl div classNametext-5xl mb-4/div h3 classNametext-lg font-medium text-gray-600 还没有知识库 /h3 p classNametext-sm text-gray-400 mt-1 创建一个知识库开始上传文档 /p Button classNamemt-4 onClick{() setOpen(true)} 新建知识库 /Button /div ) : ( div classNamegrid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 {kbs.map((kb) ( Card key{kb.id} classNamecursor-pointer hover:shadow-md transition onClick{() navigate(/knowledge-bases/${kb.id})} CardContent classNamep-5 h3 classNamefont-semibold text-gray-900{kb.name}/h3 p classNametext-sm text-gray-500 mt-1 line-clamp-2 {kb.description || 暂无描述} /p div classNameflex items-center justify-between mt-4 text-xs text-gray-400 span{kb.document_count} 个文档/span Button variantghost sizesm onClick{(e) { e.stopPropagation(); handleDelete(kb.id); }} 删除 /Button /div /CardContent /Card ))} /div )} /div ); }5. 知识库详情页// frontend/src/pages/KnowledgeBaseDetail.tsx import { useState, useEffect, useRef } from react; import { useParams, useNavigate } from react-router-dom; import { listDocuments, uploadDocument, deleteDocument, Document, } from /api/knowledgeBase; import { Button } from /components/ui/button; import { Card, CardContent } from /components/ui/card; const STATUS_LABEL: Recordstring, string { pending: 等待处理, processing: 处理中, ready: 已完成, failed: 处理失败, }; const STATUS_COLOR: Recordstring, string { pending: bg-yellow-100 text-yellow-700, processing: bg-blue-100 text-blue-700, ready: bg-green-100 text-green-700, failed: bg-red-100 text-red-700, }; export default function KnowledgeBaseDetail() { const { id } useParams(); const navigate useNavigate(); const [docs, setDocs] useStateDocument[]([]); const [loading, setLoading] useState(true); const [uploading, setUploading] useState(false); const fileRef useRefHTMLInputElement(null); const load async () { if (!id) return; setLoading(true); try { const res await listDocuments(id); setDocs(res.items); } catch (e) { console.error(e); } setLoading(false); }; useEffect(() { load(); }, [id]); const handleUpload async (e: React.ChangeEventHTMLInputElement) { const files e.target.files; if (!files?.length || !id) return; setUploading(true); for (const file of Array.from(files)) { try { await uploadDocument(id, file); } catch (err) { console.error(Upload failed:, file.name, err); } } setUploading(false); load(); if (fileRef.current) fileRef.current.value ; }; const handleDelete async (docId: string) { if (!id || !confirm(确定删除)) return; await deleteDocument(id, docId); load(); }; const formatSize (bytes: number) { if (bytes 1024) return bytes B; if (bytes 1024 * 1024) return (bytes / 1024).toFixed(1) KB; return (bytes / 1024 / 1024).toFixed(1) MB; }; return ( div classNamemax-w-4xl mx-auto px-4 py-8 div classNameflex items-center justify-between mb-6 div button onClick{() navigate(/dashboard)} classNametext-sm text-gray-400 hover:text-gray-600 mb-1 block ← 返回 /button h1 classNametext-2xl font-bold文档管理/h1 /div div classNameflex gap-3 Button variantoutline onClick{() navigate(/chat?kb${id})} 开始问答 /Button Button disabled{uploading} onClick{() fileRef.current?.click()} {uploading ? 上传中... : 上传文档} /Button input typefile ref{fileRef} classNamehidden multiple accept.pdf,.txt,.md,.docx onChange{handleUpload} / /div /div {loading ? ( div classNametext-center py-20 text-gray-400加载中.../div ) : docs.length 0 ? ( div classNametext-center py-20 border-2 border-dashed rounded-xl div classNametext-5xl mb-4/div h3 classNametext-lg font-medium text-gray-600还没有文档/h3 p classNametext-sm text-gray-400 mt-1 上传 PDF、TXT、MD 或 DOCX 文件 /p Button classNamemt-4 onClick{() fileRef.current?.click()} 上传第一个文档 /Button /div ) : ( div classNamespace-y-2 {docs.map((doc) ( Card key{doc.id} CardContent classNameflex items-center justify-between py-3 px-4 div classNameflex items-center gap-3 span classNametext-xl {doc.file_type pdf ? : doc.file_type md ? : } /span div p classNametext-sm font-medium{doc.filename}/p p classNametext-xs text-gray-400 {formatSize(doc.file_size)} · {doc.chunk_count} 个片段 /p /div /div div classNameflex items-center gap-2 span className{text-xs px-2 py-1 rounded-full ${ STATUS_COLOR[doc.status] }} {STATUS_LABEL[doc.status]} /span Button variantghost sizesm onClick{() handleDelete(doc.id)} 删除 /Button /div /CardContent /Card ))} /div )} /div ); }6. 路由配置// frontend/src/App.tsx import { BrowserRouter, Routes, Route, Navigate } from react-router-dom; import { AuthProvider } from /hooks/useAuth; import { QueryClient, QueryClientProvider } from tanstack/react-query; import Layout from /components/Layout; import ProtectedRoute from /components/ProtectedRoute; import Login from /pages/Login; import Register from /pages/Register; import Dashboard from /pages/Dashboard; import KnowledgeBaseDetail from /pages/KnowledgeBaseDetail; import Chat from /pages/Chat; const queryClient new QueryClient(); function App() { return ( QueryClientProvider client{queryClient} AuthProvider BrowserRouter Routes Route path/login element{Login /} / Route path/register element{Register /} / Route element{Layout /} Route path/dashboard element{ ProtectedRoute Dashboard / /ProtectedRoute } / Route path/knowledge-bases/:id element{ ProtectedRoute KnowledgeBaseDetail / /ProtectedRoute } / Route path/chat element{ ProtectedRoute Chat / /ProtectedRoute } / /Route Route path* element{Navigate to/dashboard /} / /Routes /BrowserRouter /AuthProvider /QueryClientProvider ); } export default App;7. 入口文件// frontend/src/main.tsx import React from react; import ReactDOM from react-dom/client; import App from ./App; import ./index.css; ReactDOM.createRoot(document.getElementById(root)!).render( React.StrictMode App / /React.StrictMode );8. 样式文件/* frontend/src/index.css */importtailwindcss;layerbase{body{applybg-gray-50 text-gray-900 antialiased;}}9. 前后端联调配置 Vite 代理让前端开发时能调用后端 API// frontend/vite.config.tsimport{defineConfig}fromvite;importreactfromvitejs/plugin-react;importtailwindcssfromtailwindcss/vite;importpathfrompath;exportdefaultdefineConfig({plugins:[react(),tailwindcss()],resolve:{alias:{:path.resolve(__dirname,./src),},},server:{port:3000,proxy:{/api:{target:http://localhost:8000,changeOrigin:true,},},},});Docker 环境下代理指向后端容器proxy:{/api:{target:http://backend:8000,changeOrigin:true,},}10. 验证# 启动前端开发服务器cdfrontendnpmrun dev# 访问openhttp://localhost:3000# 验证流程# 1. 访问 /dashboard → 自动跳转到 /login# 2. 注册一个新账号# 3. 创建知识库 → 看到卡片列表# 4. 点击知识库 → 进入文档管理# 5. 上传文档 → 看到状态变化总结今天完成了前端页面框架的搭建组件功能Layout顶部导航 底部移动端导航 响应式布局ProtectedRoute未登录自动跳转登录页Dashboard知识库列表 新建/删除KnowledgeBaseDetail文档管理 上传/删除/状态App路由配置 Auth/Query Provider下一篇我们继续前端开发——打造流式对话界面实现打字机效果和完整的对话体验。本文是《AI 全栈开发实战——做一个真正的产品》系列的第 7 篇。系列目录1-6. ✅ 后端核心功能7. ✅ 前端开发一——页面框架 ← 你在这里8. 前端开发二——对话界面…本文由 Zyentor智元界 原创发布本文发布于 Zyentor智元界 —— AI 开发者社区原文链接https://www.zyentor.com/news/3790