|
import api from '@/services' |
|
import { debounce } from 'lodash' |
|
|
|
|
|
|
|
class DataSyncService { |
|
private currentPPTId: string | null = null |
|
private saveTimeout: number | null = null |
|
private isOnline = true |
|
private autoSaveDelay = 300000 |
|
private isInitialized = false |
|
private debouncedSave: any = null |
|
|
|
constructor() { |
|
this.setupNetworkMonitoring() |
|
} |
|
|
|
|
|
async initialize() { |
|
if (this.isInitialized) return |
|
await this.setupAutoSave() |
|
this.isInitialized = true |
|
} |
|
|
|
|
|
setAutoSaveDelay(delay: number) { |
|
this.autoSaveDelay = Math.max(500, delay) |
|
if (this.isInitialized) { |
|
this.setupAutoSave() |
|
} |
|
} |
|
|
|
|
|
getAutoSaveDelay(): number { |
|
return this.autoSaveDelay |
|
} |
|
|
|
|
|
setCurrentPPTId(pptId: string) { |
|
this.currentPPTId = pptId |
|
} |
|
|
|
|
|
private async setupAutoSave() { |
|
if (this.debouncedSave) { |
|
this.debouncedSave.cancel() |
|
} |
|
|
|
this.debouncedSave = debounce(async () => { |
|
await this.savePPT() |
|
}, this.autoSaveDelay) |
|
|
|
|
|
try { |
|
const { useSlidesStore } = await import('@/store') |
|
const slidesStore = useSlidesStore() |
|
slidesStore.$subscribe(() => { |
|
if (this.isOnline && this.currentPPTId) { |
|
this.debouncedSave() |
|
} |
|
}) |
|
} |
|
catch (error) { |
|
|
|
} |
|
} |
|
|
|
|
|
private setupNetworkMonitoring() { |
|
window.addEventListener('online', () => { |
|
this.isOnline = true |
|
|
|
}) |
|
|
|
window.addEventListener('offline', () => { |
|
this.isOnline = false |
|
|
|
}) |
|
} |
|
|
|
|
|
async savePPT(force = false): Promise<boolean> { |
|
|
|
const { useAuthStore, useSlidesStore } = await import('@/store') |
|
|
|
try { |
|
const authStore = useAuthStore() |
|
const slidesStore = useSlidesStore() |
|
|
|
if (!authStore.isLoggedIn) { |
|
|
|
return false |
|
} |
|
|
|
|
|
if (!this.currentPPTId && force) { |
|
try { |
|
const response = await api.createPPT(slidesStore.title || 'Untitled Presentation') |
|
this.currentPPTId = response.pptId |
|
|
|
|
|
if (response.ppt) { |
|
slidesStore.setSlides(response.ppt.slides) |
|
slidesStore.setTitle(response.ppt.title) |
|
slidesStore.setTheme(response.ppt.theme) |
|
} |
|
|
|
|
|
return true |
|
} |
|
catch (createError) { |
|
|
|
return false |
|
} |
|
} |
|
|
|
if (!this.currentPPTId && !force) { |
|
|
|
return false |
|
} |
|
|
|
const pptData = { |
|
pptId: this.currentPPTId, |
|
title: slidesStore.title, |
|
slides: slidesStore.slides, |
|
theme: slidesStore.theme, |
|
|
|
viewportSize: slidesStore.viewportSize, |
|
viewportRatio: slidesStore.viewportRatio |
|
} |
|
|
|
await api.savePPT(pptData) |
|
|
|
return true |
|
} |
|
catch (error) { |
|
|
|
return false |
|
} |
|
} |
|
|
|
|
|
async createNewPPT(title: string): Promise<string | null> { |
|
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) { |
|
|
|
throw error |
|
} |
|
} |
|
|
|
|
|
async loadPPT(pptId: string): Promise<boolean> { |
|
const { useAuthStore, useSlidesStore } = await import('@/store') |
|
const authStore = useAuthStore() |
|
const slidesStore = useSlidesStore() |
|
|
|
if (!authStore.isLoggedIn) { |
|
throw new Error('User not logged in') |
|
} |
|
|
|
try { |
|
|
|
const pptData = await api.getPPT(pptId) |
|
|
|
if (!pptData) { |
|
throw new Error('PPT data is empty') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
if (pptData.viewportSize && pptData.viewportSize > 0) { |
|
slidesStore.setViewportSize(pptData.viewportSize) |
|
|
|
} |
|
else { |
|
|
|
slidesStore.setViewportSize(1000) |
|
} |
|
|
|
if (pptData.viewportRatio && pptData.viewportRatio > 0) { |
|
slidesStore.setViewportRatio(pptData.viewportRatio) |
|
|
|
} |
|
else { |
|
|
|
slidesStore.setViewportRatio(0.5625) |
|
} |
|
|
|
|
|
if (pptData.theme) { |
|
slidesStore.setTheme(pptData.theme) |
|
|
|
} |
|
|
|
|
|
slidesStore.setTitle(pptData.title || 'Untitled Presentation') |
|
|
|
|
|
|
|
if (Array.isArray(pptData.slides) && pptData.slides.length > 0) { |
|
|
|
const validSlides = pptData.slides.filter((slide: any) => |
|
slide && typeof slide === 'object' && Array.isArray(slide.elements) |
|
) |
|
|
|
if (validSlides.length !== pptData.slides.length) { |
|
|
|
} |
|
|
|
slidesStore.setSlides(validSlides) |
|
|
|
} |
|
else { |
|
|
|
slidesStore.setSlides([{ |
|
id: 'default-slide', |
|
elements: [], |
|
background: { type: 'solid', color: '#ffffff' } |
|
}]) |
|
} |
|
|
|
|
|
const actualPptId = pptData.id || pptData.pptId || pptId |
|
this.setCurrentPPTId(actualPptId) |
|
|
|
|
|
|
|
return true |
|
} |
|
catch (error: any) { |
|
|
|
|
|
|
|
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}`) |
|
} |
|
} |
|
|
|
|
|
async getPPTList() { |
|
const { useAuthStore } = await import('@/store') |
|
const authStore = useAuthStore() |
|
|
|
if (!authStore.isLoggedIn) { |
|
throw new Error('User not logged in') |
|
} |
|
|
|
return await api.getPPTList() |
|
} |
|
|
|
|
|
async deletePPT(pptId: string): Promise<boolean> { |
|
const { useAuthStore } = await import('@/store') |
|
const authStore = useAuthStore() |
|
|
|
if (!authStore.isLoggedIn) { |
|
throw new Error('User not logged in') |
|
} |
|
|
|
try { |
|
await api.deletePPT(pptId) |
|
|
|
|
|
if (this.currentPPTId === pptId) { |
|
this.currentPPTId = null |
|
} |
|
|
|
|
|
return true |
|
} |
|
catch (error) { |
|
|
|
throw error |
|
} |
|
} |
|
|
|
|
|
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) { |
|
|
|
throw error |
|
} |
|
} |
|
|
|
|
|
async manualSave(): Promise<boolean> { |
|
return await this.savePPT(true) |
|
} |
|
|
|
|
|
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 |
|
|
|
|
|
console.log('๐ Using new frontend screenshot strategy...') |
|
|
|
try { |
|
|
|
const dataResponse = await fetch(`${baseUrl}/api/public/screenshot-data/${authStore.currentUser!.id}/${this.currentPPTId}/${slideIndex}`) |
|
|
|
if (dataResponse.ok) { |
|
const screenshotData = await dataResponse.json() |
|
|
|
|
|
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) |
|
} |
|
|
|
|
|
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 { |
|
|
|
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 |
|
} |
|
} |
|
} |
|
|
|
|
|
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') |
|
} |
|
|
|
|
|
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(', ')}`) |
|
} |
|
|
|
|
|
const html2canvas = await this.loadHtml2Canvas() |
|
|
|
console.log('๐ผ๏ธ Starting to generate image for upload...', { format, quality }) |
|
|
|
|
|
await this.ensureResourcesReady() |
|
|
|
|
|
const canvas = await html2canvas(targetElement, { |
|
scale: 2, |
|
useCORS: true, |
|
allowTaint: true, |
|
backgroundColor: slidesStore.theme?.backgroundColor || '#ffffff', |
|
logging: false, |
|
onclone: function(clonedDoc) { |
|
|
|
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' |
|
} |
|
}) |
|
} |
|
}) |
|
|
|
|
|
const imageDataUrl = canvas.toDataURL(`image/${format}`, quality / 100) |
|
|
|
console.log('โ
Image generated, uploading 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 |
|
} |
|
} |
|
|
|
|
|
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}`) |
|
} |
|
} |
|
|
|
|
|
async generateDirectScreenshot(slideIndex = 0, options: any = {}) { |
|
try { |
|
const { useSlidesStore } = await import('@/store') |
|
const slidesStore = useSlidesStore() |
|
|
|
|
|
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 |
|
|
|
|
|
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(', ')}`) |
|
} |
|
|
|
|
|
const html2canvas = await this.loadHtml2Canvas() |
|
|
|
console.log('๐ผ๏ธ Starting to generate direct screenshot...', { width, height, quality }) |
|
|
|
|
|
await this.ensureResourcesReady() |
|
|
|
|
|
const canvas = await html2canvas(targetElement, { |
|
width, |
|
height, |
|
scale: 2, |
|
useCORS: true, |
|
allowTaint: true, |
|
backgroundColor: slidesStore.theme?.backgroundColor || '#ffffff', |
|
logging: false, |
|
onclone: function(clonedDoc) { |
|
|
|
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' |
|
} |
|
}) |
|
} |
|
}) |
|
|
|
|
|
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) |
|
|
|
|
|
console.log('๐ Falling back to frontend export page solution...') |
|
return await this.generateScreenshotUrl(slideIndex, options) |
|
} |
|
} |
|
|
|
|
|
private async loadHtml2Canvas(): Promise<any> { |
|
|
|
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/[email protected]/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) |
|
}) |
|
} |
|
|
|
|
|
private async ensureResourcesReady(): Promise<void> { |
|
|
|
if (document.fonts && document.fonts.ready) { |
|
await document.fonts.ready |
|
} |
|
|
|
|
|
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) |
|
setTimeout(() => resolve(void 0), 2000) |
|
}) |
|
}) |
|
|
|
await Promise.all(imagePromises) |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 300)) |
|
} |
|
|
|
|
|
async generateFrontendScreenshot(elementId = 'slideContainer', options: any = {}) { |
|
console.warn('โ ๏ธ generateFrontendScreenshot is deprecated, use generateDirectScreenshot instead') |
|
return await this.generateDirectScreenshot(0, { ...options, elementSelector: `#${elementId}` }) |
|
} |
|
|
|
|
|
ensureElementReady(element: HTMLElement) { |
|
console.warn('โ ๏ธ ensureElementReady is deprecated, now using ensureResourcesReady') |
|
return this.ensureResourcesReady() |
|
} |
|
|
|
|
|
generateFallbackScreenshot(width = 1000, height = 562) { |
|
const canvas = document.createElement('canvas') |
|
canvas.width = width |
|
canvas.height = height |
|
const ctx = canvas.getContext('2d') |
|
|
|
if (!ctx) return '' |
|
|
|
|
|
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) |
|
|
|
|
|
ctx.strokeStyle = '#dee2e6' |
|
ctx.lineWidth = 3 |
|
ctx.setLineDash([15, 10]) |
|
ctx.strokeRect(20, 20, width - 40, height - 40) |
|
|
|
|
|
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) |
|
} |
|
} |
|
|
|
|
|
export const dataSyncService = new DataSyncService() |
|
export default dataSyncService |