update web workflow and preview behavior
This commit is contained in:
@@ -86,12 +86,13 @@ const DEFAULT_PACKAGE_OPTIONS: PackageOptions = {
|
||||
};
|
||||
|
||||
const VIDEO_SOURCE_OPTIONS = [
|
||||
{ key: 'original', label: '原始序列' },
|
||||
{ key: 'hard_boundary', label: '硬边界' },
|
||||
{ key: 'gaussian_smooth', label: '高斯平滑' },
|
||||
{ key: 'soft_transition', label: '软过渡重建' },
|
||||
{ key: 'hard_boundary', label: '硬边界', mode: 'hard_boundary' },
|
||||
{ key: 'gaussian_smooth', label: '高斯平滑', mode: 'gaussian_smooth' },
|
||||
{ key: 'soft_transition', label: '软过渡重建', mode: 'soft_transition' },
|
||||
];
|
||||
|
||||
const PREVIEW_ALGORITHM_OPTIONS = VIDEO_SOURCE_OPTIONS;
|
||||
|
||||
type LibraryItem = {
|
||||
id: string;
|
||||
patientId: string;
|
||||
@@ -284,6 +285,8 @@ export default function App() {
|
||||
// --- Simulation State (Workspace) ---
|
||||
const [cervicalRotation, setCervicalRotation] = useState(14.5);
|
||||
const [transitionWidth, setTransitionWidth] = useState(90);
|
||||
const [previewGaussianSigma, setPreviewGaussianSigma] = useState(3);
|
||||
const [previewAlgorithm, setPreviewAlgorithm] = useState('soft_transition');
|
||||
const [showPreviewCutoffLine, setShowPreviewCutoffLine] = useState(true);
|
||||
const [isSimulating, setIsSimulating] = useState(restoredDeformationJob?.job.status === 'running');
|
||||
const [progress, setProgress] = useState(restoredDeformationJob ? progressFromJob(restoredDeformationJob.job, restoredDeformationJob.progress) : 0);
|
||||
@@ -299,7 +302,7 @@ export default function App() {
|
||||
const [packageOptions, setPackageOptions] = useState<PackageOptions>(DEFAULT_PACKAGE_OPTIONS);
|
||||
const [videoMaxAngle, setVideoMaxAngle] = useState(20);
|
||||
const [videoDuration, setVideoDuration] = useState(6);
|
||||
const [videoSource, setVideoSource] = useState('original');
|
||||
const [videoSource, setVideoSource] = useState('hard_boundary');
|
||||
const [showVideoArrow, setShowVideoArrow] = useState(true);
|
||||
|
||||
// --- User Management Shared State ---
|
||||
@@ -309,13 +312,12 @@ export default function App() {
|
||||
const [pwChangeInput, setPwChangeInput] = useState('');
|
||||
const [showAddUser, setShowAddUser] = useState(false);
|
||||
const [activeUserMenu, setActiveUserMenu] = useState<string | null>(null);
|
||||
const [isResettingDemo, setIsResettingDemo] = useState(false);
|
||||
|
||||
const selectedDataset = libraryData.find(item => item.id === selectedLibraryId) || libraryData[0];
|
||||
const selectedInputDir = selectedDataset?.dicomPath || '';
|
||||
const selectedVideoSource = VIDEO_SOURCE_OPTIONS.find(option => option.key === videoSource) || VIDEO_SOURCE_OPTIONS[0];
|
||||
const videoSourceInputDir = videoSource === 'original'
|
||||
? selectedInputDir
|
||||
: deformationJob?.result?.outputs?.[videoSource] || '';
|
||||
const videoSourceInputDir = selectedInputDir;
|
||||
const isVideoSourceReady = Boolean(videoSourceInputDir);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -364,6 +366,16 @@ export default function App() {
|
||||
link.remove();
|
||||
};
|
||||
|
||||
const downloadPreviewImage = () => {
|
||||
if (!previewImage) return;
|
||||
const link = document.createElement('a');
|
||||
link.href = previewImage;
|
||||
link.download = `quick_2d_preview_${previewAlgorithm}_${cervicalRotation.toFixed(1)}deg.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
};
|
||||
|
||||
const clearDeformationTask = () => {
|
||||
setDeformationJob(null);
|
||||
setProgress(0);
|
||||
@@ -449,6 +461,31 @@ export default function App() {
|
||||
return items;
|
||||
};
|
||||
|
||||
const resetDemoEnvironment = async () => {
|
||||
if (isResettingDemo) return;
|
||||
if (!confirm('确定恢复演示环境出厂设置吗?影像数据库将只保留 Ori_Head_CT,并清空当前任务结果。')) return;
|
||||
|
||||
setIsResettingDemo(true);
|
||||
try {
|
||||
const data = await apiRequest('/api/demo/reset', { method: 'POST' }) as { items?: LibraryItem[] };
|
||||
const items = data.items || [];
|
||||
setLibraryData(items);
|
||||
setSelectedLibraryId(items[0]?.id || '');
|
||||
setLibraryInfo(null);
|
||||
setPreviewImage('');
|
||||
setVideoJob(null);
|
||||
setZipJobs({});
|
||||
clearDeformationTask();
|
||||
setBackendOnline(true);
|
||||
setBackendMessage('演示环境已恢复出厂设置');
|
||||
showToast('已恢复演示环境出厂设置');
|
||||
} catch (error) {
|
||||
showToast((error as Error).message);
|
||||
} finally {
|
||||
setIsResettingDemo(false);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshBackendDefaults = async () => {
|
||||
try {
|
||||
const data = await apiRequest('/api/defaults');
|
||||
@@ -755,6 +792,9 @@ export default function App() {
|
||||
body: JSON.stringify({
|
||||
inputDir: selectedInputDir,
|
||||
angleDegrees: cervicalRotation,
|
||||
transitionWidth,
|
||||
gaussianSigma: previewGaussianSigma,
|
||||
mode: previewAlgorithm,
|
||||
showCutoffLine: showPreviewCutoffLine
|
||||
}),
|
||||
signal: controller.signal
|
||||
@@ -778,7 +818,7 @@ export default function App() {
|
||||
controller.abort();
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [currentPage, selectedInputDir, cervicalRotation, showPreviewCutoffLine]);
|
||||
}, [currentPage, selectedInputDir, cervicalRotation, transitionWidth, previewGaussianSigma, previewAlgorithm, showPreviewCutoffLine]);
|
||||
|
||||
const handleRunSimulation = async () => {
|
||||
if (isSimulating) return;
|
||||
@@ -808,7 +848,7 @@ export default function App() {
|
||||
const handleGenerateVideo = async () => {
|
||||
if (videoJob?.status === 'running') return;
|
||||
if (!videoSourceInputDir) {
|
||||
showToast(videoSource === 'original' ? '请选择影像库数据源' : '请先完成四状态输出,再生成该状态视频');
|
||||
showToast('请选择影像库数据源');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -819,6 +859,7 @@ export default function App() {
|
||||
maxAngle: videoMaxAngle,
|
||||
durationSeconds: videoDuration,
|
||||
showArrow: showVideoArrow,
|
||||
mode: selectedVideoSource.mode,
|
||||
})
|
||||
}) as BackendJob;
|
||||
setVideoJob(job);
|
||||
@@ -989,13 +1030,33 @@ export default function App() {
|
||||
|
||||
<div className="space-y-4 pt-2">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs font-bold mb-2 text-slate-600"><span>仰头角度</span><span className="text-blue-600 font-mono">{cervicalRotation.toFixed(1)}°</span></div>
|
||||
<input type="range" min="0" max="20" step="0.5" value={cervicalRotation} onChange={e => setCervicalRotation(parseFloat(e.target.value))} className="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600 opacity-80 hover:opacity-100 transition-opacity" />
|
||||
<div className="flex justify-between text-xs font-bold mb-2 text-slate-600"><span>快速预览算法</span><span className="text-blue-600 font-mono">{PREVIEW_ALGORITHM_OPTIONS.find(option => option.key === previewAlgorithm)?.label}</span></div>
|
||||
<select
|
||||
value={previewAlgorithm}
|
||||
onChange={event => setPreviewAlgorithm(event.target.value)}
|
||||
className="w-full px-3 py-2.5 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 text-xs font-bold text-slate-700"
|
||||
>
|
||||
{PREVIEW_ALGORITHM_OPTIONS.map(option => (
|
||||
<option key={option.key} value={option.key}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs font-bold mb-2 text-slate-600"><span>过渡平滑宽度</span><span className="text-blue-600 font-mono">{transitionWidth}</span></div>
|
||||
<input type="range" min="50" max="160" step="10" value={transitionWidth} onChange={e => setTransitionWidth(parseInt(e.target.value, 10))} className="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600 opacity-80 hover:opacity-100 transition-opacity" />
|
||||
<div className="flex justify-between text-xs font-bold mb-2 text-slate-600"><span>仰头角度</span><span className="text-blue-600 font-mono">{cervicalRotation.toFixed(1)}°</span></div>
|
||||
<input type="range" min="0" max="45" step="0.5" value={cervicalRotation} onChange={e => setCervicalRotation(parseFloat(e.target.value))} className="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600 opacity-80 hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
{previewAlgorithm === 'gaussian_smooth' && (
|
||||
<div>
|
||||
<div className="flex justify-between text-xs font-bold mb-2 text-slate-600"><span>高斯平滑强度</span><span className="text-blue-600 font-mono">{previewGaussianSigma.toFixed(1)}</span></div>
|
||||
<input type="range" min="1" max="12" step="0.5" value={previewGaussianSigma} onChange={e => setPreviewGaussianSigma(parseFloat(e.target.value))} className="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600 opacity-80 hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
)}
|
||||
{previewAlgorithm === 'soft_transition' && (
|
||||
<div>
|
||||
<div className="flex justify-between text-xs font-bold mb-2 text-slate-600"><span>软过渡宽度</span><span className="text-blue-600 font-mono">{transitionWidth}</span></div>
|
||||
<input type="range" min="50" max="160" step="10" value={transitionWidth} onChange={e => setTransitionWidth(parseInt(e.target.value, 10))} className="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600 opacity-80 hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowPreviewCutoffLine(value => !value)}
|
||||
className={`w-full py-2.5 rounded-xl text-xs font-black transition-all flex items-center justify-center gap-2 border ${
|
||||
@@ -1050,8 +1111,8 @@ export default function App() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-[10px] font-bold mb-2 text-slate-500">
|
||||
<span>视频来源序列</span>
|
||||
{videoSource !== 'original' && !isVideoSourceReady && <span className="text-amber-500">需先生成四状态</span>}
|
||||
<span>视频形变方式</span>
|
||||
{!isVideoSourceReady && <span className="text-amber-500">请选择影像库数据源</span>}
|
||||
</div>
|
||||
<select
|
||||
value={videoSource}
|
||||
@@ -1069,7 +1130,7 @@ export default function App() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-[10px] font-bold mb-2 text-slate-500"><span>最大角度</span><span>{videoMaxAngle}°</span></div>
|
||||
<input type="range" min="5" max="30" step="1" value={videoMaxAngle} onChange={e => setVideoMaxAngle(parseInt(e.target.value, 10))} className="w-full accent-blue-600" />
|
||||
<input type="range" min="5" max="45" step="1" value={videoMaxAngle} onChange={e => setVideoMaxAngle(parseInt(e.target.value, 10))} className="w-full accent-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-[10px] font-bold mb-2 text-slate-500"><span>时长</span><span>{videoDuration}s</span></div>
|
||||
@@ -1122,9 +1183,19 @@ export default function App() {
|
||||
<h4 className="font-black text-slate-800">快速 2D 预览</h4>
|
||||
<p className="text-[10px] text-slate-400 font-bold mt-1">对应 head_extension_app.py 的 preview_deform_2d</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] font-mono text-slate-400">{cervicalRotation.toFixed(1)} DEG</span>
|
||||
{isPreviewLoading && <p className="text-[9px] font-bold text-blue-500 mt-1">自动更新中...</p>}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] font-mono text-slate-400">{cervicalRotation.toFixed(1)} DEG</span>
|
||||
{isPreviewLoading && <p className="text-[9px] font-bold text-blue-500 mt-1">自动更新中...</p>}
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadPreviewImage}
|
||||
disabled={!previewImage}
|
||||
className="w-9 h-9 rounded-xl bg-slate-100 text-slate-500 hover:bg-blue-600 hover:text-white transition-all flex items-center justify-center disabled:opacity-40 disabled:hover:bg-slate-100 disabled:hover:text-slate-500"
|
||||
title="下载当前快速 2D 预览图"
|
||||
>
|
||||
<Download size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[360px] bg-slate-950 flex items-center justify-center">
|
||||
@@ -1399,6 +1470,23 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentUser?.role === 'admin' && (
|
||||
<div className="bg-white p-8 rounded-[2.5rem] border border-red-100 shadow-sm flex items-center justify-between gap-6">
|
||||
<div>
|
||||
<h3 className="font-black text-slate-800">演示环境出厂设置</h3>
|
||||
<p className="text-xs text-slate-400 font-bold mt-2">恢复后影像数据库只保留 Ori_Head_CT,并清空当前任务结果。</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={resetDemoEnvironment}
|
||||
disabled={isResettingDemo}
|
||||
className="px-6 py-3.5 bg-red-50 text-red-600 rounded-2xl text-xs font-black hover:bg-red-600 hover:text-white transition-all flex items-center gap-3 disabled:opacity-60 disabled:cursor-wait"
|
||||
>
|
||||
<RefreshCw size={15} className={isResettingDemo ? 'animate-spin' : ''} />
|
||||
{isResettingDemo ? '正在恢复' : '恢复演示环境出厂设置'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User List Header */}
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<h3 className="text-xs font-black text-slate-400 uppercase tracking-[0.2em]">系统权限生命周期管理</h3>
|
||||
|
||||
Reference in New Issue
Block a user