Add result delivery workflow

This commit is contained in:
2026-06-30 15:54:26 +08:00
parent 9c80b95385
commit 04210b0551
18 changed files with 2021 additions and 98 deletions
+122 -2
View File
@@ -7025,7 +7025,7 @@ input[type="checkbox"] {
.result-extraction-controls {
display: grid;
grid-template-columns: minmax(180px, 0.75fr) minmax(260px, 1.4fr) auto;
grid-template-columns: minmax(180px, 0.9fr) minmax(150px, 0.45fr) minmax(160px, auto) auto;
gap: 12px;
padding: 14px 16px 10px;
}
@@ -7042,7 +7042,8 @@ input[type="checkbox"] {
color: var(--color-text-muted);
}
.result-extraction-field input {
.result-extraction-field input,
.result-extraction-field select {
width: 100%;
min-width: 0;
box-sizing: border-box;
@@ -7053,6 +7054,21 @@ input[type="checkbox"] {
color: var(--color-text-primary);
}
.result-extraction-checkbox {
display: inline-flex;
align-items: end;
gap: 8px;
min-height: 55px;
color: var(--color-text-secondary);
font-size: 12px;
font-weight: 800;
}
.result-extraction-checkbox input {
width: 16px;
height: 16px;
}
.result-extraction-action-stack {
display: flex;
align-items: end;
@@ -7119,6 +7135,95 @@ input[type="checkbox"] {
align-content: start;
}
.result-extraction-workspace-split {
flex: 1 1 auto;
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(330px, 0.75fr);
border-top: 1px solid var(--color-border);
}
.result-extraction-result-column {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
.result-delivery-panel {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
border-left: 1px solid var(--color-border);
background: #fbfdff;
}
.result-delivery-list {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
scrollbar-gutter: stable;
}
.result-delivery-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: start;
padding: 11px 16px;
border-bottom: 1px solid var(--color-border);
}
.result-delivery-row-main {
min-width: 0;
display: grid;
gap: 5px;
}
.result-delivery-row-main strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
color: var(--color-text-primary);
}
.result-delivery-row-main span,
.result-delivery-row-main em {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: var(--color-text-muted);
}
.result-delivery-row-main em {
color: #b91c1c;
font-style: normal;
}
.result-delivery-downloads {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.result-delivery-downloads a {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 4px 7px;
border: 1px solid rgba(37, 99, 235, 0.2);
border-radius: 6px;
background: #eff6ff;
color: #1d4ed8;
font-size: 11px;
font-weight: 800;
text-decoration: none;
}
.result-extraction-result-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto auto;
@@ -7269,6 +7374,16 @@ input[type="checkbox"] {
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
}
.result-extraction-workspace-split {
grid-template-columns: 1fr;
}
.result-delivery-panel {
border-left: 0;
border-top: 1px solid var(--color-border);
min-height: 260px;
}
.dinsar-filter-layout {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -7357,6 +7472,11 @@ input[type="checkbox"] {
.result-extraction-action-stack {
align-items: stretch;
}
.result-extraction-checkbox {
align-items: center;
min-height: auto;
}
}
@media (max-width: 640px) {
+233 -95
View File
@@ -1,10 +1,16 @@
import { useEffect, useMemo, useState } from 'react';
import { exportDinsarResults, getDinsarResults } from './api/dinsar';
import { getDinsarResults } from './api/dinsar';
import { listSbasInsarProducts } from './api/sbasInsarProducts';
import {
createResultDelivery,
getResultDeliveryArchiveUrl,
getResultDeliveryDownloadUrl,
getResultDeliveryManifestUrl,
listResultDeliveries,
} from './api/resultDeliveries';
import { getDinsarEngineMeta } from './utils/dinsarEngines';
const DEFAULT_TARGET_DIR = String.raw`D:\Result_Export\DInSAR`;
const PAGE_SIZE = 100;
const PRODUCT_CHANNELS = [
@@ -13,8 +19,8 @@ const PRODUCT_CHANNELS = [
group: 'InSAR 成果',
label: 'D-InSAR 结果',
state: 'ready',
stateText: '可提取',
description: '从已登记的 D-InSAR 成果中选择位移结果,复制到服务器指定交付目录。',
stateText: '可交付',
description: '从已登记的 D-InSAR catalog 中选择成果,后台生成受控交付包并下载到本地。',
},
{
key: 'sbas',
@@ -22,7 +28,7 @@ const PRODUCT_CHANNELS = [
label: 'SBAS-InSAR 结果',
state: 'planned',
stateText: '目录可查',
description: '成果目录和预览已接入,统一提取接口待补齐。',
description: 'SBAS 成果目录已接入,本阶段只展示目录状态,交付打包后续接入。',
},
{
key: 'lt1_ortho',
@@ -30,7 +36,7 @@ const PRODUCT_CHANNELS = [
label: 'LT-1 正射结果',
state: 'placeholder',
stateText: '待接入',
description: '陆探一正射生产结果后续接入标准成果目录,并开放提取。',
description: '陆探一正射生产将由 LandSAR 生产链注册到统一成果目录后开放交付。',
},
{
key: 's1_ortho',
@@ -38,7 +44,7 @@ const PRODUCT_CHANNELS = [
label: 'Sentinel-1 正射结果',
state: 'placeholder',
stateText: '待接入',
description: 'Sentinel-1 正射生产占位,后续登记后统一提取。',
description: 'Sentinel-1 正射生产尚未接入,当前只保留交付通道占位。',
},
{
key: 'gf3_ortho',
@@ -46,7 +52,7 @@ const PRODUCT_CHANNELS = [
label: 'GF3 SARscape _geo',
state: 'placeholder',
stateText: '待接入',
description: 'GF3 外部生产后的 _geo 二进制和 WebP 已按本机登记思路设计,统一导出接口待接入。',
description: 'GF3 外部生产成果登记后再接入统一交付。',
},
];
@@ -56,6 +62,19 @@ function formatNumber(value) {
return new Intl.NumberFormat('zh-CN').format(number);
}
function formatBytes(value) {
const bytes = Number(value);
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let index = 0;
while (size >= 1024 && index < units.length - 1) {
size /= 1024;
index += 1;
}
return `${size.toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
}
function normalizeItems(payload) {
return Array.isArray(payload?.items) ? payload.items : [];
}
@@ -83,33 +102,71 @@ function stateClass(state) {
return 'pending';
}
function statusText(status) {
const value = String(status || '').toUpperCase();
return {
PENDING: '排队中',
RUNNING: '生成中',
READY: '可下载',
FAILED: '失败',
CANCELLED: '已取消',
EXPIRED: '已过期',
}[value] || value || '-';
}
function statusClass(status) {
const value = String(status || '').toUpperCase();
if (value === 'READY') return 'ready';
if (value === 'RUNNING' || value === 'PENDING') return 'planned';
return 'pending';
}
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 [deliveriesPayload, setDeliveriesPayload] = useState({ items: [], total: 0 });
const [loading, setLoading] = useState(true);
const [deliveryLoading, setDeliveryLoading] = useState(false);
const [error, setError] = useState('');
const [query, setQuery] = useState('');
const [targetDir, setTargetDir] = useState(DEFAULT_TARGET_DIR);
const [selectedIds, setSelectedIds] = useState(() => new Set());
const [exporting, setExporting] = useState(false);
const [exportError, setExportError] = useState('');
const [exportResult, setExportResult] = useState(null);
const [packageMode, setPackageMode] = useState('directory');
const [includeChecksums, setIncludeChecksums] = useState(true);
const [creating, setCreating] = useState(false);
const [createError, setCreateError] = useState('');
const [createResult, setCreateResult] = useState(null);
const selectedChannel = PRODUCT_CHANNELS.find(channel => channel.key === activeChannel) || PRODUCT_CHANNELS[0];
const loadDeliveries = async () => {
setDeliveryLoading(true);
try {
const payload = await listResultDeliveries({ mine: true, limit: 20, offset: 0 });
const items = normalizeItems(payload);
setDeliveriesPayload({ ...payload, items, total: extractTotal(payload, items.length) });
} catch (err) {
setCreateError(err?.response?.data?.detail || err.message || '交付包列表加载失败');
} finally {
setDeliveryLoading(false);
}
};
const loadCatalogs = async () => {
setLoading(true);
setError('');
try {
const [dinsarData, sbasData] = await Promise.all([
const [dinsarData, sbasData, deliveryData] = await Promise.all([
getDinsarResults({ limit: PAGE_SIZE, offset: 0 }),
listSbasInsarProducts({ limit: 30, offset: 0 }),
listResultDeliveries({ mine: true, limit: 20, 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 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)));
} catch (err) {
setError(err?.response?.data?.detail || err.message || '结果目录加载失败');
@@ -122,6 +179,18 @@ export default function ResultExtractionPanel({ readOnly = false }) {
loadCatalogs();
}, []);
useEffect(() => {
const hasActiveDelivery = (deliveriesPayload.items || []).some(item => {
const status = String(item.status || '').toUpperCase();
return status === 'PENDING' || status === 'RUNNING';
});
if (!hasActiveDelivery) return undefined;
const timer = window.setInterval(() => {
loadDeliveries();
}, 5000);
return () => window.clearInterval(timer);
}, [deliveriesPayload.items]);
const filteredDinsar = useMemo(() => {
const value = query.trim().toLowerCase();
const items = dinsarPayload.items || [];
@@ -147,21 +216,22 @@ export default function ResultExtractionPanel({ readOnly = false }) {
const selectedCountInView = filteredIds.filter(id => selectedIds.has(id)).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 metrics = [
{
label: 'D-InSAR 可提取',
label: 'D-InSAR 可交付',
value: dinsarPayload.total,
note: `当前载入 ${filteredDinsar.length}/${dinsarPayload.items.length}`,
tone: 'primary',
},
{
label: 'SBAS 目录',
value: sbasPayload.total,
note: '统一提取接口待接入',
label: '我的交付包',
value: deliveriesPayload.total,
note: latestDelivery ? `最近状态:${statusText(latestDelivery.status)}` : '暂无交付记录',
tone: 'neutral',
},
{
@@ -202,38 +272,95 @@ export default function ResultExtractionPanel({ readOnly = false }) {
});
};
const handleExport = async () => {
const dir = targetDir.trim();
if (!dir) {
setExportError('请输入服务器目标目录。');
return;
}
const handleCreateDelivery = async () => {
const ids = [...selectedIds].filter(id => filteredIds.includes(id));
if (ids.length === 0) {
setExportError('请至少选择一条 D-InSAR 结果。');
setCreateError('请至少选择一条 D-InSAR 结果。');
return;
}
setExporting(true);
setExportError('');
setExportResult(null);
setCreating(true);
setCreateError('');
setCreateResult(null);
try {
const response = await exportDinsarResults(ids, dir);
setExportResult(response);
const response = await createResultDelivery({
channel: 'dinsar',
compat_result_ids: ids,
package_mode: packageMode,
include_checksums: includeChecksums,
});
setCreateResult(response);
await loadDeliveries();
} catch (err) {
setExportError(err?.response?.data?.detail || err.message || 'D-InSAR 结果提取失败');
setCreateError(err?.response?.data?.detail || err.message || '成果交付任务创建失败');
} finally {
setExporting(false);
setCreating(false);
}
};
const renderDeliveryDownloads = (delivery) => {
if (String(delivery.status || '').toUpperCase() !== 'READY') {
return null;
}
const items = Array.isArray(delivery.items) ? delivery.items : [];
return (
<div className="result-delivery-downloads">
<a href={getResultDeliveryManifestUrl(delivery.delivery_id)} target="_blank" rel="noreferrer">manifest</a>
{delivery.zip_path && (
<a href={getResultDeliveryArchiveUrl(delivery.delivery_id)} target="_blank" rel="noreferrer">zip</a>
)}
{items.slice(0, 3).map(item => (
<a key={item.id} href={getResultDeliveryDownloadUrl(delivery.delivery_id, item.id)} target="_blank" rel="noreferrer">
{item.relative_path?.split(/[\\/]/).pop() || `文件 ${item.id}`}
</a>
))}
</div>
);
};
const renderDeliveryList = () => (
<section className="result-delivery-panel">
<div className="result-extraction-list-head">
<span>我的交付包</span>
<strong>{deliveryLoading ? '刷新中' : `${deliveriesPayload.items.length}/${deliveriesPayload.total}`}</strong>
</div>
<div className="result-delivery-list">
{deliveriesPayload.items.length === 0 ? (
<div className="result-extraction-empty">还没有创建过成果交付包</div>
) : (
deliveriesPayload.items.map(delivery => (
<div key={delivery.delivery_id} className="result-delivery-row">
<div className="result-delivery-row-main">
<strong>{delivery.delivery_id}</strong>
<span>
{delivery.channel}
{' · '}
{delivery.package_mode}
{' · '}
{formatNumber(delivery.item_count)} 文件
{' · '}
{formatBytes(delivery.copied_bytes || delivery.total_bytes)}
</span>
{delivery.error_message && <em>{delivery.error_message}</em>}
{renderDeliveryDownloads(delivery)}
</div>
<span className={`result-extraction-state ${statusClass(delivery.status)}`}>
{statusText(delivery.status)}
</span>
</div>
))
)}
</div>
</section>
);
const renderDinsarWorkspace = () => (
<section className="result-extraction-main-card">
<div className="result-extraction-card-head">
<div>
<span>D-InSAR 交付提取</span>
<strong>选择已登记结果并复制到服务器目录</strong>
<span>D-InSAR 成果交付</span>
<strong>选择已登记结果并生成下载包</strong>
</div>
<button type="button" onClick={loadCatalogs} disabled={loading || exporting}>
<button type="button" onClick={loadCatalogs} disabled={loading || creating}>
刷新目录
</button>
</div>
@@ -246,83 +373,94 @@ export default function ResultExtractionPanel({ readOnly = false }) {
value={query}
onChange={event => setQuery(event.target.value)}
placeholder="任务名、日期、pair_key、引擎"
disabled={loading || exporting}
disabled={loading || creating}
/>
</label>
<label className="result-extraction-field result-extraction-field-wide">
<span>服务器目标目录</span>
<label className="result-extraction-field">
<span>交付模式</span>
<select value={packageMode} onChange={event => setPackageMode(event.target.value)} disabled={loading || creating}>
<option value="directory">目录交付</option>
<option value="zip">压缩包交付</option>
</select>
</label>
<label className="result-extraction-checkbox">
<input
type="text"
value={targetDir}
onChange={event => setTargetDir(event.target.value)}
placeholder={DEFAULT_TARGET_DIR}
disabled={loading || exporting || readOnly}
type="checkbox"
checked={includeChecksums}
onChange={event => setIncludeChecksums(event.target.checked)}
disabled={loading || creating}
/>
<span>生成 SHA256 校验</span>
</label>
<div className="result-extraction-action-stack">
<button type="button" onClick={toggleVisible} disabled={loading || exporting || filteredIds.length === 0}>
<button type="button" onClick={toggleVisible} disabled={loading || creating || filteredIds.length === 0}>
{allVisibleSelected ? '取消本页' : '选择本页'}
</button>
<button
type="button"
className="primary"
onClick={handleExport}
disabled={readOnly || exporting || loading || selectedCountInView === 0 || !targetDir.trim()}
onClick={handleCreateDelivery}
disabled={creating || loading || selectedCountInView === 0}
>
{exporting ? '正在提' : `提取 ${selectedCountInView}`}
{creating ? '正在提' : `生成 ${selectedCountInView}`}
</button>
</div>
</div>
<div className="result-extraction-hint">
目标目录是服务器可访问路径后端会按任务名或成果名创建子目录避免直接覆盖同名成果
普通用户不再输入服务器路径系统会在受控交付区生成临时包完成后可下载到本地生产结果入库和系统清理由管理员处理
</div>
{error && <div className="result-extraction-message error">{error}</div>}
{exportError && <div className="result-extraction-message error">{exportError}</div>}
{exportResult && (
{createError && <div className="result-extraction-message error">{createError}</div>}
{createResult && (
<div className="result-extraction-message success">
<strong>提取完成</strong>
<span>复制 {formatNumber(exportResult.copied)} 跳过 {formatNumber(exportResult.skipped)} 失败 {formatNumber(exportResult.failed)} </span>
<code>{exportResult.target_dir}</code>
<strong>交付任务已创建</strong>
<span>任务 ID{createResult.task_id || '-'}交付包{createResult.delivery_id}</span>
<code>{createResult.delivery_dir}</code>
</div>
)}
<div className="result-extraction-list-head">
<span>结果列表</span>
<strong>{selectedCountInView}/{filteredDinsar.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>
) : (
filteredDinsar.map(result => {
const id = result.id;
const engineMeta = getDinsarEngineMeta(result.engine_code);
return (
<label key={id} className="result-extraction-result-row">
<input
type="checkbox"
checked={selectedIds.has(id)}
onChange={() => toggleOne(id)}
disabled={exporting}
/>
<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>
</span>
<span className={`dinsar-engine-badge tone-${engineMeta.tone}`}>{engineMeta.shortLabel}</span>
<span className="result-extraction-status-chip">{result.is_cached ? '预览就绪' : '预览待建'}</span>
</label>
);
})
)}
<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>
</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>
) : (
filteredDinsar.map(result => {
const id = result.id;
const engineMeta = getDinsarEngineMeta(result.engine_code);
return (
<label key={id} className="result-extraction-result-row">
<input
type="checkbox"
checked={selectedIds.has(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>
</span>
<span className={`dinsar-engine-badge tone-${engineMeta.tone}`}>{engineMeta.shortLabel}</span>
<span className="result-extraction-status-chip">{result.is_cached ? '预览就绪' : '预览待建'}</span>
</label>
);
})
)}
</div>
</div>
{renderDeliveryList()}
</div>
</section>
);
@@ -338,15 +476,15 @@ export default function ResultExtractionPanel({ readOnly = false }) {
<div className="result-extraction-contract">
<div>
<span>登记入口</span>
<strong>{selectedChannel.key === 'sbas' ? 'SBAS-InSAR 成果目录' : '生产管理成果登记'}</strong>
<strong>{selectedChannel.key === 'sbas' ? 'SBAS-InSAR 成果目录' : '生产结果 catalog'}</strong>
</div>
<div>
<span>提取接口</span>
<strong>待实现</strong>
<span>交付接口</span>
<strong>{selectedChannel.key === 'dinsar' ? '已接入' : '待实现'}</strong>
</div>
<div>
<span>交付目录</span>
<strong>服务器固定/指定路径</strong>
<span>用户权限</span>
<strong>登录用户自助申请</strong>
</div>
</div>
{selectedChannel.key === 'sbas' && (
@@ -375,14 +513,14 @@ export default function ResultExtractionPanel({ readOnly = false }) {
<span>成果交付出口</span>
<strong>结果提取工作台</strong>
<p>
三类正射生产成果D-InSAR 成果和 SBAS-InSAR 成果集中管理当前 D-InSAR 已接入真实提取
其余链路先保留清晰占位避免把未完成流程误当成可执行功能
D-InSAR 成果交付下载和后续正射成果交付统一管理当前 D-InSAR 支持后台交付包
SBASLT-1 正射Sentinel-1 正射和 GF3 正射先保留清晰占位
</p>
</div>
<div className="result-extraction-hero-meta">
<span>D-InSAR {formatNumber(dinsarPayload.total)}</span>
<span>SBAS {formatNumber(sbasPayload.total)}</span>
<span>{readOnly ? '只读账号' : '可执行账号'}</span>
<span>{readOnly ? '自助交付' : '管理员'}</span>
</div>
</section>
+1
View File
@@ -18,3 +18,4 @@ export * as statsApi from './stats';
export * as timeseriesProductionApi from './timeseriesProduction';
export * as psinsarProductsApi from './psinsarProducts';
export * as landsarLt1ProductionApi from './landsarLt1Production';
export * as resultDeliveriesApi from './resultDeliveries';
+22
View File
@@ -0,0 +1,22 @@
import apiClient from './client';
export const getResultDeliveryChannels = () =>
apiClient.get('/result-deliveries/channels').then(r => r.data);
export const createResultDelivery = payload =>
apiClient.post('/result-deliveries', payload).then(r => r.data);
export const listResultDeliveries = (params = {}) =>
apiClient.get('/result-deliveries', { params }).then(r => r.data);
export const getResultDelivery = deliveryId =>
apiClient.get(`/result-deliveries/${encodeURIComponent(deliveryId)}`).then(r => r.data);
export const getResultDeliveryDownloadUrl = (deliveryId, itemId) =>
`/api/result-deliveries/${encodeURIComponent(deliveryId)}/files/${encodeURIComponent(itemId)}/download`;
export const getResultDeliveryManifestUrl = deliveryId =>
`/api/result-deliveries/${encodeURIComponent(deliveryId)}/manifest`;
export const getResultDeliveryArchiveUrl = deliveryId =>
`/api/result-deliveries/${encodeURIComponent(deliveryId)}/archive/download`;
+2
View File
@@ -25,6 +25,7 @@ const TASK_UI_POLICIES = {
ISCE2_RUN: { label: 'D-InSAR 历史任务', featureScope: 'dinsar_production' },
PYINT_RUN: { label: 'PyINT D-InSAR 生产', featureScope: 'dinsar_production' },
LANDSAR_RUN: { label: 'LandSAR D-InSAR 生产', featureScope: 'dinsar_production' },
RESULT_DELIVERY_BUILD: { label: '成果交付包生成', featureScope: 'result_extraction' },
SBAS_GAMMA_WORKFLOW: { label: 'Gamma SBAS 工作流', featureScope: 'sbas_insar' },
SBAS_LANDSAR_WORKFLOW: { label: 'LandSAR SBAS 工作流', featureScope: 'sbas_insar' },
SBAS_COREGISTRATION: { label: 'SBAS 配准', featureScope: 'sbas_insar' },
@@ -42,6 +43,7 @@ const PREFIX_POLICIES = [
{ prefix: 'FLOOD_WATER_EXTRACTION_', label: '洪涝水体提取', featureScope: 'flood' },
{ prefix: 'FLOOD_DETECTION_', label: '洪涝检测', featureScope: 'flood' },
{ prefix: 'GF3_PROCESS_', label: 'GF3 场景处理', featureScope: 'water' },
{ prefix: 'RESULT_DELIVERY_BUILD_', label: '成果交付包生成', featureScope: 'result_extraction' },
];
export function getTaskUiPolicy(taskType) {