# Ragflow-Plus 前端开发教程
1. 目的与范围 #
本文档详细介绍了 Ragflow-Plus 前端开发的技术栈、架构模式、组件设计和最佳实践。系统包含两个独立的前端应用:用户前端(React)和管理前端(Vue.js)。
有关系统架构概述,请参阅 系统架构。有关开发指南,请参阅 开发指南。
2. 前端架构概述 #
Ragflow-Plus 采用双前端架构,分别服务于普通用户和管理员。
2.1 双前端架构 #
┌─────────────────────────────────────────┐
│ 用户前端 (User Frontend) │
│ ┌──────────┐ ┌──────────┐ ┌──────┐ │
│ │ React │ │ TypeScript│ │ Vite │ │
│ │ 18+ │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────┘ │
│ 位置: web/src/ │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ 管理前端 (Management Frontend) │
│ ┌──────────┐ ┌──────────┐ ┌──────┐ │
│ │ Vue.js │ │ TypeScript│ │ Vite │ │
│ │ 3 │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────┘ │
│ 位置: management/web/src/ │
└─────────────────────────────────────────┘2.2 技术栈对比 #
| 特性 | 用户前端 | 管理前端 |
|---|---|---|
| 框架 | React 18+ | Vue.js 3 |
| 语言 | TypeScript | TypeScript |
| 构建工具 | Vite | Vite |
| UI 库 | 自定义组件 | Element Plus |
| 状态管理 | Context + Hooks | Composables |
| 路由 | React Router | Vue Router |
| HTTP 客户端 | Axios | Axios |
| 国际化 | react-i18next | vue-i18n |
3. React 应用结构 #
用户前端使用 React + TypeScript 构建,提供聊天、文档编写和知识库管理功能。
3.1 项目目录结构 #
web/
├── src/
│ ├── pages/ # 页面组件
│ │ ├── chat/ # 聊天界面
│ │ │ ├── index.tsx
│ │ │ ├── ChatContainer.tsx
│ │ │ └── MessageItem.tsx
│ │ ├── write/ # 文档编写
│ │ │ ├── index.tsx
│ │ │ └── MarkdownEditor.tsx
│ │ └── add-knowledge/ # 知识库管理
│ │ └── index.tsx
│ ├── components/ # 通用组件
│ │ ├── Header/
│ │ ├── Sidebar/
│ │ └── Loading/
│ ├── services/ # API 服务
│ │ ├── api.ts
│ │ ├── chatService.ts
│ │ └── knowledgebaseService.ts
│ ├── hooks/ # 自定义 Hooks
│ │ ├── useChat.ts
│ │ ├── useKnowledgeBase.ts
│ │ └── useAuth.ts
│ ├── utils/ # 工具函数
│ │ ├── format.ts
│ │ └── validation.ts
│ ├── locales/ # 国际化文件
│ │ ├── zh.ts
│ │ ├── en.ts
│ │ └── zh-traditional.ts
│ ├── App.tsx # 根组件
│ └── main.tsx # 入口文件
├── public/ # 静态资源
├── package.json
└── vite.config.ts # Vite 配置3.2 核心组件设计 #
3.2.1 聊天组件 #
// pages/chat/ChatContainer.tsx
import React, { useState, useEffect } from 'react';
import { useChat } from '@/hooks/useChat';
import MessageItem from './MessageItem';
interface ChatContainerProps {
knowledgeBaseId: string;
}
const ChatContainer: React.FC<ChatContainerProps> = ({ knowledgeBaseId }) => {
const { messages, sendMessage, loading } = useChat(knowledgeBaseId);
const [input, setInput] = useState('');
const handleSend = async () => {
if (!input.trim()) return;
await sendMessage(input);
setInput('');
};
return (
<div className="chat-container">
<div className="messages">
{messages.map(msg => (
<MessageItem key={msg.id} message={msg} />
))}
</div>
<div className="input-area">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
placeholder="输入消息..."
/>
<button onClick={handleSend} disabled={loading}>
发送
</button>
</div>
</div>
);
};3.2.2 自定义 Hook #
// hooks/useChat.ts
import { useState, useCallback } from 'react';
import { chatService } from '@/services/chatService';
import { Message } from '@/types';
export const useChat = (knowledgeBaseId: string) => {
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(false);
const sendMessage = useCallback(async (content: string) => {
setLoading(true);
try {
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
const response = await chatService.sendMessage({
knowledgeBaseId,
content
});
setMessages(prev => [...prev, response]);
} catch (error) {
console.error('发送消息失败:', error);
} finally {
setLoading(false);
}
}, [knowledgeBaseId]);
return { messages, sendMessage, loading };
};3.3 状态管理模式 #
3.3.1 Context API #
// contexts/AuthContext.tsx
import React, { createContext, useContext, useState } from 'react';
interface AuthContextType {
user: User | null;
login: (token: string) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const login = (token: string) => {
// 解析 token 获取用户信息
const userInfo = parseToken(token);
setUser(userInfo);
localStorage.setItem('token', token);
};
const logout = () => {
setUser(null);
localStorage.removeItem('token');
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};4. Vue.js 管理应用 #
管理前端使用 Vue.js 3 + Composition API 构建,提供用户、团队和知识库管理功能。
4.1 项目目录结构 #
management/web/
├── src/
│ ├── pages/ # 页面组件
│ │ ├── user/ # 用户管理
│ │ │ └── index.vue
│ │ ├── team-management/ # 团队管理
│ │ │ └── index.vue
│ │ └── knowledgebase/ # 知识库管理
│ │ └── index.vue
│ ├── components/ # 通用组件
│ ├── common/ # 公共模块
│ │ ├── apis/ # API 客户端
│ │ │ ├── users/
│ │ │ ├── teams/
│ │ │ └── knowledgebases/
│ │ └── composables/ # Composables
│ │ ├── useUserService.ts
│ │ └── useFileUpload.ts
│ ├── http/ # HTTP 配置
│ │ ├── axios.ts
│ │ └── upload-axios.ts
│ ├── App.vue
│ └── main.ts
├── package.json
└── vite.config.ts4.2 Composition API 模式 #
4.2.1 组件示例 #
<!-- pages/user/index.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useUserService } from '@/common/composables/useUserService';
import { ElTable, ElInput, ElButton } from 'element-plus';
const { users, loading, fetchUsers, createUser, deleteUser } = useUserService();
const searchQuery = ref('');
const dialogVisible = ref(false);
onMounted(() => {
fetchUsers();
});
const handleSearch = () => {
fetchUsers({ query: searchQuery.value });
};
const handleCreate = async (userData: any) => {
await createUser(userData);
dialogVisible.value = false;
fetchUsers();
};
</script>
<template>
<div class="user-management">
<div class="toolbar">
<el-input
v-model="searchQuery"
placeholder="搜索用户"
@keyup.enter="handleSearch"
/>
<el-button type="primary" @click="dialogVisible = true">
新建用户
</el-button>
</div>
<el-table :data="users" :loading="loading">
<el-table-column prop="nickname" label="用户名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button @click="deleteUser(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>4.2.2 Composable 模式 #
// common/composables/useUserService.ts
import { ref } from 'vue';
import { userApi } from '@/common/apis/users';
export const useUserService = () => {
const users = ref([]);
const loading = ref(false);
const fetchUsers = async (params?: any) => {
loading.value = true;
try {
const response = await userApi.getUsers(params);
users.value = response.data.list;
} finally {
loading.value = false;
}
};
const createUser = async (userData: any) => {
await userApi.createUser(userData);
await fetchUsers();
};
const deleteUser = async (userId: string) => {
await userApi.deleteUser(userId);
await fetchUsers();
};
return {
users,
loading,
fetchUsers,
createUser,
deleteUser
};
};5. 聊天系统实现 #
5.1 流式响应处理 #
// services/chatService.ts
export const chatService = {
async sendMessage(params: {
knowledgeBaseId: string;
content: string;
}): Promise<Message> {
const eventSource = new EventSource(
`/api/v1/completion?kb_id=${params.knowledgeBaseId}&question=${encodeURIComponent(params.content)}`
);
return new Promise((resolve, reject) => {
let fullContent = '';
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'content') {
fullContent += data.content;
// 更新 UI
} else if (data.type === 'done') {
eventSource.close();
resolve({
id: data.message_id,
role: 'assistant',
content: fullContent,
timestamp: new Date()
});
}
};
eventSource.onerror = (error) => {
eventSource.close();
reject(error);
};
});
}
};5.2 消息渲染 #
// components/MessageItem.tsx
import React from 'react';
import ReactMarkdown from 'react-markdown';
import { Message } from '@/types';
interface MessageItemProps {
message: Message;
}
const MessageItem: React.FC<MessageItemProps> = ({ message }) => {
return (
<div className={`message ${message.role}`}>
<div className="message-header">
<span className="role">{message.role === 'user' ? '用户' : '助手'}</span>
<span className="timestamp">
{new Date(message.timestamp).toLocaleString()}
</span>
</div>
<div className="message-content">
{message.role === 'assistant' ? (
<ReactMarkdown>{message.content}</ReactMarkdown>
) : (
<p>{message.content}</p>
)}
</div>
</div>
);
};6. 状态管理模式 #
6.1 React 状态管理 #
使用 Context API 和自定义 Hooks 进行状态管理:
// contexts/KnowledgeBaseContext.tsx
import React, { createContext, useContext, useState } from 'react';
interface KBContextType {
knowledgeBases: KnowledgeBase[];
selectedKB: KnowledgeBase | null;
setSelectedKB: (kb: KnowledgeBase | null) => void;
refreshKBList: () => Promise<void>;
}
const KBContext = createContext<KBContextType | undefined>(undefined);
export const KBProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
const [selectedKB, setSelectedKB] = useState<KnowledgeBase | null>(null);
const refreshKBList = async () => {
const list = await knowledgebaseService.getList();
setKnowledgeBases(list);
};
return (
<KBContext.Provider value={{
knowledgeBases,
selectedKB,
setSelectedKB,
refreshKBList
}}>
{children}
</KBContext.Provider>
);
};6.2 Vue 状态管理 #
使用 Composables 进行状态管理:
// composables/useKnowledgeBase.ts
import { ref, computed } from 'vue';
import { knowledgebaseApi } from '@/common/apis/knowledgebases';
export const useKnowledgeBase = () => {
const knowledgeBases = ref<KnowledgeBase[]>([]);
const selectedKB = ref<KnowledgeBase | null>(null);
const loading = ref(false);
const fetchList = async () => {
loading.value = true;
try {
const response = await knowledgebaseApi.getList();
knowledgeBases.value = response.data.list;
} finally {
loading.value = false;
}
};
const selectKB = (kb: KnowledgeBase) => {
selectedKB.value = kb;
};
return {
knowledgeBases,
selectedKB,
loading,
fetchList,
selectKB
};
};7. 国际化系统 #
7.1 React 国际化 #
使用 react-i18next:
// i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import zh from './locales/zh';
import en from './locales/en';
i18n
.use(initReactI18next)
.init({
resources: {
zh: { translation: zh },
en: { translation: en }
},
lng: 'zh',
fallbackLng: 'zh',
interpolation: {
escapeValue: false
}
});
// 使用
import { useTranslation } from 'react-i18next';
const MyComponent = () => {
const { t } = useTranslation();
return <h1>{t('welcome')}</h1>;
};7.2 Vue 国际化 #
使用 vue-i18n:
// i18n.ts
import { createI18n } from 'vue-i18n';
import zh from './locales/zh';
import en from './locales/en';
const i18n = createI18n({
locale: 'zh',
fallbackLocale: 'zh',
messages: {
zh,
en
}
});
// 使用
<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
<template>
<h1>{{ t('welcome') }}</h1>
</template>8. 样式与主题系统 #
8.1 CSS Modules #
// ChatContainer.module.css
.chatContainer {
display: flex;
flex-direction: column;
height: 100vh;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.inputArea {
display: flex;
padding: 10px;
border-top: 1px solid #eee;
}
// 使用
import styles from './ChatContainer.module.css';
<div className={styles.chatContainer}>
{/* ... */}
</div>8.2 Tailwind CSS #
// 使用 Tailwind 类名
<div className="flex flex-col h-screen">
<div className="flex-1 overflow-y-auto p-5">
{/* 消息列表 */}
</div>
<div className="flex p-2 border-t">
{/* 输入区域 */}
</div>
</div>9. API 集成模式 #
9.1 API 客户端封装 #
// services/api.ts
import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 30000
});
// 请求拦截器
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
api.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// 处理未授权
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;9.2 服务层封装 #
// services/chatService.ts
import api from './api';
export const chatService = {
sendMessage: (params: {
knowledgeBaseId: string;
content: string;
}) => {
return api.post('/api/v1/completion', {
kb_id: params.knowledgeBaseId,
question: params.content
});
},
getConversation: (conversationId: string) => {
return api.get(`/api/v1/conversation/${conversationId}`);
}
};10. 组件开发模式 #
10.1 组件设计原则 #
- 单一职责:每个组件只负责一个功能
- 可复用性:通过 props 实现组件复用
- 可组合性:小组件组合成复杂组件
- 类型安全:使用 TypeScript 定义接口
10.2 组件示例 #
// components/Button.tsx
import React from 'react';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
children
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};11. 性能优化 #
11.1 React 优化 #
// 使用 React.memo
const MessageItem = React.memo<MessageItemProps>(({ message }) => {
return <div>{message.content}</div>;
});
// 使用 useMemo
const filteredMessages = useMemo(() => {
return messages.filter(msg => msg.role === 'user');
}, [messages]);
// 使用 useCallback
const handleClick = useCallback(() => {
// 处理逻辑
}, [dependencies]);11.2 Vue 优化 #
<script setup>
import { computed, watch } from 'vue';
// 计算属性
const filteredUsers = computed(() => {
return users.value.filter(u => u.active);
});
// 监听器
watch(searchQuery, (newVal) => {
fetchUsers({ query: newVal });
}, { debounce: 300 });
</script>12. 测试 #
12.1 React 测试 #
// ChatContainer.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import ChatContainer from './ChatContainer';
test('发送消息', async () => {
render(<ChatContainer knowledgeBaseId="kb1" />);
const input = screen.getByPlaceholderText('输入消息...');
fireEvent.change(input, { target: { value: '测试消息' } });
const button = screen.getByText('发送');
fireEvent.click(button);
await screen.findByText('测试消息');
});13. 总结 #
本文档介绍了 Ragflow-Plus 前端开发的各个方面:
- 架构设计:双前端架构(React + Vue.js)
- 组件开发:函数组件和 Composition API 模式
- 状态管理:Context API 和 Composables
- API 集成:统一的 API 客户端封装
- 国际化:多语言支持
- 性能优化:React 和 Vue 优化技巧
通过遵循本文档的指南,您可以高效地开发 Ragflow-Plus 前端功能。