Move pairing cache repair into planning flow
This commit is contained in:
@@ -218,6 +218,8 @@ async def find_pairs_endpoint(
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise e
|
||||
if isinstance(e, RuntimeError):
|
||||
raise HTTPException(status_code=409, detail=str(e))
|
||||
logger.exception("处理 AOI 或查找干涉对时发生错误")
|
||||
raise HTTPException(status_code=500, detail="处理 AOI 或查找干涉对时发生错误,请查看后端日志")
|
||||
|
||||
|
||||
@@ -74,12 +74,12 @@ class SpatialService:
|
||||
|
||||
if cache_status in {"FAILED", "UNINITIALIZED", "ERROR"} or (scene_count > 1 and pair_count == 0):
|
||||
raise RuntimeError(
|
||||
"配对候选缓存当前不可用,请先在系统自检中执行“全量重建配对缓存”或“增量修复”。"
|
||||
"配对候选缓存当前不可用,请先在生产规划页执行“修复配对基础”或“强制全量重建”。"
|
||||
)
|
||||
|
||||
if degraded:
|
||||
warnings.append(
|
||||
f"配对候选缓存当前状态为 {cache_status},本次结果基于现有缓存生成,建议尽快执行增量修复或全量重建。"
|
||||
f"配对候选缓存当前状态为 {cache_status},本次结果基于现有缓存生成,建议尽快在生产规划页执行缓存修复。"
|
||||
)
|
||||
|
||||
candidate_pool = await self._query_pairing_metric_cache(
|
||||
|
||||
@@ -6,6 +6,7 @@ import { cleanupSessions } from './api/auth';
|
||||
import { syncWaterScenesFromDisk } from './api/water';
|
||||
import { listEngines, runWslCheck } from './api/dinsarProduction';
|
||||
import { getOrbitStatus, syncOrbitPools } from './api/orbit';
|
||||
import { rebuildPairingCache, reconcileDirtyPairingCache } from './api/pairing';
|
||||
import LogManagementPanel from './LogManagementPanel';
|
||||
import DinsarCatalogPanel from './components/DinsarCatalogPanel';
|
||||
|
||||
@@ -120,6 +121,19 @@ const buildConsistencySummary = (stats, en = false) => {
|
||||
|
||||
const HEALTH_PANEL_POLL_INTERVAL_MS = 30000;
|
||||
|
||||
const formatPairingActionMode = (mode, en = false) => {
|
||||
switch (mode) {
|
||||
case 'full_rebuild':
|
||||
return en ? 'Full rebuild' : '全量重建';
|
||||
case 'incremental_reconcile':
|
||||
return en ? 'Incremental reconcile' : '增量修复';
|
||||
case 'noop':
|
||||
return en ? 'No-op reconcile' : '无需修复';
|
||||
default:
|
||||
return en ? 'Pairing cache action' : '配对缓存操作';
|
||||
}
|
||||
};
|
||||
|
||||
const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
||||
const en = language === 'en';
|
||||
const isAdmin = currentUser?.role === 'admin';
|
||||
@@ -142,6 +156,9 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
||||
const [orbitRepairing, setOrbitRepairing] = useState(false);
|
||||
const [orbitQuarantining, setOrbitQuarantining] = useState(false);
|
||||
const [orbitSyncResult, setOrbitSyncResult] = useState(null);
|
||||
const [pairingRebuilding, setPairingRebuilding] = useState(false);
|
||||
const [pairingReconciling, setPairingReconciling] = useState(false);
|
||||
const [pairingActionResult, setPairingActionResult] = useState(null);
|
||||
const statusFetchInFlightRef = useRef(false);
|
||||
|
||||
const refreshOrbitStatus = useCallback(async () => {
|
||||
@@ -166,6 +183,31 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePairingCacheAction = useCallback(async (mode) => {
|
||||
if (!isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setBusyState = mode === 'rebuild' ? setPairingRebuilding : setPairingReconciling;
|
||||
setBusyState(true);
|
||||
setPairingActionResult(null);
|
||||
|
||||
try {
|
||||
const result = mode === 'rebuild'
|
||||
? await rebuildPairingCache()
|
||||
: await reconcileDirtyPairingCache();
|
||||
setPairingActionResult(result);
|
||||
await fetchStatus({ force: true });
|
||||
} catch (err) {
|
||||
setPairingActionResult({
|
||||
mode,
|
||||
error: err.response?.data?.detail || err.message || (en ? 'Pairing cache action failed' : '配对缓存操作失败'),
|
||||
});
|
||||
} finally {
|
||||
setBusyState(false);
|
||||
}
|
||||
}, [en, fetchStatus, isAdmin]);
|
||||
|
||||
const fetchStatus = useCallback(async (options = {}) => {
|
||||
const { force = false } = options;
|
||||
if (statusFetchInFlightRef.current) {
|
||||
@@ -210,13 +252,32 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
||||
useEffect(() => {
|
||||
void fetchStatus();
|
||||
const timer = setInterval(() => {
|
||||
if (syncLoading || cleanupLoading || wslChecking || orbitSyncing || orbitRepairing || orbitQuarantining) {
|
||||
if (
|
||||
syncLoading ||
|
||||
cleanupLoading ||
|
||||
wslChecking ||
|
||||
orbitSyncing ||
|
||||
orbitRepairing ||
|
||||
orbitQuarantining ||
|
||||
pairingRebuilding ||
|
||||
pairingReconciling
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void fetchStatus();
|
||||
}, HEALTH_PANEL_POLL_INTERVAL_MS);
|
||||
return () => clearInterval(timer);
|
||||
}, [cleanupLoading, fetchStatus, orbitQuarantining, orbitRepairing, orbitSyncing, syncLoading, wslChecking]);
|
||||
}, [
|
||||
cleanupLoading,
|
||||
fetchStatus,
|
||||
orbitQuarantining,
|
||||
orbitRepairing,
|
||||
orbitSyncing,
|
||||
pairingRebuilding,
|
||||
pairingReconciling,
|
||||
syncLoading,
|
||||
wslChecking,
|
||||
]);
|
||||
|
||||
const renderBadge = (ok, label = '') => (
|
||||
<span className={`health-badge ${ok ? 'ok' : 'fail'}`}>
|
||||
@@ -287,6 +348,7 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
||||
const orbitEnviWithoutSourceCount = toNumber(orbitSource.envi_without_source_count);
|
||||
const orbitIsce2WithoutSourceCount = toNumber(orbitSource.isce2_without_source_count);
|
||||
const orbitQuarantinePath = orbitSource.quarantine_path || orbitStatus?.source_gaps?.quarantine_path;
|
||||
const pairingActionBusy = pairingRebuilding || pairingReconciling;
|
||||
const orbitOverallHealthy = Boolean(
|
||||
orbitStatus &&
|
||||
orbitMismatchCount === 0 &&
|
||||
@@ -540,6 +602,65 @@ const HealthCheckPanel = ({ language = 'zh', currentUser }) => {
|
||||
{pairingSystem.error}
|
||||
</div>
|
||||
)}
|
||||
{false && (
|
||||
<div className="health-card-note">
|
||||
{en
|
||||
? 'Repair operations require an admin account. Open this panel as admin to rebuild or reconcile the pairing cache.'
|
||||
: '配对缓存修复需要管理员账号。请使用管理员登录后,在此面板执行“全量重建配对缓存”或“增量修复”。'}
|
||||
</div>
|
||||
)}
|
||||
{pairingSystem.needs_rebuild && (
|
||||
<div className="health-card-note">
|
||||
{en
|
||||
? 'Repair entry has moved to Pair Planning. Use the production planning page to repair or rebuild the pairing foundation.'
|
||||
: '配对缓存修复入口已移到“生产规划 -> 配对规划”。请在生产规划页执行配对基础修复或强制全量重建。'}
|
||||
</div>
|
||||
)}
|
||||
{false && (
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 8 }}>
|
||||
<button
|
||||
onClick={() => void handlePairingCacheAction('rebuild')}
|
||||
disabled={pairingActionBusy}
|
||||
style={{ padding: '4px 12px', background: '#b45309', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer', fontSize: 12 }}
|
||||
>
|
||||
{pairingRebuilding
|
||||
? (en ? 'Rebuilding...' : '重建中...')
|
||||
: (en ? 'Full Pairing Rebuild' : '全量重建配对缓存')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void handlePairingCacheAction('reconcile')}
|
||||
disabled={pairingActionBusy}
|
||||
style={{ padding: '4px 12px', background: '#0f766e', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer', fontSize: 12 }}
|
||||
>
|
||||
{pairingReconciling
|
||||
? (en ? 'Reconciling...' : '修复中...')
|
||||
: (en ? 'Incremental Reconcile' : '增量修复')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{false && (
|
||||
<div style={{ marginTop: 8, fontSize: 11, color: '#475569', padding: '8px 10px', background: '#f8fafc', borderRadius: 6 }}>
|
||||
{pairingActionResult.error ? (
|
||||
<div className="health-card-note error">{pairingActionResult.error}</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={`health-card-note ${pairingActionResult.ok ? 'ok' : 'warn'}`}>
|
||||
{`${formatPairingActionMode(pairingActionResult.mode, en)}${en ? ' finished.' : '已完成。'}`}
|
||||
</div>
|
||||
<div className="health-card-note">
|
||||
{en
|
||||
? `Scenes / pairs / dirty: ${toNumber(pairingActionResult.scene_count)} / ${toNumber(pairingActionResult.pair_count)} / ${toNumber(pairingActionResult.dirty_scene_count)}`
|
||||
: `场景 / 候选对 / dirty:${toNumber(pairingActionResult.scene_count)} / ${toNumber(pairingActionResult.pair_count)} / ${toNumber(pairingActionResult.dirty_scene_count)}`}
|
||||
</div>
|
||||
<div className="health-card-note">
|
||||
{en
|
||||
? `Resolved dirty rows: ${toNumber(pairingActionResult.resolved_dirty_rows)}, deleted pair rows: ${toNumber(pairingActionResult.deleted_pair_rows)}`
|
||||
: `已解决 dirty 记录:${toNumber(pairingActionResult.resolved_dirty_rows)},删除旧候选对:${toNumber(pairingActionResult.deleted_pair_rows)}`}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="health-card">
|
||||
|
||||
@@ -2,3 +2,8 @@ import apiClient from './client';
|
||||
|
||||
export const findPairs = (formData) => apiClient.post('/find-pairs', formData).then(r => r.data);
|
||||
export const findPsTimeseries = (formData) => apiClient.post('/find-ps-timeseries', formData).then(r => r.data);
|
||||
export const getPairingHealth = () => apiClient.get('/pairing/health').then(r => r.data);
|
||||
export const rebuildPairingCache = () => apiClient.post('/pairing/rebuild-cache').then(r => r.data);
|
||||
export const reconcileDirtyPairingCache = (forceFull = false) => (
|
||||
apiClient.post('/pairing/reconcile-dirty', null, { params: { force_full: forceFull } }).then(r => r.data)
|
||||
);
|
||||
|
||||
@@ -101,6 +101,16 @@ function PairingModal({
|
||||
}
|
||||
}, [showPairingModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPairingModal || availableSatellites.length === 0) {
|
||||
return;
|
||||
}
|
||||
setSelectedSatellites((prev) => {
|
||||
const normalized = prev.filter((satellite) => availableSatellites.includes(satellite));
|
||||
return normalized.length === prev.length ? prev : normalized;
|
||||
});
|
||||
}, [availableSatellites, showPairingModal]);
|
||||
|
||||
// 同步 selectedSatellites 到 pairingParams
|
||||
useEffect(() => {
|
||||
if (selectedSatellites.length > 0) {
|
||||
|
||||
@@ -25,7 +25,7 @@ const LazyUserAdminPanel = lazy(() => import('../../UserAdminPanel'));
|
||||
const LazyAuditLogPanel = lazy(() => import('../../AuditLogPanel'));
|
||||
const LazyAiQualityPanel = lazy(() => import('../../panels/AiQualityPanel'));
|
||||
const LazyAiAnalysisPanel = lazy(() => import('../../AiAnalysisPanel'));
|
||||
const LazyPairingPanel = lazy(() => import('../../panels/PairingPanel'));
|
||||
const LazyPairingPanel = lazy(() => import('../../panels/PairPlanningPanel'));
|
||||
const LazyDinsarResultPanel = lazy(() => import('../../panels/DinsarResultPanel'));
|
||||
const LazyBatchPanel = lazy(() => import('../../panels/BatchPanel'));
|
||||
const LazyPairsListPanel = lazy(() => import('../../panels/PairsListPanel'));
|
||||
|
||||
@@ -103,7 +103,6 @@ export default function usePairingLogic({
|
||||
const findPairs = async (e, externalRequireOrbitRef) => {
|
||||
e.preventDefault();
|
||||
if (!ensureCanOperate()) return;
|
||||
setShowPairingModal(false);
|
||||
setIsLoading(true);
|
||||
addLog('info', '开始寻找干涉对...');
|
||||
setPairingAlert({ warnings: [], fallbackUsed: false });
|
||||
@@ -182,13 +181,14 @@ export default function usePairingLogic({
|
||||
addLog('warn', '当前配对结果来自降级缓存状态,建议尽快执行缓存修复。');
|
||||
}
|
||||
addLog('success', `成功找到 ${pairs.length} 个干涉对(候选 ${candidateCount},入选 ${selectedEdgeCount})。`);
|
||||
setShowPairingModal(false);
|
||||
setPairingFiles(null);
|
||||
setLeftPanelTab('pairs');
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message;
|
||||
addLog('error', `寻找干涉对失败: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setPairingFiles(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
getPairingHealth,
|
||||
rebuildPairingCache,
|
||||
reconcileDirtyPairingCache,
|
||||
} from '../api/pairing';
|
||||
|
||||
const formatIso = (value, en = false) => {
|
||||
if (!value) return en ? 'Never' : '未执行';
|
||||
try {
|
||||
return new Date(value).toLocaleString();
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const formatActionMode = (mode, en = false) => {
|
||||
switch (mode) {
|
||||
case 'full_rebuild':
|
||||
return en ? 'Full rebuild' : '全量重建';
|
||||
case 'incremental_reconcile':
|
||||
return en ? 'Incremental reconcile' : '增量修复';
|
||||
case 'noop':
|
||||
return en ? 'No-op reconcile' : '无需修复';
|
||||
default:
|
||||
return en ? 'Pairing cache action' : '配对缓存操作';
|
||||
}
|
||||
};
|
||||
|
||||
export default function PairPlanningPanel({
|
||||
foundPairs,
|
||||
selectedPairsCount,
|
||||
isLoading,
|
||||
isReadOnlyUser,
|
||||
hasEnoughRadarScenesForPlanning,
|
||||
onOpenPairingModal,
|
||||
onOpenPsModal,
|
||||
hasRadarSearched,
|
||||
onRefreshRadarSearch,
|
||||
onSearchAll,
|
||||
onRefreshDinsar,
|
||||
language,
|
||||
}) {
|
||||
const en = language === 'en';
|
||||
const [pairingStatus, setPairingStatus] = useState(null);
|
||||
const [pairingStatusLoading, setPairingStatusLoading] = useState(false);
|
||||
const [pairingStatusError, setPairingStatusError] = useState('');
|
||||
const [pairingRepairing, setPairingRepairing] = useState(false);
|
||||
const [pairingFullRebuilding, setPairingFullRebuilding] = useState(false);
|
||||
const [pairingActionResult, setPairingActionResult] = useState(null);
|
||||
|
||||
const refreshPairingStatus = useCallback(async () => {
|
||||
if (isReadOnlyUser) {
|
||||
setPairingStatus(null);
|
||||
setPairingStatusError('');
|
||||
return;
|
||||
}
|
||||
|
||||
setPairingStatusLoading(true);
|
||||
setPairingStatusError('');
|
||||
try {
|
||||
const result = await getPairingHealth();
|
||||
setPairingStatus(result);
|
||||
} catch (error) {
|
||||
setPairingStatus(null);
|
||||
setPairingStatusError(
|
||||
error.response?.data?.detail ||
|
||||
error.message ||
|
||||
(en ? 'Failed to fetch pairing foundation status.' : '配对基础状态获取失败。')
|
||||
);
|
||||
} finally {
|
||||
setPairingStatusLoading(false);
|
||||
}
|
||||
}, [en, isReadOnlyUser]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshPairingStatus();
|
||||
}, [refreshPairingStatus]);
|
||||
|
||||
const handlePairingCacheAction = useCallback(async (mode) => {
|
||||
const setBusy = mode === 'auto' ? setPairingRepairing : setPairingFullRebuilding;
|
||||
setBusy(true);
|
||||
setPairingActionResult(null);
|
||||
try {
|
||||
const result = mode === 'auto'
|
||||
? await reconcileDirtyPairingCache()
|
||||
: await rebuildPairingCache();
|
||||
setPairingActionResult(result);
|
||||
await refreshPairingStatus();
|
||||
} catch (error) {
|
||||
setPairingActionResult({
|
||||
mode,
|
||||
error: error.response?.data?.detail || error.message || (
|
||||
en ? 'Pairing cache action failed.' : '配对缓存操作失败。'
|
||||
),
|
||||
});
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [en, refreshPairingStatus]);
|
||||
|
||||
const pairingActionBusy = pairingRepairing || pairingFullRebuilding;
|
||||
|
||||
return (
|
||||
<div className="panel-content" style={{ flex: '1 1 auto', overflowY: 'auto', padding: '12px' }}>
|
||||
<div className="panel-card">
|
||||
<div className="panel-card-title">{en ? 'Pair Planning' : '配对规划'}</div>
|
||||
<p className="panel-card-desc">
|
||||
{en
|
||||
? 'Filter interferometric pairs by temporal baseline, spatial baseline, and overlap ratio. Optional AOI constraint.'
|
||||
: '基于时间基线、空间基线和重叠率筛选干涉对,可选 AOI 约束范围。'}
|
||||
</p>
|
||||
<div className="header-buttons" style={{ marginTop: '10px' }}>
|
||||
<button onClick={onOpenPairingModal} disabled={isLoading || !hasEnoughRadarScenesForPlanning || isReadOnlyUser} style={{ flex: 1 }}>
|
||||
{en ? 'Pair' : '配对'}
|
||||
</button>
|
||||
<button onClick={onOpenPsModal} disabled={isLoading || !hasEnoughRadarScenesForPlanning || isReadOnlyUser} style={{ flex: 1 }}>
|
||||
{en ? 'PS Prep' : 'PS 准备'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel-card" style={{ marginTop: '12px' }}>
|
||||
<div className="panel-card-title">{en ? 'Pairing Foundation' : '配对基础'}</div>
|
||||
{isReadOnlyUser ? (
|
||||
<p className="panel-card-desc" style={{ marginBottom: 0 }}>
|
||||
{en ? 'Repair operations require an admin account.' : '配对缓存修复需要管理员账号。'}
|
||||
</p>
|
||||
) : pairingStatusLoading ? (
|
||||
<p className="panel-card-desc" style={{ marginBottom: 0 }}>
|
||||
{en ? 'Loading pairing foundation status...' : '正在加载配对基础状态...'}
|
||||
</p>
|
||||
) : pairingStatusError ? (
|
||||
<div style={{ color: '#b91c1c', fontSize: 13 }}>{pairingStatusError}</div>
|
||||
) : pairingStatus ? (
|
||||
<>
|
||||
<div className="panel-card-row">
|
||||
<span>{en ? 'Status' : '状态'}</span>
|
||||
<strong>{pairingStatus.status || (en ? 'Unknown' : '未知')}</strong>
|
||||
</div>
|
||||
<div className="panel-card-row">
|
||||
<span>{en ? 'Scenes / pairs' : '场景 / 候选对'}</span>
|
||||
<strong>{Number(pairingStatus.scene_count || 0)} / {Number(pairingStatus.pair_count || 0)}</strong>
|
||||
</div>
|
||||
<div className="panel-card-row">
|
||||
<span>{en ? 'Dirty scenes' : 'Dirty 场景'}</span>
|
||||
<strong>{Number(pairingStatus.dirty_scene_count || 0)}</strong>
|
||||
</div>
|
||||
<div className="panel-card-row">
|
||||
<span>{en ? 'Last full rebuild' : '上次全量重建'}</span>
|
||||
<strong>{formatIso(pairingStatus.last_full_rebuild_at, en)}</strong>
|
||||
</div>
|
||||
<div className="panel-card-row">
|
||||
<span>{en ? 'Last incremental reconcile' : '上次增量修复'}</span>
|
||||
<strong>{formatIso(pairingStatus.last_incremental_reconcile_at, en)}</strong>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 10,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 8,
|
||||
fontSize: 13,
|
||||
lineHeight: 1.5,
|
||||
background: pairingStatus.needs_rebuild ? '#fff7ed' : '#f0fdf4',
|
||||
color: pairingStatus.needs_rebuild ? '#9a3412' : '#166534',
|
||||
border: `1px solid ${pairingStatus.needs_rebuild ? '#fdba74' : '#86efac'}`,
|
||||
}}
|
||||
>
|
||||
{pairingStatus.needs_rebuild
|
||||
? (en
|
||||
? 'Pairing candidate cache is not ready. Repair it here before running pair search.'
|
||||
: '配对候选缓存当前不可直接用于配对。请先在这里修复,再执行配对搜索。')
|
||||
: (en
|
||||
? 'Pairing foundation is ready. You can proceed with pair planning.'
|
||||
: '配对基础已就绪,可以直接进行配对规划。')}
|
||||
</div>
|
||||
<div className="header-buttons" style={{ marginTop: '10px' }}>
|
||||
<button
|
||||
onClick={() => void handlePairingCacheAction('auto')}
|
||||
disabled={pairingActionBusy || isLoading}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{pairingRepairing
|
||||
? (en ? 'Repairing...' : '修复中...')
|
||||
: (en ? 'Repair Pairing Foundation' : '修复配对基础')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void handlePairingCacheAction('full')}
|
||||
disabled={pairingActionBusy || isLoading}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{pairingFullRebuilding
|
||||
? (en ? 'Rebuilding...' : '重建中...')
|
||||
: (en ? 'Force Full Rebuild' : '强制全量重建')}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#64748b', lineHeight: 1.5 }}>
|
||||
{en
|
||||
? '"Repair Pairing Foundation" will choose incremental reconcile or full rebuild automatically based on current dirty state.'
|
||||
: '“修复配对基础”会根据当前 dirty 状态自动选择增量修复或全量重建;只有需要彻底重算时再使用“强制全量重建”。'}
|
||||
</div>
|
||||
{pairingActionResult && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 10,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 8,
|
||||
background: '#f8fafc',
|
||||
border: '1px solid #e2e8f0',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
{pairingActionResult.error ? (
|
||||
<div style={{ color: '#b91c1c' }}>{pairingActionResult.error}</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ color: '#0f172a', fontWeight: 600 }}>
|
||||
{formatActionMode(pairingActionResult.mode, en)}
|
||||
</div>
|
||||
<div>
|
||||
{en
|
||||
? `Scenes / pairs / dirty: ${Number(pairingActionResult.scene_count || 0)} / ${Number(pairingActionResult.pair_count || 0)} / ${Number(pairingActionResult.dirty_scene_count || 0)}`
|
||||
: `场景 / 候选对 / dirty:${Number(pairingActionResult.scene_count || 0)} / ${Number(pairingActionResult.pair_count || 0)} / ${Number(pairingActionResult.dirty_scene_count || 0)}`}
|
||||
</div>
|
||||
<div>
|
||||
{en
|
||||
? `Resolved dirty rows: ${Number(pairingActionResult.resolved_dirty_rows || 0)}, deleted pair rows: ${Number(pairingActionResult.deleted_pair_rows || 0)}`
|
||||
: `已解决 dirty 记录:${Number(pairingActionResult.resolved_dirty_rows || 0)},删除旧候选对:${Number(pairingActionResult.deleted_pair_rows || 0)}`}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="panel-card-desc" style={{ marginBottom: 0 }}>
|
||||
{en ? 'Pairing foundation status unavailable.' : '配对基础状态暂不可用。'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="panel-card" style={{ marginTop: '12px' }}>
|
||||
<div className="panel-card-title">{en ? 'Results & Refresh' : '结果与刷新'}</div>
|
||||
<div className="panel-card-row">
|
||||
<span>{en ? 'Generated Pairs' : '已生成配对'}</span>
|
||||
<strong>{foundPairs.length}</strong>
|
||||
</div>
|
||||
<div className="panel-card-row">
|
||||
<span>{en ? 'Selected' : '已选中'}</span>
|
||||
<strong>{selectedPairsCount}</strong>
|
||||
</div>
|
||||
<div className="header-buttons" style={{ marginTop: '10px' }}>
|
||||
<button onClick={hasRadarSearched ? onRefreshRadarSearch : onSearchAll} disabled={isLoading} style={{ flex: 1 }}>
|
||||
{hasRadarSearched
|
||||
? (en ? 'Refresh Current Search' : '刷新当前搜索')
|
||||
: (en ? 'Search All Source Data' : '搜索全部源数据')
|
||||
}
|
||||
</button>
|
||||
<button onClick={onRefreshDinsar} disabled={isLoading} style={{ flex: 1 }}>
|
||||
{en ? 'Refresh Results' : '刷新结果'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user