- Introduced a new ExportCanvasModal component for exporting the process flow as PNG or PDF. - Enhanced ProcessFlowCanvas with exportToPNG and exportToPDF methods, utilizing html2canvas and jsPDF for rendering. - Updated the process builder page to integrate the export modal, allowing users to initiate exports easily. - Added UI elements for selecting export format and quality, improving user experience during the export process. - Implemented error handling and progress indicators for export operations, ensuring better feedback for users.
441 lines
12 KiB
Vue
441 lines
12 KiB
Vue
<template>
|
||
<div v-if="show" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" @click="closeModal">
|
||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4" @click.stop>
|
||
<!-- Modal Header -->
|
||
<div class="px-6 py-4 border-b border-gray-200">
|
||
<div class="flex items-center justify-between">
|
||
<h3 class="text-lg font-semibold text-gray-900">Export Canvas</h3>
|
||
<button
|
||
@click="closeModal"
|
||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||
>
|
||
<span class="material-icons">close</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal Body -->
|
||
<div class="px-6 py-4 space-y-4">
|
||
<!-- Export Format -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||
Export Format
|
||
</label>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<button
|
||
@click="selectedFormat = 'png'"
|
||
:class="[
|
||
'flex items-center justify-center px-4 py-3 border rounded-lg transition-all',
|
||
selectedFormat === 'png'
|
||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||
: 'border-gray-300 hover:border-gray-400'
|
||
]"
|
||
>
|
||
<span class="material-icons mr-2 text-sm">image</span>
|
||
PNG Image
|
||
</button>
|
||
<button
|
||
@click="selectedFormat = 'pdf'"
|
||
:class="[
|
||
'flex items-center justify-center px-4 py-3 border rounded-lg transition-all',
|
||
selectedFormat === 'pdf'
|
||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||
: 'border-gray-300 hover:border-gray-400'
|
||
]"
|
||
>
|
||
<span class="material-icons mr-2 text-sm">picture_as_pdf</span>
|
||
PDF Document
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quality Settings -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||
Quality
|
||
</label>
|
||
<select
|
||
v-model="selectedQuality"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
<option value="1">Low (1x)</option>
|
||
<option value="2">Standard (2x)</option>
|
||
<option value="3">High (3x)</option>
|
||
<option value="4">Ultra (4x)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Include Background -->
|
||
<div class="flex items-center">
|
||
<input
|
||
id="includeBackground"
|
||
v-model="includeBackground"
|
||
type="checkbox"
|
||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||
>
|
||
<label for="includeBackground" class="ml-2 block text-sm text-gray-700">
|
||
Include background pattern
|
||
</label>
|
||
</div>
|
||
|
||
<!-- File Name -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||
File Name
|
||
</label>
|
||
<input
|
||
v-model="fileName"
|
||
type="text"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="Enter file name"
|
||
>
|
||
</div>
|
||
|
||
<!-- PDF Options (when PDF is selected) -->
|
||
<div v-if="selectedFormat === 'pdf'" class="space-y-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||
Page Size
|
||
</label>
|
||
<select
|
||
v-model="pdfPageSize"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
<option value="a4">A4 (210 × 297 mm)</option>
|
||
<option value="a3">A3 (297 × 420 mm)</option>
|
||
<option value="letter">Letter (8.5 × 11 in)</option>
|
||
<option value="legal">Legal (8.5 × 14 in)</option>
|
||
<option value="auto">Auto-fit</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||
Orientation
|
||
</label>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<button
|
||
@click="pdfOrientation = 'portrait'"
|
||
:class="[
|
||
'flex items-center justify-center px-3 py-2 border rounded-lg transition-all',
|
||
pdfOrientation === 'portrait'
|
||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||
: 'border-gray-300 hover:border-gray-400'
|
||
]"
|
||
>
|
||
<span class="material-icons mr-1 text-sm">stay_current_portrait</span>
|
||
Portrait
|
||
</button>
|
||
<button
|
||
@click="pdfOrientation = 'landscape'"
|
||
:class="[
|
||
'flex items-center justify-center px-3 py-2 border rounded-lg transition-all',
|
||
pdfOrientation === 'landscape'
|
||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||
: 'border-gray-300 hover:border-gray-400'
|
||
]"
|
||
>
|
||
<span class="material-icons mr-1 text-sm">stay_current_landscape</span>
|
||
Landscape
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Export Progress -->
|
||
<div v-if="isExporting" class="flex flex-col items-center justify-center py-4">
|
||
<div class="flex items-center space-x-2 mb-3">
|
||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
|
||
<span class="text-sm text-gray-600">{{ exportProgress }}</span>
|
||
</div>
|
||
<button
|
||
@click="forceClose"
|
||
class="text-xs text-gray-500 hover:text-gray-700 underline"
|
||
>
|
||
Cancel Export
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal Footer -->
|
||
<div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
|
||
<button
|
||
@click="closeModal"
|
||
:disabled="isExporting"
|
||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
@click="handleExport"
|
||
:disabled="isExporting || !fileName.trim()"
|
||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
|
||
>
|
||
<span class="material-icons mr-1 text-sm">download</span>
|
||
Export {{ selectedFormat.toUpperCase() }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch } from 'vue'
|
||
|
||
// Props
|
||
const props = defineProps({
|
||
show: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
processName: {
|
||
type: String,
|
||
default: 'process'
|
||
}
|
||
})
|
||
|
||
// Emits
|
||
const emit = defineEmits(['close', 'export'])
|
||
|
||
// State
|
||
const selectedFormat = ref('png')
|
||
const selectedQuality = ref('2')
|
||
const includeBackground = ref(true)
|
||
const fileName = ref('')
|
||
const isExporting = ref(false)
|
||
const exportProgress = ref('')
|
||
|
||
// PDF specific options
|
||
const pdfPageSize = ref('a4')
|
||
const pdfOrientation = ref('landscape')
|
||
|
||
// Computed
|
||
const exportOptions = computed(() => ({
|
||
format: selectedFormat.value,
|
||
quality: parseInt(selectedQuality.value),
|
||
includeBackground: includeBackground.value,
|
||
fileName: fileName.value.trim(),
|
||
pdf: {
|
||
pageSize: pdfPageSize.value,
|
||
orientation: pdfOrientation.value
|
||
}
|
||
}))
|
||
|
||
// Watch for prop changes to set default filename
|
||
watch(() => props.processName, (newName) => {
|
||
if (newName && !fileName.value) {
|
||
fileName.value = newName.replace(/[^a-zA-Z0-9]/g, '_')
|
||
}
|
||
}, { immediate: true })
|
||
|
||
// Watch format changes to update filename extension
|
||
watch(selectedFormat, (newFormat) => {
|
||
if (fileName.value) {
|
||
const nameWithoutExt = fileName.value.replace(/\.(png|pdf)$/i, '')
|
||
fileName.value = `${nameWithoutExt}.${newFormat}`
|
||
}
|
||
})
|
||
|
||
// Methods
|
||
const closeModal = () => {
|
||
if (!isExporting.value) {
|
||
resetForm()
|
||
emit('close')
|
||
}
|
||
}
|
||
|
||
const resetForm = () => {
|
||
selectedFormat.value = 'png'
|
||
selectedQuality.value = '2'
|
||
includeBackground.value = true
|
||
fileName.value = ''
|
||
isExporting.value = false
|
||
exportProgress.value = ''
|
||
pdfPageSize.value = 'a4'
|
||
pdfOrientation.value = 'landscape'
|
||
}
|
||
|
||
// Force close modal (for error recovery)
|
||
const forceClose = () => {
|
||
resetForm()
|
||
emit('close')
|
||
}
|
||
|
||
const handleExport = async () => {
|
||
if (!fileName.value.trim() || isExporting.value) return
|
||
|
||
try {
|
||
isExporting.value = true
|
||
exportProgress.value = 'Preparing export...'
|
||
|
||
// Update progress during export
|
||
setTimeout(() => {
|
||
if (isExporting.value) {
|
||
exportProgress.value = 'Capturing canvas...'
|
||
}
|
||
}, 500)
|
||
|
||
setTimeout(() => {
|
||
if (isExporting.value) {
|
||
exportProgress.value = 'Processing image...'
|
||
}
|
||
}, 2000)
|
||
|
||
// Emit export event with options
|
||
await emit('export', exportOptions.value)
|
||
|
||
// Success state - ensure we reset the loading state
|
||
exportProgress.value = 'Export completed!'
|
||
isExporting.value = false
|
||
|
||
// Close modal after successful export
|
||
setTimeout(() => {
|
||
closeModal()
|
||
}, 1000)
|
||
|
||
} catch (error) {
|
||
console.error('Export error:', error)
|
||
exportProgress.value = 'Export failed - ' + (error.message || 'Unknown error')
|
||
isExporting.value = false
|
||
setTimeout(() => {
|
||
exportProgress.value = ''
|
||
}, 3000)
|
||
}
|
||
}
|
||
|
||
// Update export progress
|
||
const updateProgress = (message) => {
|
||
exportProgress.value = message
|
||
}
|
||
|
||
// Expose methods for parent component
|
||
defineExpose({
|
||
updateProgress,
|
||
forceClose,
|
||
resetForm
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* Custom styling for better modal appearance */
|
||
.modal-enter-active, .modal-leave-active {
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.modal-enter-from, .modal-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
/* Ensure proper z-index for dropdowns */
|
||
select {
|
||
position: relative;
|
||
z-index: 10;
|
||
}
|
||
|
||
/* Enhanced button styles */
|
||
button {
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
button:hover:not(:disabled) {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
/* Format selection buttons */
|
||
.format-button {
|
||
transition: all 0.2s ease;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.format-button::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: -100%;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||
transition: left 0.5s;
|
||
}
|
||
|
||
.format-button:hover::before {
|
||
left: 100%;
|
||
}
|
||
|
||
/* Quality select styling */
|
||
select:focus {
|
||
ring-width: 2px;
|
||
ring-color: rgb(59 130 246);
|
||
ring-opacity: 0.5;
|
||
}
|
||
|
||
/* Checkbox styling */
|
||
input[type="checkbox"]:checked {
|
||
background-color: rgb(59 130 246);
|
||
border-color: rgb(59 130 246);
|
||
}
|
||
|
||
/* Loading spinner animation */
|
||
@keyframes spin {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.animate-spin {
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
/* Progress indicator styling */
|
||
.export-progress {
|
||
background: linear-gradient(90deg, #3b82f6, #60a5fa, #3b82f6);
|
||
background-size: 200% 100%;
|
||
animation: progressShimmer 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes progressShimmer {
|
||
0% {
|
||
background-position: -200% 0;
|
||
}
|
||
100% {
|
||
background-position: 200% 0;
|
||
}
|
||
}
|
||
|
||
/* Modal backdrop blur effect */
|
||
.fixed.inset-0 {
|
||
backdrop-filter: blur(4px);
|
||
-webkit-backdrop-filter: blur(4px);
|
||
}
|
||
|
||
/* Enhanced modal shadow */
|
||
.bg-white.rounded-lg.shadow-xl {
|
||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
/* File input styling */
|
||
input[type="text"]:focus {
|
||
ring-width: 2px;
|
||
ring-color: rgb(59 130 246);
|
||
ring-opacity: 0.5;
|
||
border-color: rgb(59 130 246);
|
||
}
|
||
|
||
/* Responsive adjustments */
|
||
@media (max-width: 640px) {
|
||
.bg-white.rounded-lg.shadow-xl {
|
||
margin: 1rem;
|
||
max-width: calc(100vw - 2rem);
|
||
}
|
||
|
||
.grid.grid-cols-2 {
|
||
grid-template-columns: 1fr;
|
||
gap: 0.75rem;
|
||
}
|
||
}
|
||
</style> |