File size: 8,723 Bytes
10b14be
 
3713bef
 
10b14be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3713bef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10b14be
 
 
 
89ce340
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
import api from '@/services'
import { debounce } from 'lodash'
// 添加前端截图支持
import { toPng, toJpeg, toSvg } from 'html-to-image'

class DataSyncService {
  private currentPPTId: string | null = null
  private saveTimeout: number | null = null
  private isOnline = true
  private autoSaveDelay = 300000 // 默认5分钟,可配置
  private isInitialized = false
  private debouncedSave: any = null

  constructor() {
    this.setupNetworkMonitoring()
  }

  // 延迟初始化,在 Pinia 可用后调用
  async initialize() {
    if (this.isInitialized) return
    await this.setupAutoSave()
    this.isInitialized = true
  }

  // 设置自动保存延迟时间(毫秒)
  setAutoSaveDelay(delay: number) {
    this.autoSaveDelay = Math.max(500, delay) // 最小500ms
    if (this.isInitialized) {
      this.setupAutoSave() // 重新设置自动保存
    }
  }

  // 获取当前自动保存延迟时间
  getAutoSaveDelay(): number {
    return this.autoSaveDelay
  }

  // 设置当前PPT ID
  setCurrentPPTId(pptId: string) {
    this.currentPPTId = pptId
  }

  // 自动保存功能
  private async setupAutoSave() {
    if (this.debouncedSave) {
      this.debouncedSave.cancel()
    }

    this.debouncedSave = debounce(async () => {
      await this.savePPT()
    }, this.autoSaveDelay) // 使用可配置的延迟时间

    // 监听slides变化
    try {
      const { useSlidesStore } = await import('@/store')
      const slidesStore = useSlidesStore()
      slidesStore.$subscribe(() => {
        if (this.isOnline && this.currentPPTId) {
          this.debouncedSave()
        }
      })
    } 
    catch (error) {
      // console.warn('无法设置自动保存,store 未就绪:', error)
    }
  }

  // 网络状态监控
  private setupNetworkMonitoring() {
    window.addEventListener('online', () => {
      this.isOnline = true
      // console.log('网络已连接,恢复自动保存')
    })

    window.addEventListener('offline', () => {
      this.isOnline = false
      // console.log('网络已断开,暂停自动保存')
    })
  }

  // 保存PPT到后端
  async savePPT(force = false): Promise<boolean> {
    // 动态导入 store,避免初始化时的依赖问题
    const { useAuthStore, useSlidesStore } = await import('@/store')
    
    try {
      const authStore = useAuthStore()
      const slidesStore = useSlidesStore()

      if (!authStore.isLoggedIn) {
        // console.warn('用户未登录,无法保存')
        return false
      }

      // 如果没有当前PPT ID且是强制保存,创建新PPT
      if (!this.currentPPTId && force) {
        try {
          const response = await api.createPPT(slidesStore.title || '未命名演示文稿')
          this.currentPPTId = response.pptId
          
          // 更新slides store中的数据以匹配新创建的PPT
          if (response.ppt) {
            slidesStore.setSlides(response.ppt.slides)
            slidesStore.setTitle(response.ppt.title)
            slidesStore.setTheme(response.ppt.theme)
          }
          
          // console.log('创建新PPT并保存成功')
          return true
        } 
        catch (createError) {
          // console.error('创建新PPT失败:', createError)
          return false
        }
      }

      if (!this.currentPPTId && !force) {
        // console.warn('没有当前PPT ID')
        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)
      // console.log('PPT保存成功')
      return true
    } 
    catch (error) {
      // console.error('PPT保存失败:', error)
      return false
    }
  }

  // 创建新PPT
  async createNewPPT(title: string): Promise<string | null> {
    const { useAuthStore } = await import('@/store')
    const authStore = useAuthStore()

    if (!authStore.isLoggedIn) {
      throw new Error('用户未登录')
    }

    try {
      const response = await api.createPPT(title)
      this.setCurrentPPTId(response.pptId)
      return response.pptId
    } 
    catch (error) {
      // console.error('创建PPT失败:', error)
      throw error
    }
  }

