Add LandSAR cluster worker deployment
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { Suspense, lazy, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
PRODUCTION_WORKSPACE_ENTRY_TO_VIEW,
|
||||
PRODUCTION_WORKSPACE_TAB,
|
||||
@@ -17,133 +16,151 @@ const LazyPairsListPanel = lazy(() => import('./panels/PairsListPanel'));
|
||||
const LazyBatchPanel = lazy(() => import('./panels/BatchPanel'));
|
||||
const LazyDataCopierPanel = lazy(() => import('./DataCopierPanel'));
|
||||
|
||||
const shellStyle = {
|
||||
minHeight: '100%',
|
||||
padding: '20px 24px 28px',
|
||||
boxSizing: 'border-box',
|
||||
background: 'linear-gradient(180deg, #f5f7fb 0%, #eef4ff 52%, #f8fafc 100%)',
|
||||
};
|
||||
|
||||
const heroStyle = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
|
||||
gap: 16,
|
||||
marginBottom: 18,
|
||||
};
|
||||
|
||||
const heroCardStyle = {
|
||||
borderRadius: 24,
|
||||
border: '1px solid #d7e0eb',
|
||||
background: 'linear-gradient(135deg, #ffffff 0%, #f8fbff 56%, #eef6ff 100%)',
|
||||
boxShadow: '0 16px 40px rgba(15, 23, 42, 0.06)',
|
||||
};
|
||||
|
||||
const summaryCardStyle = {
|
||||
padding: '12px 14px',
|
||||
borderRadius: 18,
|
||||
border: '1px solid #e2e8f0',
|
||||
background: 'rgba(255, 255, 255, 0.82)',
|
||||
};
|
||||
const WORKFLOW_STEPS = [
|
||||
'数据准备',
|
||||
'配对/栈规划',
|
||||
'生产运行',
|
||||
'质量检查',
|
||||
'成果发布',
|
||||
];
|
||||
|
||||
const SENSOR_PRODUCTION_PLACEHOLDERS = {
|
||||
lt1_production: {
|
||||
title: '陆探一号生产模块',
|
||||
subtitle: '当前先占位纳入生产管理,执行链路保留 LandSAR、ENVI+SARscape、Gamma/PyINT。',
|
||||
title: '陆探一生产占位',
|
||||
note: '当前保留 LT-1 源压缩包本机登记与按需 materialize 入口。',
|
||||
rows: [
|
||||
['源压缩包', 'D:\\LuTan1_Image_Pool_Zip,只索引包内 XML/元数据,不做全量解包。'],
|
||||
['精密轨道', 'D:\\LT1_data_lsarorbit,本机部署并绑定到源资产。'],
|
||||
['按需解包', '生产任务需要时才 materialize 到 D:\\Task_Pool\\DInSAR 或 D:\\Task_Pool\\SBAS。'],
|
||||
['生产边界', 'D-InSAR 与 SBAS-InSAR 均使用本机 Task_Pool,不允许 UNC 参与运行。'],
|
||||
['结果管理', '生成结果进入 D-InSAR/SBAS 产物目录,由生产管理结果页统一重建 catalog。'],
|
||||
['数据来源', '本机源压缩包 archive'],
|
||||
['精轨策略', '按生产任务关联 orbit 资产'],
|
||||
['准备方式', '按需 materialize 到 Task_Pool'],
|
||||
['生产边界', 'D-InSAR/SBAS 不走 UNC'],
|
||||
['结果管理', '进入统一产品 catalog'],
|
||||
],
|
||||
},
|
||||
sentinel1_production: {
|
||||
title: 'Sentinel-1 生产模块',
|
||||
subtitle: '当前先占位纳入生产管理,D-InSAR 保留 Gamma/PyINT 路径,SBAS 仍为规划态。',
|
||||
title: 'Sentinel-1 生产占位',
|
||||
note: '当前主要沉淀数据与精轨管理约束,SBAS 仅保留规划能力。',
|
||||
rows: [
|
||||
['源压缩包', 'D:\\Sentinel1_Image_Pool_ZIP,本机登记 ZIP/SAFE 元数据。'],
|
||||
['精密轨道', 'D:\\Sentinel1_EOF_Pool,本机保存 AUX_POEORB/RESORB。'],
|
||||
['按需解包', '需要运行时才将 ZIP 解包到本机 Task_Pool,界面不提供全量解包按钮。'],
|
||||
['D-InSAR', 'Gamma/PyINT 可作为生产方向,运行材料必须来自本机路径。'],
|
||||
['SBAS', '当前仅做堆栈发现和规划,执行链路未启用。'],
|
||||
['数据来源', 'ZIP/SAFE 本机 archive'],
|
||||
['精轨策略', 'EOF 精轨本机管理'],
|
||||
['准备方式', '按需解包到工作目录'],
|
||||
['D-InSAR', '走统一生产任务队列'],
|
||||
['SBAS', '保留序列规划能力'],
|
||||
],
|
||||
},
|
||||
gf3_native_registration: {
|
||||
title: '高分三结果登记',
|
||||
subtitle: 'GF3 不在本机生产;另一台 SARscape 服务器完成 _geo 后复制到本机登记。',
|
||||
note: 'GF3 由外部 SARscape 服务生产,本系统登记回传成果并生成预览。',
|
||||
rows: [
|
||||
['外部生产', '外部机器按 YYYYMMDD_geo/场景目录输出 SARscape 原生 _geo 二进制。'],
|
||||
['本机落盘', '复制到 D:\\GaoFen3_Pool\\native_geo 后递归扫描登记。'],
|
||||
['预览生成', 'WebP 从 *_geo 主二进制读取生成,不使用 *_geo_ql.tif 作为正式预览源。'],
|
||||
['精轨', 'GF3 本链路无精密轨道管理。'],
|
||||
['结果管理', '登记后的 GF3 资产进入数据管理,后续需要全影像时再提取/标准化。'],
|
||||
['生产方式', '外部 SARscape 服务'],
|
||||
['落地路径', '本机登记 _geo 二进制'],
|
||||
['预览生成', '转换 WebP 供地图使用'],
|
||||
['精轨策略', '按外部生产结果留痕'],
|
||||
['结果管理', '进入统一产品 catalog'],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function SensorProductionPlaceholder({ viewKey }) {
|
||||
const data = SENSOR_PRODUCTION_PLACEHOLDERS[viewKey];
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<section style={{ ...heroCardStyle, padding: '18px 20px' }}>
|
||||
<div style={{ fontSize: 12, color: '#475569', marginBottom: 8 }}>当前设计约定</div>
|
||||
<h3 style={{ margin: '0 0 8px', fontSize: 20, color: '#0f172a' }}>{data.title}</h3>
|
||||
<p style={{ margin: '0 0 16px', color: '#475569', fontSize: 13, lineHeight: 1.7 }}>
|
||||
{data.subtitle}
|
||||
</p>
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
{data.rows.map(([label, value]) => (
|
||||
<div
|
||||
key={label}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '120px 1fr',
|
||||
gap: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #e2e8f0',
|
||||
background: '#fff',
|
||||
}}
|
||||
>
|
||||
<strong style={{ color: '#0f172a', fontSize: 13 }}>{label}</strong>
|
||||
<span style={{ color: '#475569', fontSize: 13, lineHeight: 1.7, wordBreak: 'break-word' }}>{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
const shellStyle = {
|
||||
minHeight: '100%',
|
||||
background: '#f8fafc',
|
||||
color: '#0f172a',
|
||||
};
|
||||
|
||||
function resolveView(entry) {
|
||||
return PRODUCTION_WORKSPACE_ENTRY_TO_VIEW[entry] || PRODUCTION_WORKSPACE_ENTRY_TO_VIEW[PRODUCTION_WORKSPACE_TAB];
|
||||
const headerStyle = {
|
||||
padding: '18px 20px 14px',
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
background: '#ffffff',
|
||||
};
|
||||
|
||||
const sectionStyle = {
|
||||
padding: '16px 20px 22px',
|
||||
};
|
||||
|
||||
const compactPanelStyle = {
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: 8,
|
||||
background: '#ffffff',
|
||||
};
|
||||
|
||||
const mutedTextStyle = {
|
||||
color: '#64748b',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
};
|
||||
|
||||
function resolveView(activeEntry) {
|
||||
return PRODUCTION_WORKSPACE_ENTRY_TO_VIEW[activeEntry] || PRODUCTION_WORKSPACE_ENTRY_TO_VIEW[PRODUCTION_WORKSPACE_TAB];
|
||||
}
|
||||
|
||||
function resolveWorkbenchKey(viewKey) {
|
||||
const workbench = PRODUCTION_WORKSPACE_WORKBENCHES.find(item => (
|
||||
item.views.some(view => view.key === viewKey)
|
||||
));
|
||||
return workbench?.key || PRODUCTION_WORKSPACE_WORKBENCHES[0]?.key || 'dinsar_workbench';
|
||||
const workbench = PRODUCTION_WORKSPACE_WORKBENCHES.find(item =>
|
||||
item.views.some(view => view.key === viewKey),
|
||||
);
|
||||
return workbench?.key || PRODUCTION_WORKSPACE_WORKBENCHES[0]?.key;
|
||||
}
|
||||
|
||||
function buttonStyle(active) {
|
||||
return {
|
||||
border: `1px solid ${active ? '#2563eb' : '#cbd5e1'}`,
|
||||
background: active ? '#eff6ff' : '#ffffff',
|
||||
color: active ? '#1d4ed8' : '#334155',
|
||||
borderRadius: 6,
|
||||
padding: '7px 10px',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
lineHeight: 1.3,
|
||||
};
|
||||
}
|
||||
|
||||
function PlaceholderView({ config }) {
|
||||
if (!config) {
|
||||
return (
|
||||
<div style={{ ...compactPanelStyle, padding: 18 }}>
|
||||
<h3 style={{ margin: 0, fontSize: 16 }}>生产入口未配置</h3>
|
||||
<p style={{ ...mutedTextStyle, margin: '8px 0 0' }}>当前视图尚未接入生产面板。</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ ...compactPanelStyle, padding: 18 }}>
|
||||
<h3 style={{ margin: 0, fontSize: 16 }}>{config.title}</h3>
|
||||
<p style={{ ...mutedTextStyle, margin: '8px 0 14px' }}>{config.note}</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(96px, 140px) 1fr', gap: 0, borderTop: '1px solid #e2e8f0' }}>
|
||||
{config.rows.map(([label, value]) => (
|
||||
<div key={label} style={{ display: 'contents' }}>
|
||||
<div style={{ padding: '10px 12px', borderBottom: '1px solid #e2e8f0', color: '#475569', background: '#f8fafc', fontSize: 13 }}>
|
||||
{label}
|
||||
</div>
|
||||
<div style={{ padding: '10px 12px', borderBottom: '1px solid #e2e8f0', color: '#0f172a', fontSize: 13 }}>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProductionWorkspace({
|
||||
activeEntry = PRODUCTION_WORKSPACE_TAB,
|
||||
readOnly = false,
|
||||
activeEntry,
|
||||
readOnly,
|
||||
onTaskStart,
|
||||
apiEndpoint,
|
||||
language,
|
||||
foundPairs = [],
|
||||
selectedPairsCount = 0,
|
||||
isLoading = false,
|
||||
hasEnoughRadarScenesForPlanning = false,
|
||||
hasRadarSearched = false,
|
||||
pairingPanel = {},
|
||||
radarPanel = {},
|
||||
pairsPanel = {},
|
||||
foundPairs,
|
||||
selectedPairsCount,
|
||||
isLoading,
|
||||
hasEnoughRadarScenesForPlanning,
|
||||
hasRadarSearched,
|
||||
pairingPanel,
|
||||
radarPanel,
|
||||
pairsPanel,
|
||||
}) {
|
||||
const [activeView, setActiveView] = useState(() => resolveView(activeEntry));
|
||||
const [activeWorkbench, setActiveWorkbench] = useState(() => resolveWorkbenchKey(resolveView(activeEntry)));
|
||||
const initialView = resolveView(activeEntry);
|
||||
const [activeView, setActiveView] = useState(initialView);
|
||||
const [activeWorkbench, setActiveWorkbench] = useState(resolveWorkbenchKey(initialView));
|
||||
|
||||
useEffect(() => {
|
||||
const nextView = resolveView(activeEntry);
|
||||
@@ -151,17 +168,16 @@ export default function ProductionWorkspace({
|
||||
setActiveWorkbench(resolveWorkbenchKey(nextView));
|
||||
}, [activeEntry]);
|
||||
|
||||
const activeViewMeta = useMemo(
|
||||
() => PRODUCTION_WORKSPACE_VIEWS.find(view => view.key === activeView) || PRODUCTION_WORKSPACE_VIEWS[0],
|
||||
[activeView]
|
||||
);
|
||||
const activeWorkbenchMeta = useMemo(
|
||||
const currentWorkbench = useMemo(
|
||||
() => PRODUCTION_WORKSPACE_WORKBENCHES.find(item => item.key === activeWorkbench) || PRODUCTION_WORKSPACE_WORKBENCHES[0],
|
||||
[activeWorkbench]
|
||||
[activeWorkbench],
|
||||
);
|
||||
const currentView = useMemo(
|
||||
() => PRODUCTION_WORKSPACE_VIEWS.find(view => view.key === activeView),
|
||||
[activeView],
|
||||
);
|
||||
const activeSubViews = activeWorkbenchMeta?.views || [];
|
||||
|
||||
const switchWorkbench = (workbench) => {
|
||||
const switchWorkbench = workbench => {
|
||||
setActiveWorkbench(workbench.key);
|
||||
if (!workbench.views.some(view => view.key === activeView)) {
|
||||
setActiveView(workbench.defaultView);
|
||||
@@ -177,224 +193,176 @@ export default function ProductionWorkspace({
|
||||
};
|
||||
|
||||
const handleDinsarPrepareQueued = taskId => {
|
||||
onTaskStart?.(taskId, 'D-InSAR生产准备任务已入队,正在处理...', {
|
||||
onTaskStart?.(taskId, 'D-InSAR 生产准备任务已入队,正在处理...', {
|
||||
taskType: 'COPY_DATA',
|
||||
nonBlocking: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSbasProductQueued = taskId => {
|
||||
onTaskStart?.(taskId, 'SBAS-InSAR result catalog task queued.', {
|
||||
onTaskStart?.(taskId, 'SBAS-InSAR 结果 catalog 任务已入队。', {
|
||||
taskType: 'REBUILD_SBAS_INSAR_CATALOG',
|
||||
nonBlocking: true,
|
||||
});
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (activeView === 'dinsar_pairing') {
|
||||
return (
|
||||
<LazyPairPlanningPanel
|
||||
foundPairs={foundPairs}
|
||||
selectedPairsCount={selectedPairsCount}
|
||||
isLoading={isLoading}
|
||||
isReadOnlyUser={readOnly}
|
||||
hasEnoughRadarScenesForPlanning={hasEnoughRadarScenesForPlanning}
|
||||
onOpenPairingModal={pairingPanel?.openModal}
|
||||
hasRadarSearched={hasRadarSearched}
|
||||
onRefreshRadarSearch={radarPanel?.refresh}
|
||||
onSearchAll={radarPanel?.searchAll}
|
||||
onRefreshDinsar={pairsPanel?.refreshDinsar}
|
||||
language={language}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeView === 'dinsar_pairs') {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.1fr) minmax(360px, 0.9fr)', gap: 14 }}>
|
||||
<LazyPairsListPanel pairsPanel={pairsPanel} isReadOnlyUser={readOnly} language={language} />
|
||||
<LazyBatchPanel pairsPanel={pairsPanel} isReadOnlyUser={readOnly} language={language} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeView === 'dinsar_prepare') {
|
||||
return (
|
||||
<LazyDataCopierPanel
|
||||
apiEndpoint={apiEndpoint}
|
||||
readOnly={readOnly}
|
||||
onJobQueued={handleDinsarPrepareQueued}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeView === 'dinsar_runs') {
|
||||
return <LazyDinsarProductionPanel readOnly={readOnly} onJobQueued={handleDinsarRunQueued} />;
|
||||
}
|
||||
|
||||
if (activeView === 'dinsar_products') {
|
||||
return <LazyDinsarProductsPanel readOnly={readOnly} onJobQueued={handleDinsarProductQueued} />;
|
||||
}
|
||||
|
||||
if (['sbas_insar_planning', 'sbas_insar_batches', 'sbas_insar_prepare', 'sbas_insar_runs'].includes(activeView)) {
|
||||
const focusMap = {
|
||||
sbas_insar_planning: 'planning',
|
||||
sbas_insar_batches: 'batches',
|
||||
sbas_insar_prepare: 'prepare',
|
||||
sbas_insar_runs: 'runs',
|
||||
};
|
||||
return (
|
||||
<LazySbasInsarProductionPanel
|
||||
readOnly={readOnly}
|
||||
onTaskStart={onTaskStart}
|
||||
initialFocus={focusMap[activeView]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeView === 'sbas_insar_products') {
|
||||
return <LazySbasInsarProductsPanel readOnly={readOnly} onJobQueued={handleSbasProductQueued} />;
|
||||
}
|
||||
|
||||
return <PlaceholderView config={SENSOR_PRODUCTION_PLACEHOLDERS[activeView]} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={shellStyle}>
|
||||
<div style={heroStyle}>
|
||||
<section style={{ ...heroCardStyle, padding: '22px 24px' }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.08em', color: '#1d4ed8', textTransform: 'uppercase' }}>
|
||||
Production Management
|
||||
<div style={headerStyle}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 16, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: '#475569', marginBottom: 4 }}>生产工作台</div>
|
||||
<h2 style={{ margin: 0, fontSize: 22, lineHeight: 1.25, letterSpacing: 0 }}>InSAR 生产管理</h2>
|
||||
<p style={{ ...mutedTextStyle, margin: '8px 0 0', maxWidth: 720 }}>
|
||||
面向科研工程生产的任务编排入口,统一组织数据准备、规划、运行、质量检查与成果发布。
|
||||
</p>
|
||||
</div>
|
||||
<h2 style={{ margin: '10px 0 12px', fontSize: 32, lineHeight: 1.1, color: '#0f172a' }}>生产管理</h2>
|
||||
<p style={{ margin: 0, maxWidth: 900, fontSize: 14, lineHeight: 1.8, color: '#475569' }}>
|
||||
这里统一承载 D-InSAR 配对、批次、生产准备、运行和产物管理,以及 Gamma SBAS-InSAR 生产链。
|
||||
陆探与哨兵源数据按压缩包登记,生产时再解包到本机 Task_Pool;高分三只登记外部 SARscape 服务器复制回来的 _geo 结果。
|
||||
</p>
|
||||
</section>
|
||||
{readOnly && (
|
||||
<div style={{ border: '1px solid #f59e0b', color: '#92400e', background: '#fffbeb', borderRadius: 6, padding: '8px 10px', fontSize: 13 }}>
|
||||
当前为只读账号,生产提交操作已禁用。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<section
|
||||
style={{
|
||||
...heroCardStyle,
|
||||
padding: '18px',
|
||||
display: 'grid',
|
||||
gap: 12,
|
||||
alignContent: 'start',
|
||||
}}
|
||||
>
|
||||
<div style={summaryCardStyle}>
|
||||
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 6 }}>主生产链</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: '#0f172a' }}>D-InSAR / SBAS</div>
|
||||
<div style={{ fontSize: 12, lineHeight: 1.6, color: '#475569', marginTop: 4 }}>
|
||||
D-InSAR 使用配对批次驱动;SBAS 使用 Gamma IPTA 工作流驱动。PS/旧时序入口不再作为主流程展示。
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 14 }}>
|
||||
{WORKFLOW_STEPS.map((step, index) => (
|
||||
<div
|
||||
key={step}
|
||||
style={{
|
||||
border: '1px solid #e2e8f0',
|
||||
background: '#f8fafc',
|
||||
borderRadius: 6,
|
||||
padding: '6px 9px',
|
||||
color: '#334155',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{index + 1}. {step}
|
||||
</div>
|
||||
</div>
|
||||
<div style={summaryCardStyle}>
|
||||
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 6 }}>运行边界</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: '#0f172a' }}>本机 Task_Pool</div>
|
||||
<div style={{ fontSize: 12, lineHeight: 1.6, color: '#475569', marginTop: 4 }}>
|
||||
源压缩包先登记元数据,生产需要时再按需解包;D-InSAR/SBAS 不走 UNC。
|
||||
</div>
|
||||
</div>
|
||||
<div style={summaryCardStyle}>
|
||||
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 6 }}>结果管理</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: '#0f172a' }}>产物 catalog</div>
|
||||
<div style={{ fontSize: 12, lineHeight: 1.6, color: '#475569', marginTop: 4 }}>
|
||||
生产结果进入 D-InSAR、SBAS 或 GF3 数据目录,后续分析从结果 catalog 读取。
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section
|
||||
style={{
|
||||
...heroCardStyle,
|
||||
padding: '14px',
|
||||
marginBottom: 18,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 12 }}>
|
||||
{PRODUCTION_WORKSPACE_WORKBENCHES.map(workbench => {
|
||||
const isActive = workbench.key === activeWorkbench;
|
||||
return (
|
||||
<button
|
||||
key={workbench.key}
|
||||
type="button"
|
||||
onClick={() => switchWorkbench(workbench)}
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: '16px 18px',
|
||||
borderRadius: 10,
|
||||
border: `1px solid ${isActive ? '#93c5fd' : '#d7e0eb'}`,
|
||||
background: isActive
|
||||
? '#eff6ff'
|
||||
: 'rgba(255, 255, 255, 0.88)',
|
||||
boxShadow: isActive ? '0 10px 24px rgba(37, 99, 235, 0.12)' : 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 8 }}>
|
||||
<strong style={{ fontSize: 16, color: '#0f172a' }}>{workbench.label}</strong>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: 8,
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
color: isActive ? '#1d4ed8' : '#64748b',
|
||||
background: isActive ? '#dbeafe' : '#f1f5f9',
|
||||
}}
|
||||
>
|
||||
{isActive ? '当前视图' : '切换'}
|
||||
<div style={sectionStyle}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(220px, 280px) minmax(0, 1fr)', gap: 14, alignItems: 'start' }}>
|
||||
<aside style={{ ...compactPanelStyle, padding: 12 }}>
|
||||
<div style={{ color: '#475569', fontSize: 13, marginBottom: 8 }}>工作流</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
{PRODUCTION_WORKSPACE_WORKBENCHES.map(workbench => (
|
||||
<button
|
||||
key={workbench.key}
|
||||
type="button"
|
||||
onClick={() => switchWorkbench(workbench)}
|
||||
style={{ ...buttonStyle(activeWorkbench === workbench.key), textAlign: 'left' }}
|
||||
>
|
||||
<span style={{ display: 'block' }}>{workbench.label}</span>
|
||||
<span style={{ display: 'block', marginTop: 4, color: activeWorkbench === workbench.key ? '#2563eb' : '#64748b', fontWeight: 400 }}>
|
||||
{workbench.description}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, lineHeight: 1.7, color: '#475569' }}>{workbench.description}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
style={{
|
||||
...heroCardStyle,
|
||||
padding: '12px',
|
||||
marginBottom: 18,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 10 }}>
|
||||
{activeSubViews.map(view => {
|
||||
const isActive = view.key === activeView;
|
||||
return (
|
||||
<button
|
||||
key={view.key}
|
||||
type="button"
|
||||
onClick={() => setActiveView(view.key)}
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: '12px 14px',
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${isActive ? '#2563eb' : '#d7e0eb'}`,
|
||||
background: isActive ? '#ffffff' : '#f8fafc',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<strong style={{ display: 'block', marginBottom: 6, fontSize: 14, color: isActive ? '#1d4ed8' : '#0f172a' }}>
|
||||
{view.label}
|
||||
</strong>
|
||||
<span style={{ display: 'block', fontSize: 12, lineHeight: 1.6, color: '#475569' }}>
|
||||
{view.description}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div style={{ marginBottom: 12, fontSize: 12, color: '#64748b' }}>
|
||||
{activeWorkbenchMeta?.label} / {activeViewMeta.label}
|
||||
</div>
|
||||
<Suspense fallback={<PanelLoadingBody message={`正在加载 ${activeViewMeta.label}...`} />}>
|
||||
{SENSOR_PRODUCTION_PLACEHOLDERS[activeView] && (
|
||||
<SensorProductionPlaceholder viewKey={activeView} />
|
||||
)}
|
||||
{activeView === 'dinsar_pairing' && (
|
||||
<LazyPairPlanningPanel
|
||||
foundPairs={foundPairs}
|
||||
selectedPairsCount={selectedPairsCount}
|
||||
isLoading={isLoading}
|
||||
isReadOnlyUser={readOnly}
|
||||
hasEnoughRadarScenesForPlanning={hasEnoughRadarScenesForPlanning}
|
||||
onOpenPairingModal={pairingPanel.onOpenPairingModal}
|
||||
hasRadarSearched={hasRadarSearched}
|
||||
onRefreshRadarSearch={pairingPanel.onRefreshRadarSearch}
|
||||
onSearchAll={radarPanel.onSearchAll}
|
||||
onRefreshDinsar={pairingPanel.onRefreshDinsar}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
{activeView === 'dinsar_pairs' && (
|
||||
<div style={{ display: 'grid', gap: 16 }}>
|
||||
<LazyPairsListPanel
|
||||
onVisualizePair={pairsPanel.onVisualizePair}
|
||||
onTogglePairVisibility={pairsPanel.onTogglePairVisibility}
|
||||
onCreateDinsarBatch={pairsPanel.onCreateDinsarBatch}
|
||||
/>
|
||||
<LazyBatchPanel />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{activeView === 'dinsar_prepare' && (
|
||||
<LazyDataCopierPanel
|
||||
apiEndpoint={apiEndpoint}
|
||||
readOnly={readOnly}
|
||||
onJobQueued={handleDinsarPrepareQueued}
|
||||
/>
|
||||
)}
|
||||
{activeView === 'dinsar_runs' && (
|
||||
<LazyDinsarProductionPanel
|
||||
readOnly={readOnly}
|
||||
onJobQueued={handleDinsarRunQueued}
|
||||
/>
|
||||
)}
|
||||
{['sbas_insar_planning', 'sbas_insar_batches', 'sbas_insar_prepare', 'sbas_insar_runs'].includes(activeView) && (
|
||||
<LazySbasInsarProductionPanel
|
||||
readOnly={readOnly}
|
||||
onTaskStart={onTaskStart}
|
||||
initialFocus={{
|
||||
sbas_insar_planning: 'planning',
|
||||
sbas_insar_batches: 'batches',
|
||||
sbas_insar_prepare: 'prepare',
|
||||
sbas_insar_runs: 'runs',
|
||||
}[activeView]}
|
||||
/>
|
||||
)}
|
||||
{activeView === 'sbas_insar_products' && (
|
||||
<LazySbasInsarProductsPanel
|
||||
readOnly={readOnly}
|
||||
onJobQueued={handleSbasProductQueued}
|
||||
/>
|
||||
)}
|
||||
{activeView === 'dinsar_products' && (
|
||||
<LazyDinsarProductsPanel
|
||||
readOnly={readOnly}
|
||||
onJobQueued={handleDinsarProductQueued}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<main style={{ display: 'grid', gap: 14, minWidth: 0 }}>
|
||||
<div style={{ ...compactPanelStyle, padding: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<div>
|
||||
<div style={{ color: '#475569', fontSize: 13 }}>{currentWorkbench?.label}</div>
|
||||
<h3 style={{ margin: '3px 0 0', fontSize: 18 }}>{currentView?.label || '生产视图'}</h3>
|
||||
</div>
|
||||
<div style={{ ...mutedTextStyle, maxWidth: 560 }}>{currentView?.description}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 12 }}>
|
||||
{currentWorkbench?.views.map(view => (
|
||||
<button
|
||||
key={view.key}
|
||||
type="button"
|
||||
onClick={() => setActiveView(view.key)}
|
||||
style={buttonStyle(activeView === view.key)}
|
||||
>
|
||||
{view.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<PanelLoadingBody message="正在加载生产面板..." />}>
|
||||
{renderContent()}
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user