diff --git a/frontend/src/components/SearchControls.tsx b/frontend/src/components/SearchControls.tsx index 878f7fcc0..949365454 100644 --- a/frontend/src/components/SearchControls.tsx +++ b/frontend/src/components/SearchControls.tsx @@ -6,6 +6,7 @@ import { ReloadOutlined, } from "@ant-design/icons"; import { useEffect, useState } from "react"; +import React from "react"; import { useTranslation } from "react-i18next"; interface FilterOption { @@ -56,7 +57,8 @@ export function SearchControls({ showViewToggle = true, searchPlaceholder, filters = [], - dateRange, + selectedFilters: externalSelectedFilters, + dateRange: externalDateRange, showDatePicker = false, showReload = true, onReload, @@ -67,10 +69,36 @@ export function SearchControls({ onClearFilters, }: SearchControlsProps) { const { t } = useTranslation(); - const [selectedFilters, setSelectedFilters] = useState<{ + + // 内部状态(如果外部没有传入 selectedFilters) + const [internalSelectedFilters, setInternalSelectedFilters] = useState<{ [key: string]: string[]; }>({}); + // 使用外部传入的 selectedFilters,如果没有则使用内部状态 + const selectedFilters = externalSelectedFilters !== undefined + ? externalSelectedFilters + : internalSelectedFilters; + + // 内部 dateRange 状态(用于禁用逻辑) + const [internalDateRange, setInternalDateRange] = useState(externalDateRange); + + // 同步外部 dateRange 到内部状态 + useEffect(() => { + setInternalDateRange(externalDateRange); + }, [externalDateRange]); + + // 更新筛选值的函数 + const updateSelectedFilters = (newFilters: Record) => { + if (externalSelectedFilters !== undefined) { + // 受控模式:直接调用 onFiltersChange + onFiltersChange?.(newFilters); + } else { + // 非受控模式:更新内部状态 + setInternalSelectedFilters(newFilters); + } + }; + const filtersMap: Record = filters.reduce( (prev, cur) => ({ ...prev, [cur.key]: cur }), {} @@ -80,23 +108,23 @@ export function SearchControls({ const handleFilterChange = (filterKey: string, value: string) => { const filteredValues = { ...selectedFilters, - [filterKey]: !value ? [] : [value], + [filterKey]: !value || value === 'all' ? [] : [value], }; - setSelectedFilters(filteredValues); + updateSelectedFilters(filteredValues); }; // 清除已选筛选 const handleClearFilter = (filterKey: string, value: string | string[]) => { const isMultiple = filtersMap[filterKey]?.mode === "multiple"; if (!isMultiple) { - setSelectedFilters({ + updateSelectedFilters({ ...selectedFilters, [filterKey]: [], }); } else { const currentValues = selectedFilters[filterKey]?.[0] || []; const newValues = currentValues.filter((v) => v !== value); - setSelectedFilters({ + updateSelectedFilters({ ...selectedFilters, [filterKey]: [newValues], }); @@ -104,18 +132,27 @@ export function SearchControls({ }; const handleClearAllFilters = () => { - setSelectedFilters({}); + updateSelectedFilters({}); onClearFilters?.(); }; const hasActiveFilters = Object.values(selectedFilters).some( - (values) => values?.[0]?.length > 0 + (values) => Array.isArray(values) && values.length > 0 && values[0] !== undefined ); + // 同步外部 selectedFilters 到内部状态 useEffect(() => { + if (externalSelectedFilters !== undefined) { + setInternalSelectedFilters(externalSelectedFilters); + } + }, [externalSelectedFilters]); + + // 非受控模式下,当内部状态变化时通知父组件 + useEffect(() => { + if (externalSelectedFilters !== undefined) return; // 受控模式不需要这个 effect if (Object.keys(selectedFilters).length === 0) return; onFiltersChange?.(selectedFilters); - }, [selectedFilters]); + }, [selectedFilters, onFiltersChange, externalSelectedFilters]); return (
@@ -160,11 +197,79 @@ export function SearchControls({ {showDatePicker && ( { + setInternalDateRange(date); + onDateChange?.(date); + }} + showTime={{ format: 'HH:mm:ss' }} + format="YYYY-MM-DD HH:mm:ss" + style={{ width: 380 }} allowClear placeholder={[t('components.searchControls.startTime'), t('components.searchControls.endTime')]} + disabledDate={(current, info) => { + // 只禁用日期部分,同一天不禁用 + const startDate = info.from; + if (!startDate) { + return false; + } + // 如果是同一天,不禁用(让时间选择器处理) + if (current.isSame(startDate, 'day')) { + return false; + } + // 禁用早于开始日期的日期 + return current.isBefore(startDate, 'day'); + }} + disabledTime={(current, partial, info) => { + // partial 是 'start' 或 'end',表示当前正在选择哪个 + // info.from 是已选择的开始日期 + const startDate = info.from; + + if (partial !== 'end' || !startDate) { + return { + disabledHours: () => [], + disabledMinutes: () => [], + disabledSeconds: () => [], + }; + } + + const startHour = startDate.hour(); + const startMinute = startDate.minute(); + const startSecond = startDate.second(); + + return { + disabledHours: () => { + const hours = []; + for (let i = 0; i < startHour; i++) { + hours.push(i); + } + return hours; + }, + disabledMinutes: (selectedHour) => { + if (selectedHour > startHour) { + return []; + } + const minutes = []; + for (let i = 0; i < startMinute; i++) { + minutes.push(i); + } + return minutes; + }, + disabledSeconds: (selectedHour, selectedMinute) => { + if (selectedHour > startHour) { + return []; + } + if (selectedHour === startHour && selectedMinute > startMinute) { + return []; + } + const seconds = []; + for (let i = 0; i < startSecond; i++) { + seconds.push(i); + } + return seconds; + }, + }; + }} /> )} @@ -191,7 +296,7 @@ export function SearchControls({
{/* Active Filters Display */} - {hasActiveFilters && ( + {(hasActiveFilters || internalDateRange) && (
@@ -199,7 +304,8 @@ export function SearchControls({ {t('components.searchControls.selectedFilters')} {Object.entries(selectedFilters).map(([filterKey, values]) => - values.map((value) => { + // 只处理数组类型的筛选值 + Array.isArray(values) && values.map((value) => { const filter = filtersMap[filterKey]; const getLabeledValue = (item: string) => { @@ -222,16 +328,39 @@ export function SearchControls({ : getLabeledValue(value); }) )} + {/* 显示时间范围标签 */} + {internalDateRange && internalDateRange[0] && ( + { + setInternalDateRange(null); + onDateChange?.(null); + }} color="blue"> + {t('components.searchControls.startTime')}: {internalDateRange[0]?.format('YYYY-MM-DD HH:mm:ss')} + + )} + {internalDateRange && internalDateRange[1] && ( + + ~ + { + setInternalDateRange(null); + onDateChange?.(null); + }} color="blue"> + {t('components.searchControls.endTime')}: {internalDateRange[1]?.format('YYYY-MM-DD HH:mm:ss')} + + + )}
{/* Clear all filters button on the right */}
diff --git a/frontend/src/hooks/useFetchData.ts b/frontend/src/hooks/useFetchData.ts index f2ef25ab0..aa37afef6 100644 --- a/frontend/src/hooks/useFetchData.ts +++ b/frontend/src/hooks/useFetchData.ts @@ -114,18 +114,28 @@ export default function useFetchData( try { // 同时执行主要数据获取和额外的轮询函数 + const apiParams = { + categories: filter.categories, + ...extraParams, + keyword, + isStar: filter.selectedStar ? true : undefined, + type: getFirstOfArray(filter?.type) || undefined, + status: getFirstOfArray(filter?.status) || undefined, + built_in: filter?.builtIn !== undefined ? (getFirstOfArray(filter?.builtIn) === "true") : undefined, + tags: filter?.tags?.length ? filter.tags.join(",") : undefined, + page: current - pageOffset, + size: pageSize, // Use camelCase for HTTP query params + }; + + // 添加可能存在的额外参数(如 start_time, end_time 等) + Object.keys(searchParams).forEach(key => { + if (!['keyword', 'filter', 'current', 'pageSize'].includes(key) && apiParams[key as keyof typeof apiParams] === undefined) { + (apiParams as any)[key] = (searchParams as any)[key]; + } + }); + const promises = [ - fetchFunc({ - categories: filter.categories, - ...extraParams, - keyword, - isStar: filter.selectedStar ? true : undefined, - type: getFirstOfArray(filter?.type) || undefined, - status: getFirstOfArray(filter?.status) || undefined, - tags: filter?.tags?.length ? filter.tags.join(",") : undefined, - page: current - pageOffset, - size: pageSize, // Use camelCase for HTTP query params - }), + fetchFunc(apiParams), ...additionalPollingFuncs.map((func) => func()), ]; diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 4768c8489..218a5fc1b 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -2446,6 +2446,9 @@ }, "searchControls": { "searchPlaceholder": "Search...", + "startTime": "Start Time", + "endTime": "End Time", + "selectedFilters": "Selected Filters:", "dateRange": { "start": "Start Date", "end": "End Date", diff --git a/frontend/src/i18n/locales/zh/common.json b/frontend/src/i18n/locales/zh/common.json index 662678330..3de37d945 100644 --- a/frontend/src/i18n/locales/zh/common.json +++ b/frontend/src/i18n/locales/zh/common.json @@ -2446,6 +2446,9 @@ }, "searchControls": { "searchPlaceholder": "搜索...", + "startTime": "开始时间", + "endTime": "结束时间", + "selectedFilters": "已选筛选:", "dateRange": { "start": "开始日期", "end": "结束日期", diff --git a/frontend/src/pages/DataCollection/Home/Execution.tsx b/frontend/src/pages/DataCollection/Home/Execution.tsx index dc5de56b6..584846800 100644 --- a/frontend/src/pages/DataCollection/Home/Execution.tsx +++ b/frontend/src/pages/DataCollection/Home/Execution.tsx @@ -27,9 +27,8 @@ export default function Execution({ taskId }: { taskId?: string }) { options: [ { value: "all", label: t("dataCollection.execution.filters.allStatus") }, { value: "RUNNING", label: t("dataCollection.execution.filters.running") }, - { value: "SUCCESS", label: t("dataCollection.execution.filters.success") }, + { value: "COMPLETED", label: t("dataCollection.execution.filters.success") }, { value: "FAILED", label: t("dataCollection.execution.filters.failed") }, - { value: "STOPPED", label: t("dataCollection.execution.filters.stopped") }, ], }, ]; @@ -44,6 +43,7 @@ export default function Execution({ taskId }: { taskId?: string }) { }; const handleReset = () => { + setDateRange(null); setSearchParams({ keyword: "", filter: { @@ -53,8 +53,9 @@ export default function Execution({ taskId }: { taskId?: string }) { }, current: 1, pageSize: 10, + start_time: undefined, + end_time: undefined, }); - setDateRange(null); }; const { @@ -67,11 +68,12 @@ export default function Execution({ taskId }: { taskId?: string }) { handleKeywordChange, } = useFetchData( (params) => { - const { keyword, start_time, end_time, ...rest } = params || {}; + const { keyword, start_time, end_time, status, ...rest } = params || {}; return queryExecutionLogUsingPost({ ...rest, task_id: taskId || undefined, task_name: keyword || undefined, + status: status || undefined, start_time, end_time, }); @@ -187,26 +189,40 @@ export default function Execution({ taskId }: { taskId?: string }) { searchTerm={searchParams.keyword} onSearchChange={handleKeywordChange} filters={filterOptions} + selectedFilters={searchParams.filter} onFiltersChange={handleFiltersChange} showViewToggle={false} - onClearFilters={() => + onClearFilters={() => { + setDateRange(null); setSearchParams((prev) => ({ ...prev, filter: { ...prev.filter, status: [] }, current: 1, - })) - } + keyword: "", + start_time: undefined, + end_time: undefined, + })); + }} showDatePicker dateRange={dateRange as any} onDateChange={(date) => { + // 自动修正日期顺序:确保开始时间 <= 结束时间 + if (date && date[0] && date[1] && date[0].isAfter(date[1])) { + // 如果开始时间晚于结束时间,交换它们 + date = [date[1], date[0]]; + } setDateRange(date as any); - const start = (date?.[0] as any)?.toISOString?.() || undefined; - const end = (date?.[1] as any)?.toISOString?.() || undefined; + // 转换为不带时区的 ISO 格式字符串(YYYY-MM-DDTHH:mm:ss) + const toLocalISOString = (d: any) => { + if (!d) return undefined; + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.year()}-${pad(d.month() + 1)}-${pad(d.date())}T${pad(d.hour())}:${pad(d.minute())}:${pad(d.second())}`; + }; setSearchParams((prev) => ({ ...prev, current: 1, - start_time: start, - end_time: end, + start_time: toLocalISOString(date?.[0]), + end_time: toLocalISOString(date?.[1]), })); }} onReload={handleReset} diff --git a/frontend/src/pages/DataCollection/Home/TaskManagement.tsx b/frontend/src/pages/DataCollection/Home/TaskManagement.tsx index d6d69fa37..4f6574a21 100644 --- a/frontend/src/pages/DataCollection/Home/TaskManagement.tsx +++ b/frontend/src/pages/DataCollection/Home/TaskManagement.tsx @@ -42,12 +42,14 @@ export default function TaskManagement() { searchParams, setSearchParams, fetchData, + handleFiltersChange, } = useFetchData( (params) => { - const { keyword, ...rest } = params || {}; + const { keyword, status, ...rest } = params || {}; return queryTasksUsingGet({ ...rest, name: keyword || undefined, + status: status || undefined, }); }, (task) => mapCollectionTask(task, t), @@ -247,13 +249,15 @@ export default function TaskManagement() { } searchPlaceholder={t("dataCollection.taskManagement.filters.searchPlaceholder")} filters={filters} - onFiltersChange={() => {}} + selectedFilters={searchParams.filter} + onFiltersChange={handleFiltersChange} showViewToggle={false} onClearFilters={() => setSearchParams((prev) => ({ ...prev, filter: { ...prev.filter, status: [] }, current: 1, + keyword: "", })) } onReload={fetchData} diff --git a/frontend/src/pages/DataCollection/Home/TemplateManagement.tsx b/frontend/src/pages/DataCollection/Home/TemplateManagement.tsx index 544dd2ad2..ae2320aff 100644 --- a/frontend/src/pages/DataCollection/Home/TemplateManagement.tsx +++ b/frontend/src/pages/DataCollection/Home/TemplateManagement.tsx @@ -45,18 +45,10 @@ export default function TemplateManagement() { handleFiltersChange, } = useFetchData( (params) => { - const { keyword, builtIn, ...rest } = params || {}; - const builtInValue = Array.isArray(builtIn) - ? builtIn?.[0] - : builtIn; - + const { keyword, ...rest } = params || {}; return queryDataXTemplatesUsingGet({ ...rest, name: keyword || undefined, - built_in: - builtInValue && builtInValue !== "all" - ? builtInValue === "true" - : undefined, }); }, (tpl) => ({ @@ -145,6 +137,7 @@ export default function TemplateManagement() { } searchPlaceholder={t("dataCollection.templateManagement.filters.searchPlaceholder")} filters={filters} + selectedFilters={searchParams.filter} onFiltersChange={handleFiltersChange} showViewToggle={false} onClearFilters={() => @@ -152,6 +145,7 @@ export default function TemplateManagement() { ...prev, filter: { ...prev.filter, builtIn: [] }, current: 1, + keyword: "", })) } onReload={() => { diff --git a/runtime/datamate-python/app/module/collection/interface/collection.py b/runtime/datamate-python/app/module/collection/interface/collection.py index ba660b268..6a350ab9f 100644 --- a/runtime/datamate-python/app/module/collection/interface/collection.py +++ b/runtime/datamate-python/app/module/collection/interface/collection.py @@ -78,6 +78,7 @@ async def list_tasks( page: int = 1, size: int = 20, name: Optional[str] = Query(None, description="Fuzzy search by task name"), + status: Optional[str] = Query(None, description="Filter by task status"), db: AsyncSession = Depends(get_db) ): """分页查询归集任务""" @@ -89,6 +90,9 @@ async def list_tasks( if name: query = query.where(CollectionTask.name.ilike(f"%{name}%")) + if status: + query = query.where(CollectionTask.status == status) + # 获取总数 count_query = select(func.count()).select_from(query.subquery()) total = (await db.execute(count_query)).scalar_one() diff --git a/runtime/datamate-python/app/module/collection/interface/execution.py b/runtime/datamate-python/app/module/collection/interface/execution.py index 2ec0c9eb6..d0f107e4a 100644 --- a/runtime/datamate-python/app/module/collection/interface/execution.py +++ b/runtime/datamate-python/app/module/collection/interface/execution.py @@ -29,6 +29,7 @@ async def list_executions( size: int = 20, task_id: Optional[str] = Query(None, description="Task ID"), task_name: Optional[str] = Query(None, description="Fuzzy search by task name"), + status: Optional[str] = Query(None, description="Filter by execution status"), start_time: Optional[datetime] = Query(None, description="Start time range from (started_at >= start_time)"), end_time: Optional[datetime] = Query(None, description="Start time range to (started_at <= end_time)"), db: AsyncSession = Depends(get_db) @@ -43,6 +44,9 @@ async def list_executions( if task_name: query = query.where(TaskExecution.task_name.ilike(f"%{task_name}%")) + if status: + query = query.where(TaskExecution.status == status) + if start_time: query = query.where(TaskExecution.started_at >= start_time)