You've already forked DataMate
feat(template): 添加模板搜索功能和优化数据获取
- 添加 keyword 参数支持模板名称和描述模糊搜索 - 在 useFetchData hook 中添加 filterParamMapper 参数用于过滤参数映射 - 为模板列表页面实现内置标志过滤器映射功能 - 优化模板配置更新逻辑,改进数据验证和转换流程 - 完善模板服务中的条件查询,支持多字段模糊匹配 - 更新数据获取 hook 的依赖数组以正确处理轮询逻辑
This commit is contained in:
@@ -17,13 +17,23 @@ import { useDebouncedEffect } from "./useDebouncedEffect";
|
||||
import Loading from "@/utils/loading";
|
||||
import { App } from "antd";
|
||||
|
||||
type FetchParams = Record<string, unknown>;
|
||||
type FetchResult<T> = {
|
||||
data?: {
|
||||
content?: Partial<T>[];
|
||||
totalElements?: number;
|
||||
total?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default function useFetchData<T>(
|
||||
fetchFunc: (params?: any) => Promise<any>,
|
||||
fetchFunc: (params?: FetchParams) => Promise<FetchResult<T>>,
|
||||
mapDataFunc: (data: Partial<T>) => T = (data) => data as T,
|
||||
pollingInterval: number = 30000, // 默认30秒轮询一次
|
||||
autoRefresh: boolean = false, // 是否自动开始轮询,默认 false
|
||||
additionalPollingFuncs: (() => Promise<any>)[] = [], // 额外的轮询函数
|
||||
pageOffset: number = 1
|
||||
additionalPollingFuncs: (() => Promise<unknown>)[] = [], // 额外的轮询函数
|
||||
pageOffset: number = 1,
|
||||
filterParamMapper?: (filters: Record<string, unknown>) => Record<string, unknown>
|
||||
) {
|
||||
const { message } = App.useApp();
|
||||
|
||||
@@ -111,6 +121,7 @@ export default function useFetchData<T>(
|
||||
}
|
||||
|
||||
try {
|
||||
const mappedFilterParams = filterParamMapper ? filterParamMapper(filter) : {};
|
||||
// 同时执行主要数据获取和额外的轮询函数
|
||||
const promises = [
|
||||
fetchFunc({
|
||||
@@ -121,6 +132,7 @@ export default function useFetchData<T>(
|
||||
type: getFirstOfArray(filter?.type) || undefined,
|
||||
status: getFirstOfArray(filter?.status) || undefined,
|
||||
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
|
||||
...mappedFilterParams,
|
||||
page: current - pageOffset,
|
||||
size: pageSize, // Use camelCase for HTTP query params
|
||||
}),
|
||||
@@ -167,9 +179,11 @@ export default function useFetchData<T>(
|
||||
mapDataFunc,
|
||||
isPolling,
|
||||
clearPollingTimer,
|
||||
pageOffset,
|
||||
pollingInterval,
|
||||
message,
|
||||
additionalPollingFuncs,
|
||||
filterParamMapper,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -215,7 +229,7 @@ export default function useFetchData<T>(
|
||||
return () => {
|
||||
clearPollingTimer();
|
||||
};
|
||||
}, [clearPollingTimer]);
|
||||
}, [autoRefresh, startPolling, clearPollingTimer]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
|
||||
@@ -56,6 +56,31 @@ const TemplateList: React.FC = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const BUILT_IN_FLAG = {
|
||||
TRUE: "true",
|
||||
FALSE: "false",
|
||||
} as const;
|
||||
|
||||
const mapTemplateFilters = (filters: Record<string, string[]>) => {
|
||||
const getFirstValue = (values?: string[]) =>
|
||||
values && values.length > 0 ? values[0] : undefined;
|
||||
|
||||
const builtInRaw = getFirstValue(filters.builtIn);
|
||||
const builtIn =
|
||||
builtInRaw === BUILT_IN_FLAG.TRUE
|
||||
? true
|
||||
: builtInRaw === BUILT_IN_FLAG.FALSE
|
||||
? false
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
category: getFirstValue(filters.category),
|
||||
dataType: getFirstValue(filters.dataType),
|
||||
labelingType: getFirstValue(filters.labelingType),
|
||||
builtIn,
|
||||
};
|
||||
};
|
||||
|
||||
// Modals
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [isDetailVisible, setIsDetailVisible] = useState(false);
|
||||
@@ -71,7 +96,15 @@ const TemplateList: React.FC = () => {
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
} = useFetchData(queryAnnotationTemplatesUsingGet, undefined, undefined, undefined, undefined, 0);
|
||||
} = useFetchData(
|
||||
queryAnnotationTemplatesUsingGet,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0,
|
||||
mapTemplateFilters
|
||||
);
|
||||
|
||||
const handleCreate = () => {
|
||||
setFormMode("create");
|
||||
|
||||
@@ -67,6 +67,7 @@ async def get_template(
|
||||
async def list_template(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
size: int = Query(10, ge=1, le=100, description="每页大小"),
|
||||
keyword: Optional[str] = Query(None, description="关键词"),
|
||||
category: Optional[str] = Query(None, description="分类筛选"),
|
||||
dataType: Optional[str] = Query(None, alias="dataType", description="数据类型筛选"),
|
||||
labelingType: Optional[str] = Query(None, alias="labelingType", description="标注类型筛选"),
|
||||
@@ -78,6 +79,7 @@ async def list_template(
|
||||
|
||||
- **page**: 页码(从1开始)
|
||||
- **size**: 每页大小(1-100)
|
||||
- **keyword**: 关键词(匹配名称/描述)
|
||||
- **category**: 模板分类筛选
|
||||
- **dataType**: 数据类型筛选
|
||||
- **labelingType**: 标注类型筛选
|
||||
@@ -90,7 +92,8 @@ async def list_template(
|
||||
category=category,
|
||||
data_type=dataType,
|
||||
labeling_type=labelingType,
|
||||
built_in=builtIn
|
||||
built_in=builtIn,
|
||||
keyword=keyword
|
||||
)
|
||||
return StandardResponse(code=200, message="success", data=templates)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Annotation Template Service
|
||||
"""
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy import select, func, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException
|
||||
@@ -185,7 +185,8 @@ class AnnotationTemplateService:
|
||||
category: Optional[str] = None,
|
||||
data_type: Optional[str] = None,
|
||||
labeling_type: Optional[str] = None,
|
||||
built_in: Optional[bool] = None
|
||||
built_in: Optional[bool] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> AnnotationTemplateListResponse:
|
||||
"""
|
||||
获取模板列表
|
||||
@@ -213,6 +214,14 @@ class AnnotationTemplateService:
|
||||
conditions.append(AnnotationTemplate.labeling_type == labeling_type) # type: ignore
|
||||
if built_in is not None:
|
||||
conditions.append(AnnotationTemplate.built_in == built_in) # type: ignore
|
||||
if keyword:
|
||||
like_keyword = f"%{keyword}%"
|
||||
conditions.append(
|
||||
or_(
|
||||
AnnotationTemplate.name.ilike(like_keyword), # type: ignore
|
||||
AnnotationTemplate.description.ilike(like_keyword) # type: ignore
|
||||
)
|
||||
)
|
||||
|
||||
# 查询总数
|
||||
count_result = await db.execute(
|
||||
@@ -273,13 +282,14 @@ class AnnotationTemplateService:
|
||||
for field, value in update_data.items():
|
||||
if field == 'configuration' and value is not None:
|
||||
# 验证配置JSON
|
||||
config_dict = value.model_dump(mode='json', by_alias=False)
|
||||
config = value if isinstance(value, TemplateConfiguration) else TemplateConfiguration.model_validate(value)
|
||||
config_dict = config.model_dump(mode='json', by_alias=False)
|
||||
valid, error = LabelStudioConfigValidator.validate_configuration_json(config_dict)
|
||||
if not valid:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid configuration: {error}")
|
||||
|
||||
# 重新生成Label Studio XML配置(用于验证)
|
||||
label_config = self.generate_label_studio_config(value)
|
||||
label_config = self.generate_label_studio_config(config)
|
||||
|
||||
# 验证生成的XML
|
||||
valid, error = LabelStudioConfigValidator.validate_xml(label_config)
|
||||
|
||||
Reference in New Issue
Block a user