前言

前面六篇完成了后端核心功能:用户系统、知识库管理、文档处理、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(智元界) 原创发布