前言
前面六篇完成了后端核心功能:用户系统、知识库管理、文档处理、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 (
{/* Top Nav */}
KNow
{navLinks.map((link) => (
{link.icon} {link.label}
))}
🔖 收藏
{user ? (
{user.nickname}
退出
) : (
navigate("/login")}>
登录
)}
{/* Mobile Nav */}
{navLinks.map((link) => (
{link.icon}
{link.label}
))}
{/* Main Content */}
);
}
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 (
);
}
if (!user) {
return ;
}
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] = useState([]);
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 (
我的知识库
欢迎回来,{user?.nickname}
新建知识库
新建知识库
setName(e.target.value)}
/>
setDesc(e.target.value)}
/>
创建
{loading ? (
加载中...
) : kbs.length === 0 ? (
📚
还没有知识库
创建一个知识库,开始上传文档
setOpen(true)}>
新建知识库
) : (
{kbs.map((kb) => (
navigate(`/knowledge-bases/${kb.id}`)}
>
{kb.name}
{kb.description || "暂无描述"}
{kb.document_count} 个文档
{
e.stopPropagation();
handleDelete(kb.id);
}}
>
删除
))}
)}
);
}
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: Record = {
pending: "等待处理",
processing: "处理中",
ready: "已完成",
failed: "处理失败",
};
const STATUS_COLOR: Record = {
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] = useState([]);
const [loading, setLoading] = useState(true);
const [uploading, setUploading] = useState(false);
const fileRef = useRef(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.ChangeEvent) => {
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
navigate("/dashboard")}
className="text-sm text-gray-400 hover:text-gray-600 mb-1 block"
>
← 返回
文档管理
navigate(`/chat?kb=${id}`)}>
💬 开始问答
fileRef.current?.click()}>
{uploading ? "上传中..." : "上传文档"}
{loading ? (
加载中...
) : docs.length === 0 ? (
📄
还没有文档
上传 PDF、TXT、MD 或 DOCX 文件
fileRef.current?.click()}>
上传第一个文档
) : (
{docs.map((doc) => (
{doc.file_type === "pdf"
? "📕"
: doc.file_type === "md"
? "📝"
: "📄"}
{doc.filename}
{formatSize(doc.file_size)} · {doc.chunk_count} 个片段
{STATUS_LABEL[doc.status]}
handleDelete(doc.id)}
>
删除
))}
)}
);
}
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 (
} />
} />
}>
}
/>
}
/>
}
/>
} />
);
}
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(
);
8. 样式文件
/* frontend/src/index.css */
@import "tailwindcss";
@layer base {
body {
@apply bg-gray-50 text-gray-900 antialiased;
}
}
9. 前后端联调
配置 Vite 代理,让前端开发时能调用后端 API:
// frontend/vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "path";
export default defineConfig({
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. 验证
# 启动前端开发服务器
cd frontend && npm run dev
# 访问
open http://localhost:3000
# 验证流程
# 1. 访问 /dashboard → 自动跳转到 /login
# 2. 注册一个新账号
# 3. 创建知识库 → 看到卡片列表
# 4. 点击知识库 → 进入文档管理
# 5. 上传文档 → 看到状态变化
总结
今天完成了前端页面框架的搭建:
| 组件 | 功能 |
|---|---|
| Layout | 顶部导航 + 底部移动端导航 + 响应式布局 |
| ProtectedRoute | 未登录自动跳转登录页 |
| Dashboard | 知识库列表 + 新建/删除 |
| KnowledgeBaseDetail | 文档管理 + 上传/删除/状态 |
| App | 路由配置 + Auth/Query Provider |
下一篇我们继续前端开发——打造流式对话界面,实现打字机效果和完整的对话体验。
本文是 《AI 全栈开发实战——做一个真正的产品》 系列的第 7 篇。
系列目录:
1-6. ✅ 后端核心功能
7. ✅ 前端开发(一)——页面框架 ← 你在这里
8. 📝 前端开发(二)——对话界面
...本文由 Zyentor(智元界) 原创发布