2026-05-20-22-07-46 导出命名与映射视图摘要优化
This commit is contained in:
@@ -159,6 +159,36 @@ function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function timestampForFilename(date = new Date()) {
|
||||
const parts = new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
}).formatToParts(date);
|
||||
const value = (type: string) => parts.find((part) => part.type === type)?.value ?? '00';
|
||||
return `${value('year')}-${value('month')}-${value('day')}-${value('hour')}-${value('minute')}-${value('second')}`;
|
||||
}
|
||||
|
||||
function sanitizeFilenamePart(input: string, fallback: string) {
|
||||
const cleaned = input
|
||||
.trim()
|
||||
.replace(/[\\/:*?"<>|]+/g, '_')
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
return cleaned || fallback;
|
||||
}
|
||||
|
||||
function contentDispositionAttachment(filename: string) {
|
||||
const asciiFallback = filename.replace(/[^\x20-\x7e]/g, '_').replace(/["\\]/g, '_');
|
||||
return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
||||
}
|
||||
|
||||
function ensureDir(dir: string) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
@@ -1247,6 +1277,7 @@ function createProjectExportBundle({
|
||||
compressed,
|
||||
activePose,
|
||||
segmentationScope,
|
||||
exportRoot,
|
||||
}: {
|
||||
project: ProjectRecord;
|
||||
files: string[];
|
||||
@@ -1254,12 +1285,12 @@ function createProjectExportBundle({
|
||||
compressed: boolean;
|
||||
activePose?: ModelPoseValue;
|
||||
segmentationScope: SegmentationExportScope;
|
||||
exportRoot: string;
|
||||
}) {
|
||||
const entries: Array<{ name: string; data: Buffer; mtime?: number }> = [];
|
||||
const needsVolume = targets.includes('dicom') || targets.includes('segmentation');
|
||||
const volume = needsVolume ? readDicomHuVolume(files) : null;
|
||||
const format = compressed ? 'nii.gz' : 'nii';
|
||||
const exportRoot = `${project.id}-nifti-export`;
|
||||
|
||||
if (targets.includes('dicom') && volume) {
|
||||
entries.push({
|
||||
@@ -2488,6 +2519,7 @@ async function startServer() {
|
||||
|
||||
try {
|
||||
const files = getProjectDicomFiles(project);
|
||||
const exportBase = `${sanitizeFilenamePart(project.name, project.id)}_${timestampForFilename()}`;
|
||||
const payload = createProjectExportBundle({
|
||||
project: exportProject,
|
||||
files,
|
||||
@@ -2495,14 +2527,15 @@ async function startServer() {
|
||||
compressed,
|
||||
activePose,
|
||||
segmentationScope,
|
||||
exportRoot: exportBase,
|
||||
});
|
||||
const filename = `${project.id}-nifti-export.tar.gz`;
|
||||
const filename = `${exportBase}.tar.gz`;
|
||||
fs.writeFileSync(path.join(exportDir, filename), payload);
|
||||
project.exportedMaskCount += targets.includes('segmentation') ? 1 : 0;
|
||||
writeState(state);
|
||||
|
||||
res.setHeader('Content-Type', 'application/gzip');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.setHeader('Content-Disposition', contentDispositionAttachment(filename));
|
||||
res.send(payload);
|
||||
} catch (error) {
|
||||
res.status(422).json({ message: error instanceof Error ? error.message : '导出包生成失败' });
|
||||
|
||||
Reference in New Issue
Block a user