import { useCallback, useEffect, useMemo, useState } from 'react';
import {
getLandsarLt1Capabilities,
listLandsarLt1Products,
previewLandsarLt1Production,
submitLandsarLt1Production,
} from './api/landsarLt1Production';
import { searchRadarData } from './api/radar';
import { getRegionChildren } from './api/aoi';
const PAGE_SIZE_OPTIONS = [50, 100, 200, 500];
const DEFAULT_SEARCH = {
imaging_date_from: '',
imaging_date_to: '',
imaging_mode: '',
polarization: '',
product_level: '',
orbit_direction: '',
relative_orbit: '',
product_unique_id: '',
};
const shellStyle = { display: 'grid', gap: 12 };
const sectionStyle = {
background: '#ffffff',
border: '1px solid #d8dee8',
borderRadius: 8,
padding: 14,
};
const gridStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: 12,
};
const labelStyle = {
display: 'grid',
gap: 5,
color: '#475569',
fontSize: 12,
fontWeight: 650,
};
const inputStyle = {
width: '100%',
boxSizing: 'border-box',
border: '1px solid #cbd5e1',
borderRadius: 6,
padding: '8px 9px',
color: '#0f172a',
background: '#ffffff',
fontSize: 13,
lineHeight: 1.35,
};
const mutedStyle = { color: '#64748b', fontSize: 12, lineHeight: 1.55 };
const buttonStyle = {
border: '1px solid #2563eb',
borderRadius: 6,
background: '#2563eb',
color: '#ffffff',
padding: '8px 12px',
fontSize: 13,
fontWeight: 700,
cursor: 'pointer',
};
const ghostButtonStyle = {
...buttonStyle,
border: '1px solid #cbd5e1',
background: '#ffffff',
color: '#334155',
};
const disabledButtonStyle = {
opacity: 0.5,
cursor: 'not-allowed',
};
const tableHeaderStyle = {
textAlign: 'left',
padding: 8,
borderBottom: '1px solid #e2e8f0',
};
const tableCellStyle = {
padding: 8,
borderBottom: '1px solid #e2e8f0',
};
function formatTime(value) {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString();
}
function formatYmd(value) {
const text = String(value || '').trim();
const compact = text.match(/^(\d{4})(\d{2})(\d{2})$/);
if (compact) return `${compact[1]}-${compact[2]}-${compact[3]}`;
return text || '-';
}
function StatusPill({ ok, text }) {
return (
{text}
);
}
function sceneProduced(scene) {
return Boolean(scene.lt1_image_produced || scene.lt1_landsar_produced);
}
function getSceneTitle(scene) {
return scene.product_unique_id || scene.unique_id || scene.source_product_token || `radar:${scene.id}`;
}
function getErrorMessage(error, fallback) {
const detail = error?.response?.data?.detail;
if (typeof detail === 'string') return detail;
if (detail) return JSON.stringify(detail);
return error?.message || fallback;
}
function buildRadarSearchFormData(criteria, page, regionTreeId) {
const formData = new FormData();
formData.append('limit', String(page.limit));
formData.append('offset', String(page.offset));
formData.append('satellite_family', 'LT1');
formData.append('source_format', 'LT1_ARCHIVE');
Object.entries(criteria || {}).forEach(([key, rawValue]) => {
const value = String(rawValue ?? '').trim();
if (value) formData.append(key, value);
});
if (regionTreeId) {
formData.append('region_tree_id', regionTreeId);
}
return formData;
}
export default function LandsarLt1ProductionPanel({ readOnly, onJobQueued }) {
const [capabilities, setCapabilities] = useState(null);
const [products, setProducts] = useState([]);
const [scenes, setScenes] = useState([]);
const [scenePage, setScenePage] = useState({
limit: 100,
offset: 0,
total: 0,
hasMore: false,
});
const [searchDraft, setSearchDraft] = useState(DEFAULT_SEARCH);
const [searchApplied, setSearchApplied] = useState(DEFAULT_SEARCH);
const [regionMode, setRegionMode] = useState('none');
const [regionSelection, setRegionSelection] = useState({ province: '', city: '' });
const [regionOptions, setRegionOptions] = useState({ provinces: [], cities: [] });
const [selectedRadarIds, setSelectedRadarIds] = useState(() => new Set());
const [preview, setPreview] = useState(null);
const [message, setMessage] = useState('');
const [actionLoading, setActionLoading] = useState(false);
const [searchLoading, setSearchLoading] = useState(false);
const [regionLoading, setRegionLoading] = useState(false);
const [form, setForm] = useState({ mode: 'scene', taskName: '' });
const selectedRegionTreeId = regionSelection.city || regionSelection.province || '';
const producedRadarIds = useMemo(
() => new Set(scenes.filter(sceneProduced).map(scene => Number(scene.id))),
[scenes],
);
const selectedRadarIdList = useMemo(
() => [...selectedRadarIds].filter(id => !producedRadarIds.has(Number(id))),
[selectedRadarIds, producedRadarIds],
);
const selectableCurrentScenes = useMemo(
() => scenes.filter(scene => !sceneProduced(scene) && scene.source_product_ref_id),
[scenes],
);
const allCurrentSelectableSelected = selectableCurrentScenes.length > 0
&& selectableCurrentScenes.every(scene => selectedRadarIds.has(scene.id));
const sceneStart = scenePage.total === 0 ? 0 : scenePage.offset + 1;
const sceneEnd = Math.min(scenePage.offset + scenes.length, scenePage.total || scenePage.offset + scenes.length);
const payload = useMemo(() => ({
radar_data_ids: selectedRadarIdList,
mode: form.mode,
task_name: form.taskName.trim() || undefined,
}), [form, selectedRadarIdList]);
const refreshProducts = useCallback(async () => {
const result = await listLandsarLt1Products({ limit: 20, offset: 0 });
setProducts(Array.isArray(result?.items) ? result.items : []);
}, []);
const refreshScenes = useCallback(async () => {
setSearchLoading(true);
try {
const result = await searchRadarData(
buildRadarSearchFormData(
searchApplied,
scenePage,
regionMode === 'region' ? selectedRegionTreeId : '',
),
);
const items = Array.isArray(result?.items) ? result.items : [];
const total = Number(result?.total ?? items.length);
const offset = Number(result?.offset ?? scenePage.offset);
const limit = Number(result?.limit ?? scenePage.limit);
setScenes(items);
setScenePage(current => ({
...current,
limit,
offset,
total,
hasMore: Boolean(result?.has_more ?? (offset + items.length < total)),
}));
} finally {
setSearchLoading(false);
}
}, [regionMode, scenePage.limit, scenePage.offset, searchApplied, selectedRegionTreeId]);
const refreshCapabilities = useCallback(async () => {
const result = await getLandsarLt1Capabilities();
setCapabilities(result);
}, []);
useEffect(() => {
refreshCapabilities().catch(error => setMessage(getErrorMessage(error, '读取 LT-1 生产能力失败')));
refreshProducts().catch(() => {});
}, [refreshCapabilities, refreshProducts]);
useEffect(() => {
refreshScenes().catch(error => setMessage(getErrorMessage(error, '检索 LT-1 影像失败')));
}, [refreshScenes]);
useEffect(() => {
if (!producedRadarIds.size) return;
setSelectedRadarIds(current => {
let changed = false;
const next = new Set();
current.forEach(id => {
if (producedRadarIds.has(Number(id))) changed = true;
else next.add(id);
});
return changed ? next : current;
});
}, [producedRadarIds]);
const loadProvinces = useCallback(async () => {
setRegionLoading(true);
setMessage('');
try {
const result = await getRegionChildren('1');
setRegionOptions({ provinces: Array.isArray(result?.children) ? result.children : [], cities: [] });
} catch (error) {
setMessage(getErrorMessage(error, '加载行政区失败'));
} finally {
setRegionLoading(false);
}
}, []);
const loadCities = useCallback(async provinceId => {
if (!provinceId) {
setRegionOptions(current => ({ ...current, cities: [] }));
return;
}
setRegionLoading(true);
setMessage('');
try {
const result = await getRegionChildren(provinceId);
setRegionOptions(current => ({ ...current, cities: Array.isArray(result?.children) ? result.children : [] }));
} catch (error) {
setMessage(getErrorMessage(error, '加载地市失败'));
} finally {
setRegionLoading(false);
}
}, []);
const updateField = (field, value) => {
setForm(current => ({ ...current, [field]: value }));
setPreview(null);
setMessage('');
};
const updateSearchDraft = (field, value) => {
setSearchDraft(current => ({ ...current, [field]: value }));
setPreview(null);
setMessage('');
};
const updateRegionMode = async value => {
setRegionMode(value);
setRegionSelection({ province: '', city: '' });
setPreview(null);
setMessage('');
if (value === 'region' && regionOptions.provinces.length === 0) {
await loadProvinces();
}
};
const updateProvince = async value => {
setRegionSelection({ province: value, city: '' });
setPreview(null);
if (value) await loadCities(value);
else setRegionOptions(current => ({ ...current, cities: [] }));
};
const updateCity = value => {
setRegionSelection(current => ({ ...current, city: value }));
setPreview(null);
};
const toggleScene = scene => {
if (sceneProduced(scene) || !scene.source_product_ref_id) return;
setPreview(null);
setMessage('');
setSelectedRadarIds(current => {
const next = new Set(current);
if (next.has(scene.id)) next.delete(scene.id);
else next.add(scene.id);
return next;
});
};
const toggleCurrentPageSelection = () => {
setPreview(null);
setMessage('');
setSelectedRadarIds(current => {
const next = new Set(current);
if (allCurrentSelectableSelected) {
selectableCurrentScenes.forEach(scene => next.delete(scene.id));
} else {
selectableCurrentScenes.forEach(scene => next.add(scene.id));
}
return next;
});
};
const updatePageSize = value => {
const limit = Number(value);
setScenePage(current => ({ ...current, limit, offset: 0 }));
};
const goToScenePage = offset => {
setScenePage(current => ({ ...current, offset: Math.max(0, offset) }));
};
const applySearch = () => {
if (regionMode === 'region' && !selectedRegionTreeId) {
setMessage('请选择行政区。');
return;
}
setSearchApplied(searchDraft);
setScenePage(current => ({ ...current, offset: 0 }));
setPreview(null);
setMessage('');
};
const resetSearch = () => {
setSearchDraft(DEFAULT_SEARCH);
setSearchApplied(DEFAULT_SEARCH);
setRegionMode('none');
setRegionSelection({ province: '', city: '' });
setScenePage(current => ({ ...current, offset: 0 }));
setPreview(null);
setMessage('');
};
const handleRefresh = async () => {
setMessage('');
try {
await Promise.all([refreshProducts(), refreshScenes()]);
} catch (error) {
setMessage(getErrorMessage(error, '刷新失败'));
}
};
const handlePreview = async () => {
setActionLoading(true);
setMessage('');
try {
const result = await previewLandsarLt1Production(payload);
setPreview(result);
setMessage(result.allow_submit ? '预览通过' : '预览未通过');
} catch (error) {
setPreview(null);
setMessage(getErrorMessage(error, '预览失败'));
} finally {
setActionLoading(false);
}
};
const handleSubmit = async () => {
setActionLoading(true);
setMessage('');
try {
const result = await submitLandsarLt1Production(payload);
const queued = Array.isArray(result.queued) ? result.queued : [];
setMessage(`已提交 ${queued.length || 1} 个地理编码 GeoTIFF 生产任务`);
if (result.task_id) onJobQueued?.(result.task_id);
else queued.forEach(item => item.task_id && onJobQueued?.(item.task_id));
setPreview(null);
setSelectedRadarIds(new Set());
await Promise.all([refreshProducts(), refreshScenes()]);
} catch (error) {
setMessage(getErrorMessage(error, '提交失败'));
} finally {
setActionLoading(false);
}
};
const busy = actionLoading || searchLoading || regionLoading;
const canSubmitSelection = !readOnly && selectedRadarIdList.length > 0 && !actionLoading;
return (
LT-1 地理编码影像生产
复用影像检索筛选 LT-1 场景,再提交 Gamma 单景流水线生成 analysis_ready.tif。
{capabilities?.message && {capabilities.message}
}
{message && {message}
}
{preview && (
预览结果
场景数: {preview.scene_count}
engine: {preview.engine}
profile: {preview.profile_code}
{Array.isArray(preview.blockers) && preview.blockers.length > 0 && (
{preview.blockers.map(item => (
{item}
))}
)}
{Array.isArray(preview.warnings) && preview.warnings.length > 0 && (
{preview.warnings.map(item => (
{item}
))}
)}
)}
最近 LT-1 GeoTIFF 产品
{products.length} 项
| 产品 |
状态 |
日期 |
单位 |
时间 |
GeoTIFF |
{products.map(product => (
|
{product.display_name || product.product_id}
|
{product.status} |
{formatYmd(product.summary?.imaging_date)} |
{product.summary?.backscatter_unit || '-'} |
{formatTime(product.published_at)} |
{product.primary_asset_path || product.publish_dir || '-'}
|
))}
{products.length === 0 && (
|
暂无产品
|
)}
);
}