From 28de10061af66ac803d03ba445022bc49dc15ae6 Mon Sep 17 00:00:00 2001 From: mamamiyear Date: Sat, 25 Oct 2025 15:50:29 +0800 Subject: [PATCH] feat: use api for manage people - wrap the backend apis - post people by api - list people by api - auto fill people form by post input api - auto fill people form by post image api --- src/apis/README.md | 200 ++++++++++++++++++++++++++++++++ src/apis/config.ts | 17 +++ src/apis/index.ts | 29 +++++ src/apis/input.ts | 26 +++++ src/apis/people.ts | 168 +++++++++++++++++++++++++++ src/apis/request.ts | 163 ++++++++++++++++++++++++++ src/apis/types.ts | 61 ++++++++++ src/apis/upload.ts | 108 +++++++++++++++++ src/components/InputPanel.tsx | 93 ++++++++++++--- src/components/MainContent.tsx | 10 +- src/components/PeopleForm.tsx | 86 ++++++++++++-- src/components/ResourceList.tsx | 57 +++++---- 12 files changed, 968 insertions(+), 50 deletions(-) create mode 100644 src/apis/README.md create mode 100644 src/apis/config.ts create mode 100644 src/apis/index.ts create mode 100644 src/apis/input.ts create mode 100644 src/apis/people.ts create mode 100644 src/apis/request.ts create mode 100644 src/apis/types.ts create mode 100644 src/apis/upload.ts diff --git a/src/apis/README.md b/src/apis/README.md new file mode 100644 index 0000000..80a7610 --- /dev/null +++ b/src/apis/README.md @@ -0,0 +1,200 @@ +# API 接口封装 + +本目录包含了对 FastAPI 后端接口的完整封装,提供了类型安全的 TypeScript 接口。 + +## 文件结构 + +``` +src/apis/ +├── index.ts # 统一导出文件 +├── config.ts # API 配置 +├── request.ts # 基础请求工具 +├── types.ts # TypeScript 类型定义 +├── input.ts # 文本输入接口 +├── upload.ts # 图片上传接口 +├── people.ts # 人员管理接口 +└── README.md # 使用说明 +``` + +## 使用方法 + +### 1. 导入方式 + +```typescript +// 方式一:导入所有API +import api from '@/apis'; + +// 方式二:按需导入 +import { postInput, getPeoples, postInputImage } from '@/apis'; + +// 方式三:分模块导入 +import { api } from '@/apis'; +const { input, people, upload } = api; +``` + +### 2. 文本输入接口 + +```typescript +import { postInput } from '@/apis'; + +// 提交文本 +try { + const response = await postInput('这是一段文本'); + console.log('提交成功:', response); +} catch (error) { + console.error('提交失败:', error); +} +``` + +### 3. 图片上传接口 + +```typescript +import { postInputImage, validateImageFile } from '@/apis'; + +// 上传图片 +const handleFileUpload = async (file: File) => { + // 验证文件 + const validation = validateImageFile(file); + if (!validation.valid) { + alert(validation.error); + return; + } + + try { + const response = await postInputImage(file); + console.log('上传成功:', response); + } catch (error) { + console.error('上传失败:', error); + } +}; + +// 带进度的上传 +import { postInputImageWithProgress } from '@/apis'; + +const handleFileUploadWithProgress = async (file: File) => { + try { + const response = await postInputImageWithProgress(file, (progress) => { + console.log(`上传进度: ${progress}%`); + }); + console.log('上传成功:', response); + } catch (error) { + console.error('上传失败:', error); + } +}; +``` + +### 4. 人员管理接口 + +```typescript +import { + createPeople, + getPeoples, + searchPeoples, + deletePeople, + getPeoplesPaginated +} from '@/apis'; + +// 创建人员 +const createNewPeople = async () => { + const peopleData = { + name: '张三', + gender: '男', + age: 25, + height: 175, + marital_status: '未婚' + }; + + try { + const response = await createPeople(peopleData); + console.log('创建成功:', response); + } catch (error) { + console.error('创建失败:', error); + } +}; + +// 查询人员列表 +const fetchPeoples = async () => { + try { + const response = await getPeoples({ + limit: 20, + offset: 0 + }); + console.log('查询结果:', response.data); + } catch (error) { + console.error('查询失败:', error); + } +}; + +// 搜索人员 +const searchForPeople = async (keyword: string) => { + try { + const response = await searchPeoples(keyword, 10); + console.log('搜索结果:', response.data); + } catch (error) { + console.error('搜索失败:', error); + } +}; + +// 分页查询 +const fetchPeoplesPaginated = async (page: number) => { + try { + const response = await getPeoplesPaginated(page, 10); + console.log('分页结果:', response.data); + } catch (error) { + console.error('查询失败:', error); + } +}; + +// 删除人员 +const removePeople = async (peopleId: string) => { + try { + const response = await deletePeople(peopleId); + console.log('删除成功:', response); + } catch (error) { + console.error('删除失败:', error); + } +}; +``` + +## 错误处理 + +所有接口都会抛出 `ApiError` 类型的错误,包含以下信息: + +```typescript +try { + const response = await postInput('test'); +} catch (error) { + if (error instanceof ApiError) { + console.log('错误状态码:', error.status); + console.log('错误信息:', error.message); + console.log('错误详情:', error.data); + } +} +``` + +## 类型定义 + +所有接口都提供了完整的 TypeScript 类型支持: + +```typescript +import type { + People, + GetPeoplesParams, + PostInputRequest, + ApiResponse +} from '@/apis'; +``` + +## 配置 + +可以通过修改 `config.ts` 文件来调整 API 配置: + +```typescript +export const API_CONFIG = { + BASE_URL: 'http://127.0.0.1:8099', // API 基础地址 + TIMEOUT: 10000, // 请求超时时间 + HEADERS: { + 'Content-Type': 'application/json', + }, +}; +``` \ No newline at end of file diff --git a/src/apis/config.ts b/src/apis/config.ts new file mode 100644 index 0000000..352809b --- /dev/null +++ b/src/apis/config.ts @@ -0,0 +1,17 @@ +// API 配置 + +export const API_CONFIG = { + BASE_URL: 'http://127.0.0.1:8099', + TIMEOUT: 10000, + HEADERS: { + 'Content-Type': 'application/json', + }, +}; + +// API 端点 +export const API_ENDPOINTS = { + INPUT: '/input', + INPUT_IMAGE: '/input_image', + PEOPLES: '/peoples', + PEOPLE_BY_ID: (id: string) => `/peoples/${id}`, +} as const; \ No newline at end of file diff --git a/src/apis/index.ts b/src/apis/index.ts new file mode 100644 index 0000000..b04df0c --- /dev/null +++ b/src/apis/index.ts @@ -0,0 +1,29 @@ +// API 模块统一导出 + +// 配置和工具 +export * from './config'; +export * from './request'; +export * from './types'; + +// 具体接口 +export * from './input'; +export * from './upload'; +export * from './people'; + +// 默认导出所有API函数 +import * as inputApi from './input'; +import * as uploadApi from './upload'; +import * as peopleApi from './people'; + +export const api = { + // 文本输入相关 + input: inputApi, + + // 图片上传相关 + upload: uploadApi, + + // 人员管理相关 + people: peopleApi, +}; + +export default api; \ No newline at end of file diff --git a/src/apis/input.ts b/src/apis/input.ts new file mode 100644 index 0000000..c538d6e --- /dev/null +++ b/src/apis/input.ts @@ -0,0 +1,26 @@ +// 文本输入相关 API + +import { post } from './request'; +import { API_ENDPOINTS } from './config'; +import type { PostInputRequest, ApiResponse } from './types'; + +/** + * 提交文本输入 + * @param text 输入的文本内容 + * @returns Promise + */ +export async function postInput(text: string): Promise { + const requestData: PostInputRequest = { text }; + // 为 postInput 设置 30 秒超时时间 + return post(API_ENDPOINTS.INPUT, requestData, { timeout: 30000 }); +} + +/** + * 提交文本输入(使用对象参数) + * @param data 包含文本的请求对象 + * @returns Promise + */ +export async function postInputData(data: PostInputRequest): Promise { + // 为 postInputData 设置 30 秒超时时间 + return post(API_ENDPOINTS.INPUT, data, { timeout: 30000 }); +} \ No newline at end of file diff --git a/src/apis/people.ts b/src/apis/people.ts new file mode 100644 index 0000000..3b9363c --- /dev/null +++ b/src/apis/people.ts @@ -0,0 +1,168 @@ +// 人员管理相关 API + +import { get, post, del } from './request'; +import { API_ENDPOINTS } from './config'; +import type { + PostPeopleRequest, + GetPeoplesParams, + People, + ApiResponse, + PaginatedResponse +} from './types'; + +/** + * 创建人员信息 + * @param people 人员信息对象 + * @returns Promise + */ +export async function createPeople(people: Record): Promise { + const requestData: PostPeopleRequest = { people }; + console.log('创建人员请求数据:', requestData); + return post(API_ENDPOINTS.PEOPLES, requestData); +} + +/** + * 查询人员列表 + * @param params 查询参数 + * @returns Promise> + */ +export async function getPeoples(params?: GetPeoplesParams): Promise> { + return get>(API_ENDPOINTS.PEOPLES, params); +} + +/** + * 搜索人员 + * @param searchText 搜索关键词 + * @param topK 返回结果数量,默认5 + * @returns Promise> + */ +export async function searchPeoples( + searchText: string, + topK = 5 +): Promise> { + const params: GetPeoplesParams = { + search: searchText, + top_k: topK, + }; + return get>(API_ENDPOINTS.PEOPLES, params); +} + +/** + * 按条件筛选人员 + * @param filters 筛选条件 + * @returns Promise> + */ +export async function filterPeoples(filters: { + name?: string; + gender?: string; + age?: number; + height?: number; + marital_status?: string; +}): Promise> { + const params: GetPeoplesParams = { + ...filters, + limit: 50, // 默认返回50条 + }; + return get>(API_ENDPOINTS.PEOPLES, params); +} + +/** + * 分页获取人员列表 + * @param page 页码(从1开始) + * @param pageSize 每页数量,默认10 + * @param filters 可选的筛选条件 + * @returns Promise>> + */ +export async function getPeoplesPaginated( + page = 1, + pageSize = 10, + filters?: Partial +): Promise>> { + const params: GetPeoplesParams = { + ...filters, + limit: pageSize, + offset: (page - 1) * pageSize, + }; + + const response = await get>(API_ENDPOINTS.PEOPLES, params); + + // 将响应转换为分页格式 + const paginatedResponse: PaginatedResponse = { + items: response.data || [], + total: response.data?.length || 0, // 注意:实际项目中应该从后端获取总数 + limit: pageSize, + offset: (page - 1) * pageSize, + }; + + return { + ...response, + data: paginatedResponse, + }; +} + +/** + * 删除人员信息 + * @param peopleId 人员ID + * @returns Promise + */ +export async function deletePeople(peopleId: string): Promise { + return del(API_ENDPOINTS.PEOPLE_BY_ID(peopleId)); +} + +/** + * 批量创建人员信息 + * @param peopleList 人员信息数组 + * @returns Promise + */ +export async function createPeoplesBatch( + peopleList: Record[] +): Promise { + const promises = peopleList.map(people => createPeople(people)); + return Promise.all(promises); +} + +/** + * 高级搜索人员 + * @param options 搜索选项 + * @returns Promise> + */ +export async function advancedSearchPeoples(options: { + searchText?: string; + filters?: { + name?: string; + gender?: string; + ageRange?: { min?: number; max?: number }; + heightRange?: { min?: number; max?: number }; + marital_status?: string; + }; + pagination?: { + page?: number; + pageSize?: number; + }; + topK?: number; +}): Promise> { + const { searchText, filters = {}, pagination = {}, topK = 10 } = options; + const { page = 1, pageSize = 10 } = pagination; + + const params: GetPeoplesParams = { + search: searchText, + name: filters.name, + gender: filters.gender, + marital_status: filters.marital_status, + limit: pageSize, + offset: (page - 1) * pageSize, + top_k: topK, + }; + + // 处理年龄范围(这里简化处理,实际可能需要后端支持范围查询) + if (filters.ageRange?.min !== undefined) { + params.age = filters.ageRange.min; + } + + // 处理身高范围(这里简化处理,实际可能需要后端支持范围查询) + if (filters.heightRange?.min !== undefined) { + params.height = filters.heightRange.min; + } + + return get>(API_ENDPOINTS.PEOPLES, params); +} \ No newline at end of file diff --git a/src/apis/request.ts b/src/apis/request.ts new file mode 100644 index 0000000..e87edde --- /dev/null +++ b/src/apis/request.ts @@ -0,0 +1,163 @@ +// 基础请求工具函数 + +import { API_CONFIG } from './config'; +import type { HTTPValidationError } from './types'; + +// 请求选项接口 +export interface RequestOptions { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + headers?: Record; + body?: any; + timeout?: number; +} + +// 自定义错误类 +export class ApiError extends Error { + status?: number; + data?: any; + + constructor(message: string, status?: number, data?: any) { + super(message); + this.name = 'ApiError'; + this.status = status; + this.data = data; + } +} + +// 基础请求函数 +export async function request( + url: string, + options: RequestOptions = {} +): Promise { + const { + method = 'GET', + headers = {}, + body, + timeout = API_CONFIG.TIMEOUT, + } = options; + + const fullUrl = url.startsWith('http') ? url : `${API_CONFIG.BASE_URL}${url}`; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const requestHeaders: Record = { + ...API_CONFIG.HEADERS, + ...headers, + }; + + let requestBody: string | FormData | undefined; + + if (body instanceof FormData) { + // 对于 FormData,不设置 Content-Type,让浏览器自动设置 + delete requestHeaders['Content-Type']; + requestBody = body; + } else if (body) { + requestBody = JSON.stringify(body); + } + + const response = await fetch(fullUrl, { + method, + headers: requestHeaders, + body: requestBody, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + let errorData: any; + try { + errorData = await response.json(); + } catch { + errorData = { message: response.statusText }; + } + + throw new ApiError( + errorData.message || `HTTP ${response.status}: ${response.statusText}`, + response.status, + errorData + ); + } + + // 检查响应是否有内容 + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + return await response.json(); + } else { + // 如果没有 JSON 内容,返回空对象 + return {} as T; + } + } catch (error) { + clearTimeout(timeoutId); + + if (error instanceof ApiError) { + throw error; + } + + if (error instanceof Error) { + if (error.name === 'AbortError') { + throw new ApiError('请求超时', 408); + } + throw new ApiError(error.message); + } + + throw new ApiError('未知错误'); + } +} + +// GET 请求 +export function get(url: string, params?: Record): Promise { + let fullUrl = url; + + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, String(value)); + } + }); + + const queryString = searchParams.toString(); + if (queryString) { + fullUrl += (url.includes('?') ? '&' : '?') + queryString; + } + } + + return request(fullUrl, { method: 'GET' }); +} + +// POST 请求 +export function post(url: string, data?: any, options?: Partial): Promise { + return request(url, { + method: 'POST', + body: data, + ...options, + }); +} + +// PUT 请求 +export function put(url: string, data?: any): Promise { + return request(url, { + method: 'PUT', + body: data, + }); +} + +// DELETE 请求 +export function del(url: string): Promise { + return request(url, { method: 'DELETE' }); +} + +// 文件上传请求 +export function upload(url: string, file: File, fieldName = 'file', options?: Partial): Promise { + const formData = new FormData(); + formData.append(fieldName, file); + + return request(url, { + method: 'POST', + body: formData, + ...options, + }); +} \ No newline at end of file diff --git a/src/apis/types.ts b/src/apis/types.ts new file mode 100644 index 0000000..cae0ee4 --- /dev/null +++ b/src/apis/types.ts @@ -0,0 +1,61 @@ +// API 请求和响应类型定义 + +// 基础响应类型 +export interface ApiResponse { + data?: T; + error_code: number; + error_info?: string; +} + +// 验证错误类型 +export interface ValidationError { + loc: (string | number)[]; + msg: string; + type: string; +} + +export interface HTTPValidationError { + detail: ValidationError[]; +} + +// 文本输入请求类型 +export interface PostInputRequest { + text: string; +} + +// 人员信息请求类型 +export interface PostPeopleRequest { + people: Record; +} + +// 人员查询参数类型 +export interface GetPeoplesParams { + name?: string; + gender?: string; + age?: number; + height?: number; + marital_status?: string; + limit?: number; + offset?: number; + search?: string; + top_k?: number; +} + +// 人员信息类型 +export interface People { + id?: string; + name?: string; + gender?: string; + age?: number; + height?: number; + marital_status?: string; + [key: string]: any; +} + +// 分页响应类型 +export interface PaginatedResponse { + items: T[]; + total: number; + limit: number; + offset: number; +} \ No newline at end of file diff --git a/src/apis/upload.ts b/src/apis/upload.ts new file mode 100644 index 0000000..fe76e93 --- /dev/null +++ b/src/apis/upload.ts @@ -0,0 +1,108 @@ +// 图片上传相关 API + +import { upload } from './request'; +import { API_ENDPOINTS } from './config'; +import type { ApiResponse } from './types'; + +/** + * 上传图片文件 + * @param file 要上传的图片文件 + * @returns Promise + */ +export async function postInputImage(file: File): Promise { + // 验证文件类型 + if (!file.type.startsWith('image/')) { + throw new Error('只能上传图片文件'); + } + + return upload(API_ENDPOINTS.INPUT_IMAGE, file, 'image', { timeout: 30000 }); +} + +/** + * 上传图片文件(带进度回调) + * @param file 要上传的图片文件 + * @param onProgress 上传进度回调函数 + * @returns Promise + */ +export async function postInputImageWithProgress( + file: File, + onProgress?: (progress: number) => void +): Promise { + // 验证文件类型 + if (!file.type.startsWith('image/')) { + throw new Error('只能上传图片文件'); + } + + const formData = new FormData(); + formData.append('file', file); + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + // 监听上传进度 + if (onProgress) { + xhr.upload.addEventListener('progress', (event) => { + if (event.lengthComputable) { + const progress = Math.round((event.loaded / event.total) * 100); + onProgress(progress); + } + }); + } + + // 监听请求完成 + xhr.addEventListener('load', () => { + if (xhr.status >= 200 && xhr.status < 300) { + try { + const response = xhr.responseText ? JSON.parse(xhr.responseText) : {}; + resolve(response); + } catch (error) { + resolve({error_code: 1, error_info: '解析响应失败'}); + } + } else { + reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`)); + } + }); + + // 监听请求错误 + xhr.addEventListener('error', () => { + reject(new Error('网络错误')); + }); + + // 监听请求超时 + xhr.addEventListener('timeout', () => { + reject(new Error('请求超时')); + }); + + // 发送请求 + xhr.open('POST', `http://127.0.0.1:8099${API_ENDPOINTS.INPUT_IMAGE}`); + xhr.timeout = 30000; // 30秒超时 + xhr.send(formData); + }); +} + +/** + * 验证图片文件 + * @param file 文件对象 + * @param maxSize 最大文件大小(字节),默认 10MB + * @returns 验证结果 + */ +export function validateImageFile(file: File, maxSize = 10 * 1024 * 1024): { valid: boolean; error?: string } { + // 检查文件类型 + if (!file.type.startsWith('image/')) { + return { valid: false, error: '只能上传图片文件' }; + } + + // 检查文件大小 + if (file.size > maxSize) { + const maxSizeMB = Math.round(maxSize / (1024 * 1024)); + return { valid: false, error: `文件大小不能超过 ${maxSizeMB}MB` }; + } + + // 检查支持的图片格式 + const supportedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; + if (!supportedTypes.includes(file.type)) { + return { valid: false, error: '支持的图片格式:JPEG、PNG、GIF、WebP' }; + } + + return { valid: true }; +} \ No newline at end of file diff --git a/src/components/InputPanel.tsx b/src/components/InputPanel.tsx index 136e8b7..31a496e 100644 --- a/src/components/InputPanel.tsx +++ b/src/components/InputPanel.tsx @@ -1,29 +1,72 @@ import React from 'react'; -import { Input, Upload, message, Button } from 'antd'; -import { PictureOutlined, SendOutlined } from '@ant-design/icons'; +import { Input, Upload, message, Button, Spin } from 'antd'; +import { PictureOutlined, SendOutlined, LoadingOutlined } from '@ant-design/icons'; +import { postInput, postInputImage } from '../apis'; import './InputPanel.css'; const { TextArea } = Input; -const InputPanel: React.FC = () => { +interface InputPanelProps { + onResult?: (data: any) => void; +} + +const InputPanel: React.FC = ({ onResult }) => { const [value, setValue] = React.useState(''); const [fileList, setFileList] = React.useState([]); + const [loading, setLoading] = React.useState(false); - const send = () => { + const send = async () => { const hasText = value.trim().length > 0; const hasImage = fileList.length > 0; if (!hasText && !hasImage) { message.info('请输入内容或上传图片'); return; } - // 此处替换为真实发送逻辑 - console.log('发送内容:', { text: value, files: fileList }); - setValue(''); - setFileList([]); - message.success('已发送'); + + setLoading(true); + try { + let response; + + // 如果有图片,优先处理图片上传 + if (hasImage) { + const file = fileList[0].originFileObj || fileList[0]; + if (!file) { + message.error('图片文件无效,请重新选择'); + return; + } + + console.log('上传图片:', file.name); + response = await postInputImage(file); + } else { + // 只有文本时,调用文本处理 API + console.log('处理文本:', value.trim()); + response = await postInput(value.trim()); + } + + console.log('API响应:', response); + if (response.error_code === 0 && response.data) { + message.success('处理完成!已自动填充表单'); + // 将结果传递给父组件 + onResult?.(response.data); + + message.info('输入已清空'); + // 清空输入 + setValue(''); + setFileList([]); + } else { + message.error(response.error_info || '处理失败,请重试'); + } + } catch (error) { + console.error('API调用失败:', error); + message.error('网络错误,请检查连接后重试'); + } finally { + setLoading(false); + } }; const onKeyDown = (e: React.KeyboardEvent) => { + if (loading) return; // 加载中时禁用快捷键 + if (e.key === 'Enter') { if (e.shiftKey) { // Shift+Enter 换行(保持默认行为) @@ -37,13 +80,20 @@ const InputPanel: React.FC = () => { return (
-