feat: implement segment-based PDF download functionality with a dedicated UI for multi-page export
Build and Push Multi-Platform Images / build-and-push (push) Successful in 34s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 34s
This commit is contained in:
@@ -107,8 +107,10 @@ export const inboxApi = {
|
||||
)
|
||||
.then((r) => r.data),
|
||||
|
||||
downloadBlob: (id: string) =>
|
||||
api.get<Blob>(`/api/inbox/${encodeURIComponent(id)}/download`, { responseType: 'blob' }).then((r) => r.data),
|
||||
downloadSegmentBlob: (id: string, pages: number[]) =>
|
||||
api
|
||||
.post<Blob>(`/api/inbox/${encodeURIComponent(id)}/download-segment`, { pages }, { responseType: 'blob' })
|
||||
.then((r) => r.data),
|
||||
|
||||
sendEmail: (
|
||||
id: string,
|
||||
|
||||
@@ -529,6 +529,113 @@ function PostprocessWizardModal({
|
||||
}
|
||||
|
||||
|
||||
interface DownloadSegmentsDialogProps {
|
||||
open: boolean;
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
documents: DocumentSegment[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function DownloadSegmentsDialog({ open, fileId, fileName, documents, onClose }: DownloadSegmentsDialogProps) {
|
||||
const [filenames, setFilenames] = useState<string[]>([]);
|
||||
const [downloading, setDownloading] = useState<number | 'all' | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const base = fileName.replace(/\.pdf$/i, '');
|
||||
setFilenames(
|
||||
documents.map((doc) =>
|
||||
doc.belegname || (documents.length === 1 ? base : `${base}_${doc.index + 1}`),
|
||||
),
|
||||
);
|
||||
setDownloading(null);
|
||||
}, [open, documents, fileName]);
|
||||
|
||||
const triggerDownload = (blob: Blob, name: string) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = name.endsWith('.pdf') ? name : `${name}.pdf`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const downloadOne = async (idx: number) => {
|
||||
setDownloading(idx);
|
||||
try {
|
||||
const blob = await inboxApi.downloadSegmentBlob(fileId, documents[idx].pages);
|
||||
triggerDownload(blob, filenames[idx] || fileName);
|
||||
} catch {
|
||||
message.error('Download fehlgeschlagen');
|
||||
} finally {
|
||||
setDownloading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadAll = async () => {
|
||||
setDownloading('all');
|
||||
try {
|
||||
for (let i = 0; i < documents.length; i++) {
|
||||
const blob = await inboxApi.downloadSegmentBlob(fileId, documents[i].pages);
|
||||
triggerDownload(blob, filenames[i] || fileName);
|
||||
if (i < documents.length - 1) await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
} catch {
|
||||
message.error('Download fehlgeschlagen');
|
||||
} finally {
|
||||
setDownloading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title="Dokumente herunterladen"
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
Schließen
|
||||
</Button>,
|
||||
<Button key="all" type="primary" icon={<SaveOutlined />} loading={downloading === 'all'} disabled={downloading !== null && downloading !== 'all'} onClick={downloadAll}>
|
||||
Alle herunterladen
|
||||
</Button>,
|
||||
]}
|
||||
width={560}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 16 }}>
|
||||
{documents.map((doc, i) => {
|
||||
const first = doc.pages[0];
|
||||
const last = doc.pages[doc.pages.length - 1];
|
||||
const range = first === last ? `Seite ${first}` : `Seiten ${first}–${last}`;
|
||||
return (
|
||||
<div key={doc.index} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<span style={{ minWidth: 90, color: '#888', fontSize: 12 }}>{range}</span>
|
||||
<Input
|
||||
value={filenames[i] ?? ''}
|
||||
onChange={(e) =>
|
||||
setFilenames((prev) => prev.map((f, j) => (j === i ? e.target.value : f)))
|
||||
}
|
||||
suffix=".pdf"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
icon={<SaveOutlined />}
|
||||
loading={downloading === i}
|
||||
disabled={downloading !== null && downloading !== i}
|
||||
onClick={() => downloadOne(i)}
|
||||
>
|
||||
Laden
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function TiptapToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
||||
if (!editor) return null;
|
||||
const btnStyle = (active: boolean): React.CSSProperties => ({
|
||||
@@ -638,7 +745,7 @@ export default function InboxDetailPage() {
|
||||
const [selectedPage, setSelectedPage] = useState(1);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false);
|
||||
const [emailDialogOpen, setEmailDialogOpen] = useState(false);
|
||||
const [scanMode, setScanMode] = useState(false);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
@@ -1005,8 +1112,7 @@ export default function InboxDetailPage() {
|
||||
<Dropdown.Button
|
||||
type="primary"
|
||||
icon={<DownOutlined />}
|
||||
disabled={documents.length === 0 || downloading}
|
||||
loading={downloading}
|
||||
disabled={documents.length === 0}
|
||||
onClick={() => setWizardOpen(true)}
|
||||
menu={{
|
||||
items: [
|
||||
@@ -1015,17 +1121,7 @@ export default function InboxDetailPage() {
|
||||
] as MenuProps['items'],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'save') {
|
||||
setDownloading(true);
|
||||
inboxApi.downloadBlob(file.id).then((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = file.name;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}).catch(() => {
|
||||
message.error('Download fehlgeschlagen');
|
||||
}).finally(() => setDownloading(false));
|
||||
setDownloadDialogOpen(true);
|
||||
}
|
||||
if (key === 'email') setEmailDialogOpen(true);
|
||||
},
|
||||
@@ -1454,6 +1550,13 @@ export default function InboxDetailPage() {
|
||||
onClose={() => setWizardOpen(false)}
|
||||
onDeleted={() => navigate('/inbox')}
|
||||
/>
|
||||
<DownloadSegmentsDialog
|
||||
open={downloadDialogOpen}
|
||||
fileId={file.id}
|
||||
fileName={file.name}
|
||||
documents={documents}
|
||||
onClose={() => setDownloadDialogOpen(false)}
|
||||
/>
|
||||
<SendEmailDialog
|
||||
open={emailDialogOpen}
|
||||
fileId={file.id}
|
||||
|
||||
Reference in New Issue
Block a user