  // 加载PPT
  async loadPPT(pptId: string): Promise<boolean> {
    const { useAuthStore, useSlidesStore } = await import('@/store')
    const authStore = useAuthStore()
    const slidesStore = useSlidesStore()

    if (!authStore.isLoggedIn) {
      throw new Error('用户未登录')
    }

    try {
      const pptData = await api.getPPT(pptId)
      
      slidesStore.setSlides(pptData.slides)
      slidesStore.setTitle(pptData.title)
      if (pptData.theme) {
        slidesStore.setTheme(pptData.theme)
      }
      
      this.setCurrentPPTId(pptId)
      // console.log('PPT加载成功')
      return true
    } 
    catch (error) {
      // console.error('PPT加载失败:', error)
      throw error
    }
  }

  // 获取PPT列表
  async getPPTList() {
    const { useAuthStore } = await import('@/store')
    const authStore = useAuthStore()

    if (!authStore.isLoggedIn) {
      throw new Error('用户未登录')
    }

    return await api.getPPTList()
  }

  // 删除PPT
  async deletePPT(pptId: string): Promise<boolean> {
    const { useAuthStore } = await import('@/store')
    const authStore = useAuthStore()

    if (!authStore.isLoggedIn) {
      throw new Error('用户未登录')
    }

    try {
      await api.deletePPT(pptId)
      
      // 如果删除的是当前PPT,清除当前PPT ID
      if (this.currentPPTId === pptId) {
        this.currentPPTId = null
      }
      
      // console.log('PPT删除成功')
      return true
    } 
    catch (error) {
      // console.error('PPT删除失败:', error)
      throw error
    }
  }

  // 生成分享链接
  async generateShareLink(slideIndex = 0) {
    const { useAuthStore } = await import('@/store')
    const authStore = useAuthStore()

    if (!authStore.isLoggedIn || !this.currentPPTId) {
      throw new Error('用户未登录或没有当前PPT')
    }

    try {
      const response = await api.generateShareLink(
        authStore.currentUser!.id,
        this.currentPPTId,
        slideIndex
      )
      return response
    } 
    catch (error) {
      // console.error('生成分享链接失败:', error)
      throw error
    }
  }

  // 手动保存
  async manualSave(): Promise<boolean> {
    return await this.savePPT(true)
  }

  // 前端截图功能 - 作为后端截图的备用方案
  async generateFrontendScreenshot(elementId = 'slideContainer', options = {}) {
    try {
      const element = document.getElementById(elementId)
      if (!element) {
        throw new Error(`Element with id '${elementId}' not found`)
      }

      const defaultOptions = {
        quality: 0.95,
        pixelRatio: 1,
        backgroundColor: '#ffffff',
        ...options
      }

      console.log('🖼️ 开始前端截图生成...')
      
      // 使用html-to-image生成截图
      const imageDataUrl = await toJpeg(element, defaultOptions)
      
      console.log('✅ 前端截图生成成功')
      return imageDataUrl
    } catch (error) {
      console.error('❌ 前端截图生成失败:', error)
      throw error
    }
  }

  // 生成PPT截图链接(优先使用后端,失败时尝试前端)
  async generateScreenshotUrl(slideIndex = 0, useFrontend = false) {
    const { useAuthStore } = await import('@/store')
    const authStore = useAuthStore()

    if (!authStore.isLoggedIn || !this.currentPPTId) {
      throw new Error('用户未登录或没有当前PPT')
    }

    const baseUrl = window.location.origin
    const backendScreenshotUrl = `${baseUrl}/api/public/screenshot/${authStore.currentUser!.id}/${this.currentPPTId}/${slideIndex}`

    if (useFrontend) {
      try {
        // 尝试前端截图
        const frontendScreenshot = await this.generateFrontendScreenshot()
        return {
          type: 'frontend',
          url: frontendScreenshot,
          isDataUrl: true
        }
      } catch (frontendError) {
        console.warn('前端截图失败,回退到后端URL:', frontendError.message)
        return {
          type: 'backend',
          url: backendScreenshotUrl,
          isDataUrl: false
        }
      }
    }

    return {
      type: 'backend',
      url: backendScreenshotUrl,
      isDataUrl: false
    }
  }
}

// 创建单例实例
export const dataSyncService = new DataSyncService()
export default dataSyncService