Integrate SBAS workflows and redesign task center
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user