select contents for four-state zip package

This commit is contained in:
2026-05-03 02:45:45 +08:00
parent 49b797e7dc
commit 2fcab9c71a
2 changed files with 225 additions and 5 deletions

View File

@@ -60,6 +60,31 @@ type BackendJob = {
type ZipJobsByTarget = Record<string, BackendJob>;
type PackageOptions = {
dicom: string[];
images: string[];
};
const DICOM_PACKAGE_OPTIONS = [
{ key: 'original', label: '原始序列 DICOM' },
{ key: 'hard_boundary', label: '硬边界 DICOM' },
{ key: 'gaussian_smooth', label: '高斯平滑 DICOM' },
{ key: 'soft_transition', label: '软过渡重建 DICOM' },
];
const IMAGE_PACKAGE_OPTIONS = [
{ key: 'comparison', label: '原始图片四状态过程对比图' },
{ key: 'original', label: '原始序列' },
{ key: 'hard_boundary', label: '硬边界' },
{ key: 'gaussian_smooth', label: '高斯平滑' },
{ key: 'soft_transition', label: '软过渡重建' },
];
const DEFAULT_PACKAGE_OPTIONS: PackageOptions = {
dicom: DICOM_PACKAGE_OPTIONS.map(option => option.key),
images: IMAGE_PACKAGE_OPTIONS.map(option => option.key),
};
type LibraryItem = {
id: string;
patientId: string;
@@ -263,6 +288,8 @@ export default function App() {
const [deformationJob, setDeformationJob] = useState<BackendJob | null>(restoredDeformationJob?.job || null);
const [videoJob, setVideoJob] = useState<BackendJob | null>(null);
const [zipJobs, setZipJobs] = useState<ZipJobsByTarget>({});
const [isPackageDialogOpen, setIsPackageDialogOpen] = useState(false);
const [packageOptions, setPackageOptions] = useState<PackageOptions>(DEFAULT_PACKAGE_OPTIONS);
const [videoMaxAngle, setVideoMaxAngle] = useState(20);
const [videoDuration, setVideoDuration] = useState(6);
@@ -339,7 +366,7 @@ export default function App() {
setIsSimulating(job.status === 'running');
};
const handlePackageDownload = async (target: string) => {
const handlePackageDownload = async (target: string, selectedPackageOptions?: PackageOptions) => {
if (!deformationJob?.id || zipJobs[target]?.status === 'running') return;
try {
const job = await apiRequest('/api/deformation/package', {
@@ -347,7 +374,8 @@ export default function App() {
body: JSON.stringify({
username: currentUser?.username || 'anonymous',
jobId: deformationJob.id,
target
target,
packageOptions: selectedPackageOptions
})
}) as BackendJob;
setZipJobs(current => ({ ...current, [target]: job }));
@@ -367,6 +395,38 @@ export default function App() {
}
};
const togglePackageOption = (group: keyof PackageOptions, key: string) => {
setPackageOptions(current => {
const exists = current[group].includes(key);
return {
...current,
[group]: exists
? current[group].filter(value => value !== key)
: [...current[group], key],
};
});
};
const setPackageGroupSelection = (group: keyof PackageOptions, keys: string[]) => {
setPackageOptions(current => ({ ...current, [group]: keys }));
};
const invertPackageGroupSelection = (group: keyof PackageOptions, allKeys: string[]) => {
setPackageOptions(current => ({
...current,
[group]: allKeys.filter(key => !current[group].includes(key)),
}));
};
const confirmPackageDownload = () => {
if (!packageOptions.dicom.length && !packageOptions.images.length) {
showToast('请至少选择一个打包内容');
return;
}
setIsPackageDialogOpen(false);
handlePackageDownload('all', packageOptions);
};
const loadLibrary = async () => {
const data = await apiRequest('/api/library');
const items = data.items || [];
@@ -947,7 +1007,10 @@ export default function App() {
{deformationJob?.error && <p className="text-[10px] text-red-500 font-bold mt-2 break-all">{deformationJob.error}</p>}
{deformationJob?.status === 'completed' && deformationJob.result?.outputs && (
<button
onClick={() => handlePackageDownload('all')}
onClick={() => {
setPackageOptions(DEFAULT_PACKAGE_OPTIONS);
setIsPackageDialogOpen(true);
}}
disabled={zipJobs.all?.status === 'running'}
className="mt-3 w-full py-3 bg-green-600 text-white rounded-xl text-xs font-bold hover:bg-green-700 transition-all flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-wait"
>
@@ -1394,6 +1457,105 @@ export default function App() {
</div>
</main>
{isPackageDialogOpen && (
<div className="fixed inset-0 bg-slate-950/45 backdrop-blur-sm z-40 flex items-center justify-center p-6">
<div className="w-full max-w-3xl bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden">
<div className="px-7 py-5 border-b flex items-center justify-between">
<div>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]"> ZIP </p>
<h3 className="text-xl font-black text-slate-800 mt-1"></h3>
</div>
<button
onClick={() => setIsPackageDialogOpen(false)}
className="w-10 h-10 rounded-xl bg-slate-50 text-slate-400 hover:bg-red-50 hover:text-red-500 flex items-center justify-center transition-all"
>
<X size={20} />
</button>
</div>
<div className="p-7 space-y-6">
{[
{
key: 'dicom' as keyof PackageOptions,
title: 'DICOM 类',
options: DICOM_PACKAGE_OPTIONS,
allKeys: DICOM_PACKAGE_OPTIONS.map(option => option.key),
},
{
key: 'images' as keyof PackageOptions,
title: '图片类',
options: IMAGE_PACKAGE_OPTIONS,
allKeys: IMAGE_PACKAGE_OPTIONS.map(option => option.key),
},
].map(group => (
<div key={group.key} className="border border-slate-100 rounded-2xl p-5 bg-slate-50/50">
<div className="flex items-center justify-between mb-4">
<h4 className="text-sm font-black text-slate-800">{group.title}</h4>
<div className="flex items-center gap-2">
<button
onClick={() => setPackageGroupSelection(group.key, group.allKeys)}
className="px-3 py-1.5 rounded-lg bg-white border border-slate-200 text-[10px] font-black text-slate-500 hover:text-blue-600 hover:border-blue-200 transition-all"
>
</button>
<button
onClick={() => invertPackageGroupSelection(group.key, group.allKeys)}
className="px-3 py-1.5 rounded-lg bg-white border border-slate-200 text-[10px] font-black text-slate-500 hover:text-blue-600 hover:border-blue-200 transition-all"
>
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
{group.options.map(option => {
const checked = packageOptions[group.key].includes(option.key);
return (
<button
key={`${group.key}-${option.key}`}
onClick={() => togglePackageOption(group.key, option.key)}
className={`flex items-center gap-3 p-3 rounded-xl border text-left transition-all ${
checked
? 'bg-blue-50 border-blue-200 text-blue-700'
: 'bg-white border-slate-100 text-slate-500 hover:border-slate-200'
}`}
>
<span className={`w-5 h-5 rounded-md border flex items-center justify-center shrink-0 ${
checked ? 'bg-blue-600 border-blue-600 text-white' : 'border-slate-200'
}`}>
{checked && <Check size={13} />}
</span>
<span className="text-xs font-black">{option.label}</span>
</button>
);
})}
</div>
</div>
))}
<div className="flex items-center justify-between pt-2">
<p className="text-[10px] font-bold text-slate-400">
{packageOptions.dicom.length + packageOptions.images.length}
</p>
<div className="flex items-center gap-3">
<button
onClick={() => setIsPackageDialogOpen(false)}
className="px-5 py-3 rounded-xl bg-slate-100 text-slate-500 text-xs font-black hover:bg-slate-200 transition-all"
>
</button>
<button
onClick={confirmPackageDownload}
className="px-5 py-3 rounded-xl bg-green-600 text-white text-xs font-black hover:bg-green-700 transition-all flex items-center gap-2"
>
<Download size={14} />
</button>
</div>
</div>
</div>
</div>
</div>
)}
{(libraryInfo || isLibraryInfoLoading) && (
<div className="fixed inset-0 bg-slate-950/45 backdrop-blur-sm z-40 flex items-center justify-center p-6">
<div className="w-full max-w-4xl max-h-[85vh] bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden">