Add result delivery workflow
This commit is contained in:
+122
-2
@@ -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) {
|
||||
|
||||
@@ -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 支持后台交付包,
|
||||
SBAS、LT-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>
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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`;
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user