Integrate SBAS workflows and redesign task center

This commit is contained in:
2026-06-13 13:10:12 +08:00
parent b931e8db53
commit 58a87706ef
275 changed files with 18750 additions and 42524 deletions
@@ -15,7 +15,6 @@ export default function useAppAuthLifecycle({
setHasRadarSearched,
setCurrentUser,
setAuthChecked,
setIsGlobalLocked,
setPendingTaskIds,
setLicenseLoading,
setLicenseStatus,
@@ -72,7 +71,6 @@ export default function useAppAuthLifecycle({
setHasRadarSearched(false);
clearRadarSearchResults();
setCurrentUser(null);
setIsGlobalLocked(false);
setPendingTaskIds([]);
prevLicenseOkRef.current = false;
setAuthChecked(true);
@@ -82,7 +80,6 @@ export default function useAppAuthLifecycle({
radarSearchRequestSeqRef,
setHasRadarSearched,
setCurrentUser,
setIsGlobalLocked,
setPendingTaskIds,
prevLicenseOkRef,
setAuthChecked,
+1 -13
View File
@@ -11,8 +11,6 @@ import { normalizePagePayload } from '../utils/appHelpers';
import { normalizeTaskStatus } from '../utils/appUiHelpers';
import { DEFAULT_LIST_PAGE_SIZE } from '../config/appConstants';
const NON_BLOCKING_TASK_TYPES = new Set(['UNPACK_ARCHIVES', 'UNPACK_SENTINEL1', 'GF3_UNPACK', 'GF3_SARSCAPE_PRODUCE', 'GF3_SARSCAPE_SYNC', 'GF3_SARSCAPE_CLEAN', 'SCAN_ASSET_INVENTORY', 'COPY_DATA']);
export default function useDinsarOperations({
onCleanupDinsarLayers,
fetchRadarImagingDates,
@@ -26,7 +24,7 @@ export default function useDinsarOperations({
setAiStatus, setActiveAiReport,
} = useDinsarStore();
const { setHazardPoints } = useHazardStore();
const { setPendingTaskIds, setNonBlockingTaskIds, setIsGlobalLocked } = useTaskStore();
const { setPendingTaskIds } = useTaskStore();
const { currentUser } = useAuthStore();
const {
hasRadarSearched, radarPagination,
@@ -123,16 +121,8 @@ export default function useDinsarOperations({
};
const handleTaskStart = (taskId, message, options = {}) => {
const taskType = String(options?.taskType || '').trim().toUpperCase();
const isNonBlocking = !!options?.nonBlocking || NON_BLOCKING_TASK_TYPES.has(taskType);
if (taskId) {
setPendingTaskIds(prev => [...prev, taskId]);
if (isNonBlocking) {
setNonBlockingTaskIds(prev => [...new Set([...prev, taskId])]);
}
}
if (!isNonBlocking) {
setIsGlobalLocked(true);
}
if (message) addLog('info', message);
};
@@ -313,7 +303,6 @@ export default function useDinsarOperations({
const handleAnalyzeResult = async (resultId) => {
if (!ensureCanOperate()) return;
addLog('info', `正在对结果 ID:${resultId} 发起 AI 智能诊断任务...`);
setIsGlobalLocked(true);
try {
const response = await apiClient.post(`/ai/analyze-result/${resultId}`);
const taskId = response.data.task_id;
@@ -322,7 +311,6 @@ export default function useDinsarOperations({
} catch (error) {
const msg = error.response?.data?.detail || error.message;
addLog('error', `发起 AI 诊断失败: ${msg}`);
setIsGlobalLocked(false);
}
};
+14 -82
View File
@@ -2,13 +2,6 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import apiClient from '../api/client';
import { normalizeTaskStatus } from '../utils/appUiHelpers';
const NON_BLOCKING_TASK_TYPES = new Set(['UNPACK_ARCHIVES', 'UNPACK_SENTINEL1', 'GF3_UNPACK', 'GF3_SARSCAPE_PRODUCE', 'GF3_SARSCAPE_SYNC', 'GF3_SARSCAPE_CLEAN', 'SCAN_ASSET_INVENTORY', 'COPY_DATA']);
const isTaskNonBlocking = (taskId, taskType, nonBlockingTaskIds = []) => (
NON_BLOCKING_TASK_TYPES.has(String(taskType || '').toUpperCase())
|| nonBlockingTaskIds.includes(taskId)
);
export default function useGlobalTaskControl({
currentUser,
licenseOk,
@@ -16,32 +9,15 @@ export default function useGlobalTaskControl({
setActiveTasks,
pendingTaskIds,
setPendingTaskIds,
nonBlockingTaskIds,
setNonBlockingTaskIds,
isGlobalLocked,
setIsGlobalLocked,
setIsCheckingTasks,
handleTaskCompletionRef,
initializeAppDataRef,
addLog,
}) {
const [forceUnlockPwd, setForceUnlockPwd] = useState('');
const [showForceUnlock, setShowForceUnlock] = useState(false);
const lastLockTimeRef = useRef(null);
const isGlobalLockedRef = useRef(isGlobalLocked);
useEffect(() => {
isGlobalLockedRef.current = isGlobalLocked;
if (isGlobalLocked) {
lastLockTimeRef.current = Date.now();
}
}, [isGlobalLocked]);
const [cancelTaskPwd, setCancelTaskPwd] = useState('');
const [showCancelTask, setShowCancelTask] = useState(false);
// Stable refs so SSE handler doesn't need to re-subscribe on every render
const pendingTaskIdsRef = useRef(pendingTaskIds);
useEffect(() => { pendingTaskIdsRef.current = pendingTaskIds; }, [pendingTaskIds]);
const nonBlockingTaskIdsRef = useRef(nonBlockingTaskIds);
useEffect(() => { nonBlockingTaskIdsRef.current = nonBlockingTaskIds; }, [nonBlockingTaskIds]);
const handleTasksUpdate = useCallback(async (tasks) => {
setActiveTasks(tasks);
@@ -51,26 +27,13 @@ export default function useGlobalTaskControl({
setIsCheckingTasks(false);
const currentPending = pendingTaskIdsRef.current;
let updatedPending = currentPending;
let effectiveNonBlockingTaskIds = Array.from(new Set([
...nonBlockingTaskIdsRef.current,
...tasks
.filter((task) => NON_BLOCKING_TASK_TYPES.has(String(task.task_type || '').toUpperCase()))
.map((task) => task.task_id),
]));
// 如果 pendingTaskIds 为空,但 activeTasks 有任务,说明是刷新后的初始化
// 需要将 activeTasks 中的任务添加到 pendingTaskIds
if (currentPending.length === 0 && hasRunningTasks) {
const activeTaskIds = tasks.map((t) => t.task_id);
const nextNonBlockingIds = tasks
.filter((task) => NON_BLOCKING_TASK_TYPES.has(String(task.task_type || '').toUpperCase()))
.map((task) => task.task_id);
console.log('初始化:将活跃任务添加到 pending 列表:', activeTaskIds);
setPendingTaskIds(activeTaskIds);
setNonBlockingTaskIds(nextNonBlockingIds);
updatedPending = activeTaskIds;
effectiveNonBlockingTaskIds = nextNonBlockingIds;
}
if (currentPending.length > 0) {
@@ -121,47 +84,16 @@ export default function useGlobalTaskControl({
if (reallyFinishedIds.length > 0) {
console.log('真正完成的任务:', reallyFinishedIds);
setPendingTaskIds((prev) => {
const newPending = prev.filter((id) => !reallyFinishedIds.includes(id));
updatedPending = newPending;
return newPending;
});
setNonBlockingTaskIds((prev) => prev.filter((id) => !reallyFinishedIds.includes(id)));
effectiveNonBlockingTaskIds = effectiveNonBlockingTaskIds.filter((id) => !reallyFinishedIds.includes(id));
} else {
// 没有真正完成的任务,保持 updatedPending 不变
updatedPending = currentPending;
setPendingTaskIds((prev) => prev.filter((id) => !reallyFinishedIds.includes(id)));
}
}
}
// 只有当没有运行中的任务且没有待处理的任务时,才解锁
const blockingRunningTasks = tasks.filter((task) => (
!isTaskNonBlocking(task.task_id, task.task_type, effectiveNonBlockingTaskIds)
));
const blockingPendingTaskIds = updatedPending.filter((taskId) => (
!effectiveNonBlockingTaskIds.includes(taskId)
));
const shouldBeLocked = blockingRunningTasks.length > 0 || blockingPendingTaskIds.length > 0;
if (shouldBeLocked !== isGlobalLockedRef.current) {
setIsGlobalLocked(shouldBeLocked);
if (!shouldBeLocked) {
addLog('success', '后台任务已完成,正在同步最新数据...');
setTimeout(() => {
initializeAppDataRef.current?.({ refreshRadarSearch: true });
}, 500);
}
}
}, [
setActiveTasks,
setIsCheckingTasks,
handleTaskCompletionRef,
setPendingTaskIds,
setNonBlockingTaskIds,
setIsGlobalLocked,
addLog,
initializeAppDataRef,
]);
// Fallback polling (used when SSE is unavailable)
@@ -216,22 +148,22 @@ export default function useGlobalTaskControl({
};
}, [currentUser, licenseOk, syncActiveTasks, handleTasksUpdate]);
const handleForceUnlock = useCallback(() => {
if (!forceUnlockPwd || activeTasks.length === 0) return;
const handleCancelActiveTasks = useCallback(() => {
if (!cancelTaskPwd || activeTasks.length === 0) return;
Promise.all(activeTasks.map((task) =>
apiClient.post(`/tasks/${task.task_id}/force-cancel`, { password: forceUnlockPwd }).catch(() => {})
apiClient.post(`/tasks/${task.task_id}/force-cancel`, { password: cancelTaskPwd }).catch(() => {})
)).then(() => {
setForceUnlockPwd('');
setShowForceUnlock(false);
setCancelTaskPwd('');
setShowCancelTask(false);
syncActiveTasks();
});
}, [activeTasks, forceUnlockPwd, syncActiveTasks]);
}, [activeTasks, cancelTaskPwd, syncActiveTasks]);
return {
forceUnlockPwd,
setForceUnlockPwd,
showForceUnlock,
setShowForceUnlock,
handleForceUnlock,
cancelTaskPwd,
setCancelTaskPwd,
showCancelTask,
setShowCancelTask,
handleCancelActiveTasks,
};
}
+37 -2
View File
@@ -5,6 +5,7 @@
* focusBatchAfterCreate, clearPsResults
*/
import apiClient from '../api/client';
import { getPairingHealth } from '../api/pairing';
import {
useUiStore, usePairingStore, useMapStore, useBatchStore, useAuthStore,
} from '../store';
@@ -171,8 +172,42 @@ export default function usePairingLogic({
setPairingAlert({ warnings: [], fallbackUsed: false });
const formData = new FormData();
for (const key in pairingParams) {
const value = pairingParams[key];
const effectivePairingParams = { ...pairingParams };
if (!effectivePairingParams.strategy) {
effectivePairingParams.strategy = 'sbas';
}
if (effectivePairingParams.strategy === 'all') {
const hasDateWindow = Boolean(
effectivePairingParams.master_date_from
|| effectivePairingParams.master_date_to
|| effectivePairingParams.slave_date_from
|| effectivePairingParams.slave_date_to
);
if (!hasDateWindow && pairingAoiMode !== 'region' && !pairingFiles?.length) {
addLog('warn', '全部配对可能返回大量结果。请先限定行政区、上传 AOI 或设置主/从影像时间范围。');
setIsLoading(false);
return;
}
}
try {
const pairingHealth = await getPairingHealth();
if (pairingHealth?.needs_rebuild || pairingHealth?.status !== 'READY') {
addLog(
'warn',
`配对基础当前状态为 ${pairingHealth?.status || 'UNKNOWN'}dirty 场景 ${Number(pairingHealth?.dirty_scene_count || 0)}。请先在“配对规划”页执行“修复配对基础”。`
);
setIsLoading(false);
return;
}
} catch (error) {
addLog('warn', `配对基础状态检查失败: ${error.response?.data?.detail || error.message}`);
setIsLoading(false);
return;
}
for (const key in effectivePairingParams) {
const value = effectivePairingParams[key];
// 跳过 null/undefined 值
if (value === null || value === undefined) continue;
// allowed_satellites 是数组,需要序列化为 JSON
+109
View File
@@ -0,0 +1,109 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTaskStore } from '../store';
import { getRecentTasks, getTaskLogs } from '../api/tasks';
const normalizeList = (value) => (Array.isArray(value) ? value.filter(Boolean) : []);
const matchesTask = (task, taskTypes, taskTypePrefixes, taskIds) => {
const taskId = String(task?.task_id || '');
const taskType = String(task?.task_type || '').toUpperCase();
if (taskIds.length && taskIds.includes(taskId)) return true;
if (taskTypes.length && taskTypes.includes(taskType)) return true;
if (taskTypePrefixes.length && taskTypePrefixes.some(prefix => taskType.startsWith(prefix))) return true;
return !taskTypes.length && !taskTypePrefixes.length && !taskIds.length;
};
export default function useTaskMonitor({
taskTypes = [],
taskTypePrefixes = [],
taskIds = [],
showRecent = false,
recentLimit = 5,
pollRecentMs = 0,
} = {}) {
const activeTasks = useTaskStore((state) => state.activeTasks);
const normalizedTaskTypes = useMemo(
() => normalizeList(taskTypes).map(item => String(item).toUpperCase()),
[taskTypes],
);
const normalizedPrefixes = useMemo(
() => normalizeList(taskTypePrefixes).map(item => String(item).toUpperCase()),
[taskTypePrefixes],
);
const normalizedTaskIds = useMemo(
() => normalizeList(taskIds).map(item => String(item)),
[taskIds],
);
const [recentTasks, setRecentTasks] = useState([]);
const [recentLoading, setRecentLoading] = useState(false);
const [recentError, setRecentError] = useState('');
const filteredActiveTasks = useMemo(
() => activeTasks.filter(task => matchesTask(task, normalizedTaskTypes, normalizedPrefixes, normalizedTaskIds)),
[activeTasks, normalizedTaskTypes, normalizedPrefixes, normalizedTaskIds],
);
const refreshRecentTasks = useCallback(async () => {
if (!showRecent) return [];
setRecentLoading(true);
setRecentError('');
try {
if (!normalizedTaskTypes.length) {
setRecentTasks([]);
return [];
}
const data = await getRecentTasks(normalizedTaskTypes, [], recentLimit, 0);
const tasks = Array.isArray(data) ? data : (data?.tasks || []);
const filtered = tasks.filter(task => matchesTask(task, normalizedTaskTypes, normalizedPrefixes, normalizedTaskIds));
setRecentTasks(filtered);
return filtered;
} catch (error) {
setRecentError(error?.response?.data?.detail || error?.message || '任务记录加载失败');
setRecentTasks([]);
return [];
} finally {
setRecentLoading(false);
}
}, [normalizedTaskTypes, normalizedPrefixes, normalizedTaskIds, recentLimit, showRecent]);
useEffect(() => {
if (!showRecent) {
setRecentTasks([]);
setRecentError('');
return undefined;
}
void refreshRecentTasks();
if (!pollRecentMs) return undefined;
const timer = window.setInterval(() => {
void refreshRecentTasks();
}, pollRecentMs);
return () => window.clearInterval(timer);
}, [pollRecentMs, refreshRecentTasks, showRecent]);
const latestTask = filteredActiveTasks[0] || recentTasks[0] || null;
const isBusy = filteredActiveTasks.length > 0;
const loadTaskLogs = useCallback((taskId, limit = 50, offset = 0) => (
getTaskLogs(taskId, limit, offset)
), []);
return useMemo(() => ({
activeTasks: filteredActiveTasks,
recentTasks,
latestTask,
isBusy,
recentLoading,
recentError,
refreshRecentTasks,
loadTaskLogs,
}), [
filteredActiveTasks,
recentTasks,
latestTask,
isBusy,
recentLoading,
recentError,
refreshRecentTasks,
loadTaskLogs,
]);
}