import api from '@/services' import { debounce } from 'lodash' // Using html2canvas instead of html-to-image for better compatibility // import { toJpeg } from 'html-to-image' class DataSyncService { private currentPPTId: string | null = null private saveTimeout: number | null = null private isOnline = true private autoSaveDelay = 300000 // Default 5 minutes, configurable private isInitialized = false private debouncedSave: any = null constructor() { this.setupNetworkMonitoring() } // Delayed initialization, called after Pinia is available async initialize() { if (this.isInitialized) return await this.setupAutoSave() this.isInitialized = true } // Set auto-save delay time (milliseconds) setAutoSaveDelay(delay: number) { this.autoSaveDelay = Math.max(500, delay) // Minimum 500ms if (this.isInitialized) { this.setupAutoSave() // Reset auto-save } } // Get current auto-save delay time getAutoSaveDelay(): number { return this.autoSaveDelay } // Set current PPT ID setCurrentPPTId(pptId: string) { this.currentPPTId = pptId } // Auto-save functionality private async setupAutoSave() { if (this.debouncedSave) { this.debouncedSave.cancel() } this.debouncedSave = debounce(async () => { await this.savePPT() }, this.autoSaveDelay) // Use configurable delay time // Listen for slides changes try { const { useSlidesStore } = await import('@/store') const slidesStore = useSlidesStore() slidesStore.$subscribe(() => { if (this.isOnline && this.currentPPTId) { this.debouncedSave() } }) } catch (error) { // console.warn('Unable to set up auto-save, store not ready:', error) } } // Network status monitoring private setupNetworkMonitoring() { window.addEventListener('online', () => { this.isOnline = true // console.log('Network connected, resuming auto-save') }) window.addEventListener('offline', () => { this.isOnline = false // console.log('Network disconnected, pausing auto-save') }) } // Save PPT to backend async savePPT(force = false): Promise { // Dynamically import store to avoid dependency issues during initialization const { useAuthStore, useSlidesStore } = await import('@/store') try { const authStore = useAuthStore() const slidesStore = useSlidesStore() if (!authStore.isLoggedIn) { // console.warn('User not logged in, unable to save') return false } // If no current PPT ID and forced save, create new PPT if (!this.currentPPTId && force) { try { const response = await api.createPPT(slidesStore.title || 'Untitled Presentation') this.currentPPTId = response.pptId // Update slides store data to match newly created PPT if (response.ppt) { slidesStore.setSlides(response.ppt.slides) slidesStore.setTitle(response.ppt.title) slidesStore.setTheme(response.ppt.theme) } // console.log('Created new PPT and saved successfully') return true } catch (createError) { // console.error('Failed to create new PPT:', createError) return false } } if (!this.currentPPTId && !force) { // console.warn('No current PPT ID') return false } const pptData = { pptId: this.currentPPTId, title: slidesStore.title, slides: slidesStore.slides, theme: slidesStore.theme, // Add critical size information viewportSize: slidesStore.viewportSize, viewportRatio: slidesStore.viewportRatio } await api.savePPT(pptData) // console.log('PPT saved successfully') return true } catch (error) { // console.error('Failed to save PPT:', error) return false } } // Create new PPT async createNewPPT(title: string): Promise { const { useAuthStore } = await import('@/store') const authStore = useAuthStore() if (!authStore.isLoggedIn) { throw new Error('User not logged in') } try { const response = await api.createPPT(title) this.setCurrentPPTId(response.pptId) return response.pptId } catch (error) { // console.error('Failed to create PPT:', error) throw error } } // Load PPT async loadPPT(pptId: string): Promise { const { useAuthStore, useSlidesStore } = await import('@/store') const authStore = useAuthStore() const slidesStore = useSlidesStore() if (!authStore.isLoggedIn) { throw new Error('User not logged in') } try { // console.log(`🔍 Starting to load PPT: ${pptId}`) const pptData = await api.getPPT(pptId) if (!pptData) { throw new Error('PPT data is empty') } // console.log('📋 PPT data loaded successfully, updating store:', {...}) // Fix: Ensure complete PPT data is loaded and set in correct order // 1. First set viewport information to ensure correct canvas size if (pptData.viewportSize && pptData.viewportSize > 0) { slidesStore.setViewportSize(pptData.viewportSize) // console.log('✅ Viewport size set successfully:', pptData.viewportSize) } else { // console.log('⚠️ Using default viewport size: 1000') slidesStore.setViewportSize(1000) } if (pptData.viewportRatio && pptData.viewportRatio > 0) { slidesStore.setViewportRatio(pptData.viewportRatio) // console.log('✅ Viewport ratio set successfully:', pptData.viewportRatio) } else { // console.log('⚠️ Using default viewport ratio: 0.5625') slidesStore.setViewportRatio(0.5625) } // 2. Set theme if (pptData.theme) { slidesStore.setTheme(pptData.theme) // console.log('✅ Theme set successfully') } // 3. Set title slidesStore.setTitle(pptData.title || 'Untitled Presentation') // console.log('✅ Title set successfully:', pptData.title) // 4. Finally set slides, ensuring it is done after viewport information is set if (Array.isArray(pptData.slides) && pptData.slides.length > 0) { // Validate slides data integrity const validSlides = pptData.slides.filter((slide: any) => slide && typeof slide === 'object' && Array.isArray(slide.elements) ) if (validSlides.length !== pptData.slides.length) { // console.warn(`⚠️ Found ${pptData.slides.length - validSlides.length} invalid slides, filtered out`) } slidesStore.setSlides(validSlides) // console.log('✅ Slides set successfully:', validSlides.length, 'pages') } else { // console.warn('⚠️ PPT has no valid slides, creating default slide') slidesStore.setSlides([{ id: 'default-slide', elements: [], background: { type: 'solid', color: '#ffffff' } }]) } // 5. Set current PPT ID const actualPptId = pptData.id || pptData.pptId || pptId this.setCurrentPPTId(actualPptId) // console.log('✅ PPT ID set successfully:', actualPptId) // console.log('🎉 PPT loaded successfully!') return true } catch (error: any) { // console.error('❌ Failed to load PPT:', { pptId, error: error.message, stack: error.stack }) // Provide more specific error information if (error.message?.includes('404') || error.message?.includes('not found')) { throw new Error(`PPT not found (ID: ${pptId})`) } if (error.message?.includes('403') || error.message?.includes('unauthorized')) { throw new Error('No access permission') } if (error.message?.includes('Network')) { throw new Error('Network connection failed, please check network status') } throw new Error(`Failed to load: ${error.message}`) } } // Get PPT list async getPPTList() { const { useAuthStore } = await import('@/store') const authStore = useAuthStore() if (!authStore.isLoggedIn) { throw new Error('User not logged in') } return await api.getPPTList() } // Delete PPT async deletePPT(pptId: string): Promise { const { useAuthStore } = await import('@/store') const authStore = useAuthStore() if (!authStore.isLoggedIn) { throw new Error('User not logged in') } try { await api.deletePPT(pptId) // If the deleted PPT is the current PPT, clear the current PPT ID if (this.currentPPTId === pptId) { this.currentPPTId = null } // console.log('PPT deleted successfully') return true } catch (error) { // console.error('Failed to delete PPT:', error) throw error } } // Generate share link async generateShareLink(slideIndex = 0) { const { useAuthStore } = await import('@/store') const authStore = useAuthStore() if (!authStore.isLoggedIn || !this.currentPPTId) { throw new Error('User not logged in or no current PPT') } try { const response = await api.generateShareLink( authStore.currentUser!.id, this.currentPPTId, slideIndex ) return response } catch (error) { // console.error('Failed to generate share link:', error) throw error } } // Manual save async manualSave(): Promise { return await this.savePPT(true) } // New screenshot solution: use direct image API async generateScreenshotUrl(slideIndex = 0, options: any = {}) { const { useAuth = true, format = 'jpeg', quality = 90 } = options; if (useAuth) { const { useAuthStore } = await import('@/store') const authStore = useAuthStore() if (!authStore.isLoggedIn || !this.currentPPTId) { throw new Error('User not logged in or no current PPT') } const baseUrl = window.location.origin // 🔧 NEW: Use frontend screenshot data API for direct generation console.log('🚀 Using new frontend screenshot strategy...') try { // Get PPT data for frontend generation const dataResponse = await fetch(`${baseUrl}/api/public/screenshot-data/${authStore.currentUser!.id}/${this.currentPPTId}/${slideIndex}`) if (dataResponse.ok) { const screenshotData = await dataResponse.json() // Return data for frontend direct generation return { type: 'frontend-data', data: screenshotData, isDataUrl: false, info: { format, quality, strategy: 'frontend-direct-generation', pptTitle: screenshotData.pptData?.title, slideIndex: screenshotData.slideIndex, dimensions: `${screenshotData.exportConfig?.width}x${screenshotData.exportConfig?.height}` } } } } catch (error) { console.warn('Frontend data API failed, falling back to export page:', error) } // Fallback: Frontend export page (user opens in new tab) const frontendExportUrl = `${baseUrl}/api/public/image/${authStore.currentUser!.id}/${this.currentPPTId}/${slideIndex}?format=${format}&quality=${quality}` return { type: 'frontend-export-page', url: frontendExportUrl, isDataUrl: false, info: { format, quality, strategy: 'frontend-export-page', usage: 'Open this URL in a new tab to generate and download screenshot' } } } else { // Public access mode const baseUrl = window.location.origin return { type: 'frontend-export-page-public', url: `${baseUrl}/api/public/image/public/public/${slideIndex}?format=${format}&quality=${quality}`, isDataUrl: false } } } // Generate image and upload to backend for external link access async generateAndUploadImage(slideIndex = 0, options: any = {}) { const { format = 'jpeg', quality = 90, elementSelector = '.slide-viewport, .canvas-viewport, .viewport-wrapper' } = options; try { const { useAuthStore, useSlidesStore } = await import('@/store') const authStore = useAuthStore() const slidesStore = useSlidesStore() if (!authStore.isLoggedIn || !this.currentPPTId) { throw new Error('User not logged in or no current PPT') } // Find target element for screenshot let targetElement: HTMLElement | null = null const selectors = elementSelector.split(',').map((s: string) => s.trim()) for (const selector of selectors) { targetElement = document.querySelector(selector) as HTMLElement if (targetElement) { console.log(`✅ Found screenshot target element: ${selector}`) break } } if (!targetElement) { throw new Error(`Screenshot target element not found. Tried selectors: ${selectors.join(', ')}`) } // Dynamically load html2canvas const html2canvas = await this.loadHtml2Canvas() console.log('🖼️ Starting to generate image for upload...', { format, quality }) // Wait for fonts and images to load await this.ensureResourcesReady() // Generate screenshot const canvas = await html2canvas(targetElement, { scale: 2, // High resolution useCORS: true, allowTaint: true, backgroundColor: slidesStore.theme?.backgroundColor || '#ffffff', logging: false, onclone: function(clonedDoc) { // Ensure correct fonts in cloned document const clonedElements = clonedDoc.querySelectorAll('*') clonedElements.forEach((el: any) => { if (el.style) { el.style.fontFamily = 'Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, Helvetica, sans-serif' } }) } }) // Convert to specified format const imageDataUrl = canvas.toDataURL(`image/${format}`, quality / 100) console.log('✅ Image generated, uploading to backend...') // Upload to backend const baseUrl = window.location.origin const uploadResponse = await fetch(`${baseUrl}/api/public/save-image/${authStore.currentUser!.id}/${this.currentPPTId}/${slideIndex}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageData: imageDataUrl, format, quality }) }) if (!uploadResponse.ok) { throw new Error(`Upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`) } const uploadResult = await uploadResponse.json() console.log('✅ Image uploaded successfully, external URL:', uploadResult.imageUrl) return { success: true, imageUrl: uploadResult.imageUrl, imageId: uploadResult.imageId, expiresAt: uploadResult.expiresAt, format, quality, width: canvas.width, height: canvas.height } } catch (error: any) { console.error('❌ Failed to generate and upload image:', error) throw error } } // Get PPT screenshot data (for frontend direct rendering) async getScreenshotData(slideIndex = 0) { const { useAuthStore } = await import('@/store') const authStore = useAuthStore() if (!authStore.isLoggedIn || !this.currentPPTId) { throw new Error('User not logged in or no current PPT') } try { const baseUrl = window.location.origin const response = await fetch(`${baseUrl}/api/public/screenshot-data/${authStore.currentUser!.id}/${this.currentPPTId}/${slideIndex}`) if (!response.ok) { throw new Error(`Failed to get screenshot data: ${response.status}`) } const data = await response.json() return data } catch (error: any) { console.error('❌ Failed to get screenshot data:', error) throw new Error(`Failed to get screenshot data: ${error.message}`) } } // Generate screenshot directly in current page (best solution) async generateDirectScreenshot(slideIndex = 0, options: any = {}) { try { const { useSlidesStore } = await import('@/store') const slidesStore = useSlidesStore() // Check if slide data is available if (!slidesStore.slides || slideIndex >= slidesStore.slides.length) { throw new Error('Invalid slide index or no slides available') } const { format = 'jpeg', quality = 90, width = slidesStore.viewportSize || 1000, height = Math.ceil((slidesStore.viewportSize || 1000) * (slidesStore.viewportRatio || 0.562)), elementSelector = '.canvas-main, .slide-container, .editor-main' } = options // Find screenshot target element let targetElement: HTMLElement | null = null const selectors = elementSelector.split(',').map((s: string) => s.trim()) for (const selector of selectors) { targetElement = document.querySelector(selector) as HTMLElement if (targetElement) { console.log(`✅ Found screenshot target element: ${selector}`) break } } if (!targetElement) { throw new Error(`Screenshot target element not found. Tried selectors: ${selectors.join(', ')}`) } // Dynamically load html2canvas const html2canvas = await this.loadHtml2Canvas() console.log('🖼️ Starting to generate direct screenshot...', { width, height, quality }) // Wait for fonts and images to load await this.ensureResourcesReady() // Generate screenshot const canvas = await html2canvas(targetElement, { width, height, scale: 2, // High resolution useCORS: true, allowTaint: true, backgroundColor: slidesStore.theme?.backgroundColor || '#ffffff', logging: false, onclone: function(clonedDoc) { // Ensure correct fonts in cloned document const clonedElements = clonedDoc.querySelectorAll('*') clonedElements.forEach((el: any) => { if (el.style) { el.style.fontFamily = 'Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, Helvetica, sans-serif' } }) } }) // Convert to specified format const imageDataUrl = canvas.toDataURL(`image/${format}`, quality / 100) console.log('✅ Direct screenshot generated successfully, size:', imageDataUrl.length) return { type: 'direct-frontend', url: imageDataUrl, isDataUrl: true, width: canvas.width, height: canvas.height, format, quality } } catch (error: any) { console.error('❌ Failed to generate direct screenshot:', error) // Fallback to frontend export page solution console.log('🔄 Falling back to frontend export page solution...') return await this.generateScreenshotUrl(slideIndex, options) } } // Dynamically load html2canvas library private async loadHtml2Canvas(): Promise { // Check if already loaded if ((window as any).html2canvas) { return (window as any).html2canvas } return new Promise((resolve, reject) => { const script = document.createElement('script') script.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js' script.onload = () => { resolve((window as any).html2canvas) } script.onerror = (error) => { reject(new Error('Failed to load html2canvas library')) } document.head.appendChild(script) }) } // Wait for resources to be ready private async ensureResourcesReady(): Promise { // Wait for font loading if (document.fonts && document.fonts.ready) { await document.fonts.ready } // Wait for image loading const images = document.querySelectorAll('img') const imagePromises = Array.from(images).map(img => { if (img.complete) return Promise.resolve() return new Promise((resolve) => { img.onload = () => resolve(void 0) img.onerror = () => resolve(void 0) // Continue even if failed setTimeout(() => resolve(void 0), 2000) // 2 second timeout }) }) await Promise.all(imagePromises) // Additional wait to ensure rendering is complete await new Promise(resolve => setTimeout(resolve, 300)) } // Simplified frontend screenshot generation (for compatibility) async generateFrontendScreenshot(elementId = 'slideContainer', options: any = {}) { console.warn('⚠️ generateFrontendScreenshot is deprecated, use generateDirectScreenshot instead') return await this.generateDirectScreenshot(0, { ...options, elementSelector: `#${elementId}` }) } // Ensure element is fully rendered (for compatibility) ensureElementReady(element: HTMLElement) { console.warn('⚠️ ensureElementReady is deprecated, now using ensureResourcesReady') return this.ensureResourcesReady() } // Generate frontend fallback screenshot (for compatibility) generateFallbackScreenshot(width = 1000, height = 562) { const canvas = document.createElement('canvas') canvas.width = width canvas.height = height const ctx = canvas.getContext('2d') if (!ctx) return '' // Draw background const gradient = ctx.createLinearGradient(0, 0, width, height) gradient.addColorStop(0, '#f8f9fa') gradient.addColorStop(1, '#e9ecef') ctx.fillStyle = gradient ctx.fillRect(0, 0, width, height) // Draw border ctx.strokeStyle = '#dee2e6' ctx.lineWidth = 3 ctx.setLineDash([15, 10]) ctx.strokeRect(20, 20, width - 40, height - 40) // Optimized font rendering ctx.fillStyle = '#495057' ctx.font = 'bold 32px "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Noto Sans SC", "WenQuanYi Micro Hei", "SimHei", "SimSun", Arial, sans-serif' ctx.textAlign = 'center' ctx.fillText('PPT Preview', width / 2, height * 0.35) ctx.fillStyle = '#6c757d' ctx.font = '18px "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Noto Sans SC", "WenQuanYi Micro Hei", "SimHei", "SimSun", Arial, sans-serif' ctx.fillText('Frontend Screenshot Service', width / 2, height * 0.5) ctx.fillStyle = '#adb5bd' ctx.font = '16px "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Noto Sans SC", "WenQuanYi Micro Hei", "SimHei", "SimSun", Arial, sans-serif' ctx.fillText(`Size: ${width} × ${height}`, width / 2, height * 0.6) return canvas.toDataURL('image/jpeg', 0.9) } } // Create singleton instance export const dataSyncService = new DataSyncService() export default dataSyncService