Enable LT1 and GF3 result delivery

This commit is contained in:
2026-06-30 17:12:15 +08:00
parent 04210b0551
commit ab2c9740b8
8 changed files with 702 additions and 113 deletions
+170 -60
View File
@@ -7,6 +7,7 @@ import {
getResultDeliveryArchiveUrl,
getResultDeliveryDownloadUrl,
getResultDeliveryManifestUrl,
listResultDeliveryCatalog,
listResultDeliveries,
} from './api/resultDeliveries';
import { getDinsarEngineMeta } from './utils/dinsarEngines';
@@ -34,9 +35,9 @@ const PRODUCT_CHANNELS = [
key: 'lt1_ortho',
group: '正射成果',
label: 'LT-1 正射结果',
state: 'placeholder',
stateText: '待接入',
description: '陆探一正射生产将由 LandSAR 生产链注册到统一成果目录后开放交付。',
state: 'ready',
stateText: '可交付',
description: '服务器生产的 LT-1 分析就绪正射 GeoTIFF 已接入交付,可打包下载到本地。',
},
{
key: 's1_ortho',
@@ -50,9 +51,9 @@ const PRODUCT_CHANNELS = [
key: 'gf3_ortho',
group: '正射成果',
label: 'GF3 SARscape _geo',
state: 'placeholder',
stateText: '待接入',
description: 'GF3 外部生产成果登记后再接入统一交付。',
state: 'ready',
stateText: '可交付',
description: '已登记的 GF3 SARscape 标准化正射成品可直接创建交付。',
},
];
@@ -85,17 +86,89 @@ function extractTotal(payload, fallback = 0) {
}
function resultDisplayName(result) {
return String(result?.name || result?.task_alias || result?.task_name || result?.product_id || `#${result?.id || ''}`).trim();
return String(result?.display_name || result?.name || result?.task_alias || result?.task_name || result?.product_id || `#${result?.id || ''}`).trim();
}
function resultItemId(result) {
return result?.item_id ?? result?.id;
}
function selectionKey(channelKey, id) {
return `${channelKey}:${id}`;
}
function resultDateText(result) {
const name = resultDisplayName(result);
if (result?.imaging_date) return String(result.imaging_date);
const matches = name.match(/(\d{8})/g);
if (matches?.length >= 2) return `${matches[0]} / ${matches[1]}`;
if (matches?.length === 1) return matches[0];
return '-';
}
function channelMetricLabel(channelKey) {
return {
dinsar: 'D-InSAR 可交付',
sbas: 'SBAS 目录',
lt1_ortho: 'LT-1 正射',
gf3_ortho: 'GF3 正射',
s1_ortho: 'Sentinel-1 正射',
}[channelKey] || '可交付结果';
}
function channelListTitle(channelKey) {
return {
dinsar: 'D-InSAR 结果列表',
lt1_ortho: 'LT-1 正射结果列表',
gf3_ortho: 'GF3 正射结果列表',
}[channelKey] || '结果列表';
}
function channelEmptyText(channelKey) {
return {
dinsar: '当前条件下没有可交付的 D-InSAR 结果。',
lt1_ortho: '当前条件下没有可交付的 LT-1 正射结果。',
gf3_ortho: '当前条件下没有可交付的 GF3 正射结果。',
}[channelKey] || '当前条件下没有可交付结果。';
}
function channelCreateTitle(channelKey) {
return {
dinsar: 'D-InSAR 成果交付',
lt1_ortho: 'LT-1 正射成果交付',
gf3_ortho: 'GF3 正射成果交付',
}[channelKey] || '成果交付';
}
function channelCreateSubtitle(channelKey) {
return {
dinsar: '选择已登记结果并生成下载包',
lt1_ortho: '选择服务器已生产正射 GeoTIFF 并生成下载包',
gf3_ortho: '选择已登记 GF3 SARscape 正射成品并生成下载包',
}[channelKey] || '选择结果并生成下载包';
}
function itemMetaText(item, channelKey) {
if (channelKey === 'dinsar') {
return `${resultDateText(item)} · ${item.pair_key || item.product_id || '-'}`;
}
const parts = [
item.imaging_date || resultDateText(item),
item.polarization,
item.pixel_size_m ? `${item.pixel_size_m} m` : null,
item.product_id,
].filter(Boolean);
return parts.join(' · ') || '-';
}
function itemStatusText(item, channelKey) {
if (channelKey === 'dinsar') {
return item.is_cached ? '预览就绪' : '预览待建';
}
if (item.file_size) return formatBytes(item.file_size);
return item.health_status || item.status || 'READY';
}
function stateClass(state) {
if (state === 'ready') return 'ready';
if (state === 'planned') return 'planned';
@@ -125,6 +198,8 @@ export default function ResultExtractionPanel({ readOnly = false }) {
const [activeChannel, setActiveChannel] = useState('dinsar');
const [dinsarPayload, setDinsarPayload] = useState({ items: [], total: 0 });
const [sbasPayload, setSbasPayload] = useState({ items: [], total: 0 });
const [lt1Payload, setLt1Payload] = useState({ items: [], total: 0 });
const [gf3Payload, setGf3Payload] = useState({ items: [], total: 0 });
const [deliveriesPayload, setDeliveriesPayload] = useState({ items: [], total: 0 });
const [loading, setLoading] = useState(true);
const [deliveryLoading, setDeliveryLoading] = useState(false);
@@ -161,13 +236,21 @@ export default function ResultExtractionPanel({ readOnly = false }) {
listSbasInsarProducts({ limit: 30, offset: 0 }),
listResultDeliveries({ mine: true, limit: 20, offset: 0 }),
]);
const [lt1Data, gf3Data] = await Promise.all([
listResultDeliveryCatalog('lt1_ortho', { limit: PAGE_SIZE, offset: 0 }),
listResultDeliveryCatalog('gf3_ortho', { limit: PAGE_SIZE, offset: 0 }),
]);
const dinsarItems = normalizeItems(dinsarData);
setDinsarPayload({ ...dinsarData, items: dinsarItems, total: extractTotal(dinsarData, dinsarItems.length) });
const sbasItems = normalizeItems(sbasData);
setSbasPayload({ ...sbasData, items: sbasItems, total: extractTotal(sbasData, sbasItems.length) });
const lt1Items = normalizeItems(lt1Data);
setLt1Payload({ ...lt1Data, items: lt1Items, total: extractTotal(lt1Data, lt1Items.length) });
const gf3Items = normalizeItems(gf3Data);
setGf3Payload({ ...gf3Data, items: gf3Items, total: extractTotal(gf3Data, gf3Items.length) });
const deliveryItems = normalizeItems(deliveryData);
setDeliveriesPayload({ ...deliveryData, items: deliveryItems, total: extractTotal(deliveryData, deliveryItems.length) });
setSelectedIds(new Set(dinsarItems.map(item => item.id).filter(id => id !== undefined && id !== null)));
setSelectedIds(new Set(dinsarItems.map(item => resultItemId(item)).filter(id => id !== undefined && id !== null).map(id => selectionKey('dinsar', id))));
} catch (err) {
setError(err?.response?.data?.detail || err.message || '结果目录加载失败');
} finally {
@@ -191,41 +274,63 @@ export default function ResultExtractionPanel({ readOnly = false }) {
return () => window.clearInterval(timer);
}, [deliveriesPayload.items]);
const filteredDinsar = useMemo(() => {
const activePayload = activeChannel === 'lt1_ortho'
? lt1Payload
: activeChannel === 'gf3_ortho'
? gf3Payload
: activeChannel === 'sbas'
? sbasPayload
: activeChannel === 's1_ortho'
? { items: [], total: 0 }
: dinsarPayload;
const activeItems = activePayload.items || [];
const filteredResults = useMemo(() => {
const value = query.trim().toLowerCase();
const items = dinsarPayload.items || [];
const items = activeItems;
if (!value) return items;
return items.filter(item => {
const haystack = [
item.display_name,
item.name,
item.task_name,
item.task_alias,
item.pair_key,
item.product_id,
item.engine_code,
item.profile_code,
item.imaging_date,
item.polarization,
item.file_path,
item.primary_asset_path,
item.publish_dir,
].filter(Boolean).join(' ').toLowerCase();
return haystack.includes(value);
});
}, [dinsarPayload.items, query]);
}, [activeItems, query]);
const filteredIds = useMemo(
() => filteredDinsar.map(item => item.id).filter(id => id !== undefined && id !== null),
[filteredDinsar],
() => filteredResults.map(item => resultItemId(item)).filter(id => id !== undefined && id !== null),
[filteredResults],
);
const selectedCountInView = filteredIds.filter(id => selectedIds.has(id)).length;
const filteredKeys = useMemo(
() => filteredIds.map(id => selectionKey(activeChannel, id)),
[activeChannel, filteredIds],
);
const selectedCountInView = filteredKeys.filter(key => selectedIds.has(key)).length;
const allVisibleSelected = filteredIds.length > 0 && selectedCountInView === filteredIds.length;
const latestDelivery = deliveriesPayload.items?.[0] || null;
const orthoPlaceholderCount = PRODUCT_CHANNELS.filter(channel => channel.group === '正射成果').length;
const currentCatalogTotal = Number(dinsarPayload.total || 0) + Number(sbasPayload.total || 0);
const readyOrthoCount = Number(lt1Payload.total || 0) + Number(gf3Payload.total || 0);
const currentCatalogTotal = Number(dinsarPayload.total || 0) + Number(sbasPayload.total || 0) + readyOrthoCount;
const metrics = [
{
label: 'D-InSAR 可交付',
value: dinsarPayload.total,
note: `当前载入 ${filteredDinsar.length}/${dinsarPayload.items.length}`,
label: channelMetricLabel(activeChannel),
value: activePayload.total,
note: `当前载入 ${filteredResults.length}/${activeItems.length}`,
tone: 'primary',
},
{
@@ -237,24 +342,25 @@ export default function ResultExtractionPanel({ readOnly = false }) {
{
label: '当前接入目录',
value: currentCatalogTotal,
note: 'D-InSAR + SBAS 已接入清单',
note: 'D-InSAR + SBAS + LT-1/GF3 正射',
tone: 'neutral',
},
{
label: '正射通道',
value: orthoPlaceholderCount,
note: 'LT-1 / S1 / GF3 占位',
tone: 'warning',
label: '可交付正射',
value: readyOrthoCount,
note: 'LT-1 与 GF3 已接入,S1 预留',
tone: 'neutral',
},
];
const toggleOne = (id) => {
const key = selectionKey(activeChannel, id);
setSelectedIds(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
if (next.has(key)) {
next.delete(key);
} else {
next.add(id);
next.add(key);
}
return next;
});
@@ -264,30 +370,35 @@ export default function ResultExtractionPanel({ readOnly = false }) {
setSelectedIds(prev => {
const next = new Set(prev);
if (allVisibleSelected) {
filteredIds.forEach(id => next.delete(id));
filteredKeys.forEach(key => next.delete(key));
} else {
filteredIds.forEach(id => next.add(id));
filteredKeys.forEach(key => next.add(key));
}
return next;
});
};
const handleCreateDelivery = async () => {
const ids = [...selectedIds].filter(id => filteredIds.includes(id));
const ids = filteredIds.filter(id => selectedIds.has(selectionKey(activeChannel, id)));
if (ids.length === 0) {
setCreateError('请至少选择一条 D-InSAR 结果。');
setCreateError(`请至少选择一条${selectedChannel.label}`);
return;
}
setCreating(true);
setCreateError('');
setCreateResult(null);
try {
const response = await createResultDelivery({
channel: 'dinsar',
compat_result_ids: ids,
const payload = {
channel: activeChannel,
package_mode: packageMode,
include_checksums: includeChecksums,
});
};
if (activeChannel === 'dinsar') {
payload.compat_result_ids = ids;
} else {
payload.item_ids = ids;
}
const response = await createResultDelivery(payload);
setCreateResult(response);
await loadDeliveries();
} catch (err) {
@@ -353,12 +464,12 @@ export default function ResultExtractionPanel({ readOnly = false }) {
</section>
);
const renderDinsarWorkspace = () => (
const renderReadyWorkspace = () => (
<section className="result-extraction-main-card">
<div className="result-extraction-card-head">
<div>
<span>D-InSAR 成果交付</span>
<strong>选择已登记结果并生成下载包</strong>
<span>{channelCreateTitle(activeChannel)}</span>
<strong>{channelCreateSubtitle(activeChannel)}</strong>
</div>
<button type="button" onClick={loadCatalogs} disabled={loading || creating}>
刷新目录
@@ -372,7 +483,7 @@ export default function ResultExtractionPanel({ readOnly = false }) {
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
placeholder="任务名、日期、pair_key、引擎"
placeholder="任务名、日期、产品号、极化、引擎"
disabled={loading || creating}
/>
</label>
@@ -424,36 +535,34 @@ export default function ResultExtractionPanel({ readOnly = false }) {
<div className="result-extraction-workspace-split">
<div className="result-extraction-result-column">
<div className="result-extraction-list-head">
<span>结果列表</span>
<strong>{selectedCountInView}/{filteredDinsar.length}</strong>
<span>{channelListTitle(activeChannel)}</span>
<strong>{selectedCountInView}/{filteredResults.length}</strong>
</div>
<div className="result-extraction-result-list">
{loading ? (
<div className="result-extraction-empty">正在加载成果目录...</div>
) : filteredDinsar.length === 0 ? (
<div className="result-extraction-empty">当前条件下没有可交付的 D-InSAR 结果</div>
) : filteredResults.length === 0 ? (
<div className="result-extraction-empty">{channelEmptyText(activeChannel)}</div>
) : (
filteredDinsar.map(result => {
const id = result.id;
const engineMeta = getDinsarEngineMeta(result.engine_code);
filteredResults.map(result => {
const id = resultItemId(result);
const engineMeta = activeChannel === 'dinsar'
? getDinsarEngineMeta(result.engine_code)
: { tone: 'landsar', shortLabel: result.engine_code || result.profile_code || 'ORTHO' };
return (
<label key={id} className="result-extraction-result-row">
<label key={`${activeChannel}-${id}`} className="result-extraction-result-row">
<input
type="checkbox"
checked={selectedIds.has(id)}
checked={selectedIds.has(selectionKey(activeChannel, id))}
onChange={() => toggleOne(id)}
disabled={creating}
/>
<span className="result-extraction-result-main">
<strong title={result.file_path || resultDisplayName(result)}>{resultDisplayName(result)}</strong>
<span>
{resultDateText(result)}
{' · '}
{result.pair_key || result.product_id || '-'}
</span>
<strong title={result.file_path || result.primary_asset_path || resultDisplayName(result)}>{resultDisplayName(result)}</strong>
<span>{itemMetaText(result, activeChannel)}</span>
</span>
<span className={`dinsar-engine-badge tone-${engineMeta.tone}`}>{engineMeta.shortLabel}</span>
<span className="result-extraction-status-chip">{result.is_cached ? '预览就绪' : '预览待建'}</span>
<span className="result-extraction-status-chip">{itemStatusText(result, activeChannel)}</span>
</label>
);
})
@@ -480,7 +589,7 @@ export default function ResultExtractionPanel({ readOnly = false }) {
</div>
<div>
<span>交付接口</span>
<strong>{selectedChannel.key === 'dinsar' ? '已接入' : '待实现'}</strong>
<strong>{selectedChannel.state === 'ready' ? '已接入' : '待实现'}</strong>
</div>
<div>
<span>用户权限</span>
@@ -513,13 +622,14 @@ export default function ResultExtractionPanel({ readOnly = false }) {
<span>成果交付出口</span>
<strong>结果提取工作台</strong>
<p>
D-InSAR 成果交付下载和后续正射成果交付统一管理当前 D-InSAR 支持后台交付包
SBASLT-1 正射Sentinel-1 正射和 GF3 正射先保留清晰占位
D-InSARLT-1 正射和 GF3 正射成果交付下载统一管理后台生成受控交付包用户完成后下载到本地
SBAS Sentinel-1 正射保留清晰接入状态
</p>
</div>
<div className="result-extraction-hero-meta">
<span>D-InSAR {formatNumber(dinsarPayload.total)}</span>
<span>SBAS {formatNumber(sbasPayload.total)}</span>
<span>LT-1 {formatNumber(lt1Payload.total)}</span>
<span>GF3 {formatNumber(gf3Payload.total)}</span>
<span>{readOnly ? '自助交付' : '管理员'}</span>
</div>
</section>
@@ -549,7 +659,7 @@ export default function ResultExtractionPanel({ readOnly = false }) {
</button>
))}
</aside>
{activeChannel === 'dinsar' ? renderDinsarWorkspace() : renderPlaceholderWorkspace()}
{selectedChannel.state === 'ready' ? renderReadyWorkspace() : renderPlaceholderWorkspace()}
</section>
</div>
);
+3
View File
@@ -9,6 +9,9 @@ export const createResultDelivery = payload =>
export const listResultDeliveries = (params = {}) =>
apiClient.get('/result-deliveries', { params }).then(r => r.data);
export const listResultDeliveryCatalog = (channel, params = {}) =>
apiClient.get(`/result-deliveries/catalog/${encodeURIComponent(channel)}`, { params }).then(r => r.data);
export const getResultDelivery = deliveryId =>
apiClient.get(`/result-deliveries/${encodeURIComponent(deliveryId)}`).then(r => r.data);