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
This commit is contained in:
200
src/apis/README.md
Normal file
200
src/apis/README.md
Normal file
@@ -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',
|
||||
},
|
||||
};
|
||||
```
|
||||
17
src/apis/config.ts
Normal file
17
src/apis/config.ts
Normal file
@@ -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;
|
||||
29
src/apis/index.ts
Normal file
29
src/apis/index.ts
Normal file
@@ -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;
|
||||
26
src/apis/input.ts
Normal file
26
src/apis/input.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// 文本输入相关 API
|
||||
|
||||
import { post } from './request';
|
||||
import { API_ENDPOINTS } from './config';
|
||||
import type { PostInputRequest, ApiResponse } from './types';
|
||||
|
||||
/**
|
||||
* 提交文本输入
|
||||
* @param text 输入的文本内容
|
||||
* @returns Promise<ApiResponse>
|
||||
*/
|
||||
export async function postInput(text: string): Promise<ApiResponse> {
|
||||
const requestData: PostInputRequest = { text };
|
||||
// 为 postInput 设置 30 秒超时时间
|
||||
return post<ApiResponse>(API_ENDPOINTS.INPUT, requestData, { timeout: 30000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交文本输入(使用对象参数)
|
||||
* @param data 包含文本的请求对象
|
||||
* @returns Promise<ApiResponse>
|
||||
*/
|
||||
export async function postInputData(data: PostInputRequest): Promise<ApiResponse> {
|
||||
// 为 postInputData 设置 30 秒超时时间
|
||||
return post<ApiResponse>(API_ENDPOINTS.INPUT, data, { timeout: 30000 });
|
||||
}
|
||||
168
src/apis/people.ts
Normal file
168
src/apis/people.ts
Normal file
@@ -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<ApiResponse>
|
||||
*/
|
||||
export async function createPeople(people: Record<string, any>): Promise<ApiResponse> {
|
||||
const requestData: PostPeopleRequest = { people };
|
||||
console.log('创建人员请求数据:', requestData);
|
||||
return post<ApiResponse>(API_ENDPOINTS.PEOPLES, requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询人员列表
|
||||
* @param params 查询参数
|
||||
* @returns Promise<ApiResponse<People[]>>
|
||||
*/
|
||||
export async function getPeoples(params?: GetPeoplesParams): Promise<ApiResponse<People[]>> {
|
||||
return get<ApiResponse<People[]>>(API_ENDPOINTS.PEOPLES, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索人员
|
||||
* @param searchText 搜索关键词
|
||||
* @param topK 返回结果数量,默认5
|
||||
* @returns Promise<ApiResponse<People[]>>
|
||||
*/
|
||||
export async function searchPeoples(
|
||||
searchText: string,
|
||||
topK = 5
|
||||
): Promise<ApiResponse<People[]>> {
|
||||
const params: GetPeoplesParams = {
|
||||
search: searchText,
|
||||
top_k: topK,
|
||||
};
|
||||
return get<ApiResponse<People[]>>(API_ENDPOINTS.PEOPLES, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按条件筛选人员
|
||||
* @param filters 筛选条件
|
||||
* @returns Promise<ApiResponse<People[]>>
|
||||
*/
|
||||
export async function filterPeoples(filters: {
|
||||
name?: string;
|
||||
gender?: string;
|
||||
age?: number;
|
||||
height?: number;
|
||||
marital_status?: string;
|
||||
}): Promise<ApiResponse<People[]>> {
|
||||
const params: GetPeoplesParams = {
|
||||
...filters,
|
||||
limit: 50, // 默认返回50条
|
||||
};
|
||||
return get<ApiResponse<People[]>>(API_ENDPOINTS.PEOPLES, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取人员列表
|
||||
* @param page 页码(从1开始)
|
||||
* @param pageSize 每页数量,默认10
|
||||
* @param filters 可选的筛选条件
|
||||
* @returns Promise<ApiResponse<PaginatedResponse<People>>>
|
||||
*/
|
||||
export async function getPeoplesPaginated(
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
filters?: Partial<GetPeoplesParams>
|
||||
): Promise<ApiResponse<PaginatedResponse<People>>> {
|
||||
const params: GetPeoplesParams = {
|
||||
...filters,
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
};
|
||||
|
||||
const response = await get<ApiResponse<People[]>>(API_ENDPOINTS.PEOPLES, params);
|
||||
|
||||
// 将响应转换为分页格式
|
||||
const paginatedResponse: PaginatedResponse<People> = {
|
||||
items: response.data || [],
|
||||
total: response.data?.length || 0, // 注意:实际项目中应该从后端获取总数
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
};
|
||||
|
||||
return {
|
||||
...response,
|
||||
data: paginatedResponse,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除人员信息
|
||||
* @param peopleId 人员ID
|
||||
* @returns Promise<ApiResponse>
|
||||
*/
|
||||
export async function deletePeople(peopleId: string): Promise<ApiResponse> {
|
||||
return del<ApiResponse>(API_ENDPOINTS.PEOPLE_BY_ID(peopleId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建人员信息
|
||||
* @param peopleList 人员信息数组
|
||||
* @returns Promise<ApiResponse[]>
|
||||
*/
|
||||
export async function createPeoplesBatch(
|
||||
peopleList: Record<string, any>[]
|
||||
): Promise<ApiResponse[]> {
|
||||
const promises = peopleList.map(people => createPeople(people));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级搜索人员
|
||||
* @param options 搜索选项
|
||||
* @returns Promise<ApiResponse<People[]>>
|
||||
*/
|
||||
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<ApiResponse<People[]>> {
|
||||
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<ApiResponse<People[]>>(API_ENDPOINTS.PEOPLES, params);
|
||||
}
|
||||
163
src/apis/request.ts
Normal file
163
src/apis/request.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
// 基础请求工具函数
|
||||
|
||||
import { API_CONFIG } from './config';
|
||||
import type { HTTPValidationError } from './types';
|
||||
|
||||
// 请求选项接口
|
||||
export interface RequestOptions {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
headers?: Record<string, string>;
|
||||
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<T = any>(
|
||||
url: string,
|
||||
options: RequestOptions = {}
|
||||
): Promise<T> {
|
||||
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<string, string> = {
|
||||
...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<T = any>(url: string, params?: Record<string, any>): Promise<T> {
|
||||
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<T>(fullUrl, { method: 'GET' });
|
||||
}
|
||||
|
||||
// POST 请求
|
||||
export function post<T = any>(url: string, data?: any, options?: Partial<RequestOptions>): Promise<T> {
|
||||
return request<T>(url, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// PUT 请求
|
||||
export function put<T = any>(url: string, data?: any): Promise<T> {
|
||||
return request<T>(url, {
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE 请求
|
||||
export function del<T = any>(url: string): Promise<T> {
|
||||
return request<T>(url, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// 文件上传请求
|
||||
export function upload<T = any>(url: string, file: File, fieldName = 'file', options?: Partial<RequestOptions>): Promise<T> {
|
||||
const formData = new FormData();
|
||||
formData.append(fieldName, file);
|
||||
|
||||
return request<T>(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
61
src/apis/types.ts
Normal file
61
src/apis/types.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// API 请求和响应类型定义
|
||||
|
||||
// 基础响应类型
|
||||
export interface ApiResponse<T = any> {
|
||||
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<string, any>;
|
||||
}
|
||||
|
||||
// 人员查询参数类型
|
||||
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<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
108
src/apis/upload.ts
Normal file
108
src/apis/upload.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// 图片上传相关 API
|
||||
|
||||
import { upload } from './request';
|
||||
import { API_ENDPOINTS } from './config';
|
||||
import type { ApiResponse } from './types';
|
||||
|
||||
/**
|
||||
* 上传图片文件
|
||||
* @param file 要上传的图片文件
|
||||
* @returns Promise<ApiResponse>
|
||||
*/
|
||||
export async function postInputImage(file: File): Promise<ApiResponse> {
|
||||
// 验证文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
throw new Error('只能上传图片文件');
|
||||
}
|
||||
|
||||
return upload<ApiResponse>(API_ENDPOINTS.INPUT_IMAGE, file, 'image', { timeout: 30000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片文件(带进度回调)
|
||||
* @param file 要上传的图片文件
|
||||
* @param onProgress 上传进度回调函数
|
||||
* @returns Promise<ApiResponse>
|
||||
*/
|
||||
export async function postInputImageWithProgress(
|
||||
file: File,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<ApiResponse> {
|
||||
// 验证文件类型
|
||||
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 };
|
||||
}
|
||||
@@ -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<InputPanelProps> = ({ onResult }) => {
|
||||
const [value, setValue] = React.useState('');
|
||||
const [fileList, setFileList] = React.useState<any[]>([]);
|
||||
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 });
|
||||
|
||||
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([]);
|
||||
message.success('已发送');
|
||||
} else {
|
||||
message.error(response.error_info || '处理失败,请重试');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API调用失败:', error);
|
||||
message.error('网络错误,请检查连接后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (loading) return; // 加载中时禁用快捷键
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
if (e.shiftKey) {
|
||||
// Shift+Enter 换行(保持默认行为)
|
||||
@@ -37,13 +80,20 @@ const InputPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="input-panel">
|
||||
<Spin
|
||||
spinning={loading}
|
||||
tip="正在处理中,请稍候..."
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
>
|
||||
<TextArea
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="请输入个人信息描述,或点击右侧上传图片…"
|
||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Spin>
|
||||
<div className="input-actions">
|
||||
<Upload
|
||||
accept="image/*"
|
||||
@@ -52,10 +102,17 @@ const InputPanel: React.FC = () => {
|
||||
onChange={({ fileList }) => setFileList(fileList as any)}
|
||||
maxCount={9}
|
||||
showUploadList={{ showPreviewIcon: false }}
|
||||
disabled={loading}
|
||||
>
|
||||
<Button type="text" icon={<PictureOutlined />} />
|
||||
<Button type="text" icon={<PictureOutlined />} disabled={loading} />
|
||||
</Upload>
|
||||
<Button type="primary" icon={<SendOutlined />} onClick={send} />
|
||||
<Button
|
||||
type="primary"
|
||||
icon={loading ? <LoadingOutlined /> : <SendOutlined />}
|
||||
onClick={send}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,12 @@ import './MainContent.css';
|
||||
const { Content } = Layout;
|
||||
|
||||
const MainContent: React.FC = () => {
|
||||
const [formData, setFormData] = React.useState<any>(null);
|
||||
|
||||
const handleInputResult = (data: any) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Content className="main-content">
|
||||
<div className="content-body">
|
||||
@@ -18,11 +24,11 @@ const MainContent: React.FC = () => {
|
||||
输入个人信息描述,上传图片,我将自动整理资源信息
|
||||
</Typography.Paragraph>
|
||||
|
||||
<PeopleForm />
|
||||
<PeopleForm initialData={formData} />
|
||||
</div>
|
||||
|
||||
<div className="input-panel-wrapper">
|
||||
<InputPanel />
|
||||
<InputPanel onResult={handleInputResult} />
|
||||
<HintText />
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
@@ -1,18 +1,84 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Input, Select, InputNumber, Button, message, Row, Col } from 'antd';
|
||||
import './PeopleForm.css';
|
||||
import KeyValueList from './KeyValueList.tsx'
|
||||
import { createPeople } from '../apis';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const PeopleForm: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
interface PeopleFormProps {
|
||||
initialData?: any;
|
||||
}
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
// 暂时打印内容,模拟提交
|
||||
console.log('People form submit:', values);
|
||||
message.success('表单已提交');
|
||||
const PeopleForm: React.FC<PeopleFormProps> = ({ initialData }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 当 initialData 变化时,自动填充表单
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
console.log('收到API返回数据,自动填充表单:', initialData);
|
||||
|
||||
// 处理返回的数据,将其转换为表单需要的格式
|
||||
const formData: any = {};
|
||||
|
||||
if (initialData.name) formData.name = initialData.name;
|
||||
if (initialData.gender) formData.gender = initialData.gender;
|
||||
if (initialData.age) formData.age = initialData.age;
|
||||
if (initialData.height) formData.height = initialData.height;
|
||||
if (initialData.marital_status) formData.marital_status = initialData.marital_status;
|
||||
if (initialData.match_requirement) formData.match_requirement = initialData.match_requirement;
|
||||
if (initialData.introduction) formData.introduction = initialData.introduction;
|
||||
|
||||
// 设置表单字段值
|
||||
form.setFieldsValue(formData);
|
||||
|
||||
// 显示成功消息
|
||||
message.success('已自动填充表单,请检查并确认信息');
|
||||
}
|
||||
}, [initialData, form]);
|
||||
|
||||
const onFinish = async (values: any) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const peopleData = {
|
||||
name: values.name,
|
||||
gender: values.gender,
|
||||
age: values.age,
|
||||
height: values.height || undefined,
|
||||
marital_status: values.marital_status || undefined,
|
||||
introduction: values.introduction || {},
|
||||
match_requirement: values.match_requirement || undefined,
|
||||
};
|
||||
|
||||
console.log('提交人员数据:', peopleData);
|
||||
|
||||
const response = await createPeople(peopleData);
|
||||
|
||||
console.log('API响应:', response);
|
||||
|
||||
if (response.error_code === 0) {
|
||||
message.success('人员信息已成功提交到后端!');
|
||||
form.resetFields();
|
||||
} else {
|
||||
message.error(response.error_info || '提交失败,请重试');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('提交失败:', error);
|
||||
|
||||
// 根据错误类型显示不同的错误信息
|
||||
if (error.status === 422) {
|
||||
message.error('表单数据格式有误,请检查输入内容');
|
||||
} else if (error.status >= 500) {
|
||||
message.error('服务器错误,请稍后重试');
|
||||
} else {
|
||||
message.error(error.message || '提交失败,请重试');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -75,8 +141,8 @@ const PeopleForm: React.FC = () => {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
提交
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
{loading ? '提交中...' : '提交'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -1,33 +1,50 @@
|
||||
import React from 'react';
|
||||
import { Layout, Typography, Table, Grid, InputNumber, Button, Space, Tag } from 'antd';
|
||||
import { Layout, Typography, Table, Grid, InputNumber, Button, Space, Tag, message } from 'antd';
|
||||
import type { ColumnsType, ColumnType } from 'antd/es/table';
|
||||
import type { FilterDropdownProps } from 'antd/es/table/interface';
|
||||
import type { TableProps } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import './MainContent.css';
|
||||
import { getPeoples } from '../apis';
|
||||
import type { People } from '../apis';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
// 数据类型定义
|
||||
// 数据类型定义 - 使用 API 中的 People 类型
|
||||
export type DictValue = Record<string, string>;
|
||||
export type Resource = {
|
||||
id: string;
|
||||
name: string;
|
||||
gender: '男' | '女' | '其他/保密' | string;
|
||||
age: number;
|
||||
height?: number;
|
||||
marital_status?: string;
|
||||
introduction?: DictValue;
|
||||
};
|
||||
export type Resource = People;
|
||||
|
||||
// 模拟从后端获取资源列表(真实环境替换为实际接口)
|
||||
// 获取人员列表数据
|
||||
async function fetchResources(): Promise<Resource[]> {
|
||||
try {
|
||||
const res = await fetch('/api/resources');
|
||||
if (!res.ok) throw new Error('network');
|
||||
const data = await res.json();
|
||||
return data as Resource[];
|
||||
} catch (e) {
|
||||
const response = await getPeoples({
|
||||
limit: 1000, // 获取大量数据用于前端分页和筛选
|
||||
offset: 0
|
||||
});
|
||||
|
||||
// 检查响应是否成功
|
||||
if (response.error_code !== 0) {
|
||||
console.error('API错误:', response.error_info);
|
||||
message.error(response.error_info || '获取数据失败');
|
||||
// 返回空数组或使用 mock 数据作为后备
|
||||
return [];
|
||||
}
|
||||
|
||||
// 转换数据格式以匹配组件期望的结构
|
||||
return response.data?.map((person: any) => ({
|
||||
id: person.id || `person-${Date.now()}-${Math.random()}`,
|
||||
name: person.name || '未知',
|
||||
gender: person.gender || '其他/保密',
|
||||
age: person.age || 0,
|
||||
height: person.height,
|
||||
marital_status: person.marital_status,
|
||||
introduction: person.introduction || {},
|
||||
})) || [];
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('获取人员列表失败:', error);
|
||||
message.error('获取人员列表失败,使用模拟数据');
|
||||
|
||||
// 回退到 mock 数据,便于本地开发
|
||||
return [
|
||||
{
|
||||
@@ -415,7 +432,7 @@ async function fetchResources(): Promise<Resource[]> {
|
||||
}
|
||||
|
||||
// 数字范围筛选下拉
|
||||
function buildNumberRangeFilter<T extends Resource>(dataIndex: keyof T, label: string): ColumnType<T> {
|
||||
function buildNumberRangeFilter(dataIndex: keyof Resource, label: string): ColumnType<Resource> {
|
||||
return {
|
||||
title: label,
|
||||
dataIndex,
|
||||
@@ -476,7 +493,7 @@ function buildNumberRangeFilter<T extends Resource>(dataIndex: keyof T, label: s
|
||||
if (max !== undefined && val > max) return false;
|
||||
return true;
|
||||
},
|
||||
} as ColumnType<T>;
|
||||
} as ColumnType<Resource>;
|
||||
}
|
||||
|
||||
const ResourceList: React.FC = () => {
|
||||
@@ -592,7 +609,7 @@ const ResourceList: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#9ca3af' }}>{k}</span>
|
||||
<span style={{ color: '#e5e7eb' }}>{v}</span>
|
||||
<span style={{ color: '#e5e7eb' }}>{String(v)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user