jvs-ppt-ui 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/jvs-ppt-ui.es.js +135068 -112688
- package/dist/lib/jvs-ppt-ui.umd.js +101 -101
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/components/CanvasArea.vue +51 -11
- package/src/components/Header.vue +31 -20
- package/src/components/PptEditor.vue +1040 -1005
- package/src/components/PresentationMode.vue +3 -1
- package/src/components/SidebarLeft.vue +14 -7
- package/src/components/sidebar/panels/SidebarDesignPanel.vue +1341 -1336
- package/src/composables/canvas/useCanvasTable.ts +183 -146
- package/src/composables/sidebar/useSidebarTextFormat.ts +1 -1
- package/src/composables/useSlides.ts +231 -34
- package/src/index.ts +2 -1
- package/dist/lib/__vite-browser-external-CbV4tiB6.js +0 -7
- package/dist/lib/dist-DkuYVyNR.js +0 -16010
- package/dist/lib/es-CfdgxdYp.js +0 -477
- package/dist/lib/jszip.min-Kba9Mb0l.js +0 -3034
- package/dist/lib/pptxgen.es-D0HrgUe_.js +0 -2352
|
@@ -1,1005 +1,1040 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, computed, watch, onMounted, nextTick, useSlots, getCurrentInstance } from 'vue'
|
|
3
|
-
import jvsPickerColorV3 from 'jvs-picker-color-v3'
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import ToastNotification from './ToastNotification.vue'
|
|
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
|
-
(e: '
|
|
55
|
-
(e: '
|
|
56
|
-
(e: '
|
|
57
|
-
(e: '
|
|
58
|
-
(e: '
|
|
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
|
-
() => props.
|
|
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
|
-
{ immediate: true }
|
|
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
|
-
function
|
|
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
|
-
const
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
detectedType = '
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
detectedType = '
|
|
484
|
-
|
|
485
|
-
detectedType = '
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (
|
|
587
|
-
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
()
|
|
766
|
-
(
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
zoomIn,
|
|
854
|
-
zoomOut,
|
|
855
|
-
resetZoom,
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
<
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, useSlots, getCurrentInstance, defineAsyncComponent, type Ref } from 'vue'
|
|
3
|
+
import jvsPickerColorV3 from 'jvs-picker-color-v3'
|
|
4
|
+
import '../assets/iconfont/iconfont.js'
|
|
5
|
+
import { saveAs } from 'file-saver'
|
|
6
|
+
import Header from './Header.vue'
|
|
7
|
+
import Toolbar from './Toolbar.vue'
|
|
8
|
+
import SidebarLeft from './SidebarLeft.vue'
|
|
9
|
+
import CanvasArea from './CanvasArea.vue'
|
|
10
|
+
import SidebarRight from './SidebarRight.vue'
|
|
11
|
+
import ToastNotification from './ToastNotification.vue'
|
|
12
|
+
// 大弹层/放映视图异步化:不进首屏主包,首次打开才加载
|
|
13
|
+
const PresentationMode = defineAsyncComponent(() => import('./PresentationMode.vue'))
|
|
14
|
+
const FormulaDialog = defineAsyncComponent(() => import('./FormulaDialog.vue'))
|
|
15
|
+
const DslInputDialog = defineAsyncComponent(() => import('./DslInputDialog.vue'))
|
|
16
|
+
const AIPptDialog = defineAsyncComponent(() => import('./AIPptDialog.vue'))
|
|
17
|
+
const UnsplashImageDialog = defineAsyncComponent(() => import('./UnsplashImageDialog.vue'))
|
|
18
|
+
|
|
19
|
+
// 自动检测并兼容单组件 import { PptEditor } 局部引用的场景(若宿主未调用 app.use(JvsPptUi) 则静默自动装配)
|
|
20
|
+
// Element Plus 组件已由构建期按需解析(unplugin-vue-components),无需运行时全局注册
|
|
21
|
+
const currentApp = getCurrentInstance()?.appContext?.app
|
|
22
|
+
if (currentApp) {
|
|
23
|
+
const registeredComponents = currentApp._context?.components || {}
|
|
24
|
+
if (!registeredComponents['jvs-picker-color-v3']) {
|
|
25
|
+
currentApp.use(jvsPickerColorV3)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
import { useSlides } from '../composables/useSlides'
|
|
30
|
+
import type { Slide, SlideMaster, DeckData, PptEditorProps, PptEditorApi, SaveOptions, LoadSourceOptions, LoadSourceResult } from '../types'
|
|
31
|
+
|
|
32
|
+
const props = withDefaults(defineProps<PptEditorProps>(), {
|
|
33
|
+
modelValue: null,
|
|
34
|
+
readonly: false,
|
|
35
|
+
showHeader: true,
|
|
36
|
+
showSidebarLeft: true,
|
|
37
|
+
showSidebarRight: true,
|
|
38
|
+
enableMcp: false,
|
|
39
|
+
enableAi: undefined,
|
|
40
|
+
aiApiUrl: '',
|
|
41
|
+
aiHeaders: () => ({}),
|
|
42
|
+
enableMaterial: undefined,
|
|
43
|
+
materialApiUrl: '',
|
|
44
|
+
materialListApiUrl: '',
|
|
45
|
+
materialTreeApiUrl: '',
|
|
46
|
+
materialPageApiUrl: '',
|
|
47
|
+
materialTypeApiUrl: '',
|
|
48
|
+
materialSaveApiUrl: '',
|
|
49
|
+
materialHeaders: () => ({}),
|
|
50
|
+
config: () => ({})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const emit = defineEmits<{
|
|
54
|
+
(e: 'update:modelValue', value: DeckData | Slide[]): void
|
|
55
|
+
(e: 'change', payload: { slides: Slide[]; activeIndex: number; title: string }): void
|
|
56
|
+
(e: 'save', payload: any): void
|
|
57
|
+
(e: 'load', payload: LoadSourceResult): void
|
|
58
|
+
(e: 'export', payload: { format: string; title: string; data?: any }): void
|
|
59
|
+
(e: 'slideChange', payload: { index: number; slide: Slide | null }): void
|
|
60
|
+
(e: 'ready', api: PptEditorApi): void
|
|
61
|
+
}>()
|
|
62
|
+
|
|
63
|
+
const slidesState = useSlides()
|
|
64
|
+
const {
|
|
65
|
+
slides,
|
|
66
|
+
activeIndex,
|
|
67
|
+
activeSlide,
|
|
68
|
+
presentationTitle,
|
|
69
|
+
slideMasters,
|
|
70
|
+
themeClrScheme,
|
|
71
|
+
slideRatio,
|
|
72
|
+
slideHeight,
|
|
73
|
+
logicalWidth,
|
|
74
|
+
logicalHeight,
|
|
75
|
+
formulaDialogVisible,
|
|
76
|
+
dslInputDialogVisible,
|
|
77
|
+
aiPptDialogVisible,
|
|
78
|
+
unsplashDialogVisible,
|
|
79
|
+
isPresenting,
|
|
80
|
+
importSlides,
|
|
81
|
+
exportSlides,
|
|
82
|
+
currentEditingFormulaEl,
|
|
83
|
+
addElement,
|
|
84
|
+
notify,
|
|
85
|
+
activeTab,
|
|
86
|
+
undo,
|
|
87
|
+
redo,
|
|
88
|
+
canUndo,
|
|
89
|
+
canRedo,
|
|
90
|
+
addSlide,
|
|
91
|
+
deleteSlide,
|
|
92
|
+
selectSlide,
|
|
93
|
+
startPresentation,
|
|
94
|
+
stopPresentation,
|
|
95
|
+
zoomIn,
|
|
96
|
+
zoomOut,
|
|
97
|
+
resetZoom,
|
|
98
|
+
setAiConfig,
|
|
99
|
+
isAiBeautifyEnabled,
|
|
100
|
+
setMaterialConfig,
|
|
101
|
+
isMaterialEnabled,
|
|
102
|
+
saveCurrentSlideToMaterial,
|
|
103
|
+
isCanvasDragging,
|
|
104
|
+
pauseHistory,
|
|
105
|
+
setCustomUploadHandler,
|
|
106
|
+
getCustomUploadHandler,
|
|
107
|
+
uploadMediaResourcesInSlides
|
|
108
|
+
} = slidesState
|
|
109
|
+
|
|
110
|
+
// 弹层挂载闩锁:首次 visible 后保持挂载(异步组件只加载一次,关闭动画不被 v-if 掐断)
|
|
111
|
+
function latchMount(source: Ref<boolean>) {
|
|
112
|
+
const opened = ref(false)
|
|
113
|
+
watch(source, v => { if (v) opened.value = true })
|
|
114
|
+
return computed(() => source.value || opened.value)
|
|
115
|
+
}
|
|
116
|
+
const showFormulaDialog = latchMount(formulaDialogVisible)
|
|
117
|
+
const showDslInputDialog = latchMount(dslInputDialogVisible)
|
|
118
|
+
const showAiPptDialog = latchMount(aiPptDialogVisible)
|
|
119
|
+
const showUnsplashDialog = latchMount(unsplashDialogVisible)
|
|
120
|
+
|
|
121
|
+
// 标志位:防止外部 v-model 变化与内部 watch 产生互相覆盖的回环
|
|
122
|
+
let isInternalUpdating = false
|
|
123
|
+
let lastEmittedModelValue: any = null
|
|
124
|
+
|
|
125
|
+
// 监听外部传入的 AI 相关配置(优先 props,次选 config 对象)
|
|
126
|
+
watch(
|
|
127
|
+
[
|
|
128
|
+
() => props.aiApiUrl,
|
|
129
|
+
() => props.config?.aiApiUrl,
|
|
130
|
+
() => props.enableAi,
|
|
131
|
+
() => props.config?.enableAi,
|
|
132
|
+
() => props.aiHeaders,
|
|
133
|
+
() => props.config?.aiHeaders
|
|
134
|
+
],
|
|
135
|
+
() => {
|
|
136
|
+
const url = props.aiApiUrl || props.config?.aiApiUrl || ''
|
|
137
|
+
const headers = { ...(props.config?.aiHeaders || {}), ...(props.aiHeaders || {}) }
|
|
138
|
+
// 若显式传入 enableAi,则以其为准;若传了 url 则开启;未指定时保留 undefined 触发默认策略
|
|
139
|
+
const rawEnabled = props.enableAi !== undefined ? props.enableAi : props.config?.enableAi
|
|
140
|
+
const enabled = rawEnabled !== undefined ? rawEnabled : (url ? true : undefined)
|
|
141
|
+
setAiConfig({ apiUrl: url, headers, enable: enabled })
|
|
142
|
+
},
|
|
143
|
+
{ immediate: true, deep: true }
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// 监听外部传入的素材库相关配置(优先 props,次选 config 对象)
|
|
147
|
+
watch(
|
|
148
|
+
[
|
|
149
|
+
() => props.materialApiUrl,
|
|
150
|
+
() => props.config?.materialApiUrl,
|
|
151
|
+
() => props.materialListApiUrl,
|
|
152
|
+
() => props.config?.materialListApiUrl,
|
|
153
|
+
() => props.materialTreeApiUrl,
|
|
154
|
+
() => props.config?.materialTreeApiUrl,
|
|
155
|
+
() => props.materialPageApiUrl,
|
|
156
|
+
() => props.config?.materialPageApiUrl,
|
|
157
|
+
() => props.materialTypeApiUrl,
|
|
158
|
+
() => props.config?.materialTypeApiUrl,
|
|
159
|
+
() => props.materialSaveApiUrl,
|
|
160
|
+
() => props.config?.materialSaveApiUrl,
|
|
161
|
+
() => props.enableMaterial,
|
|
162
|
+
() => props.config?.enableMaterial,
|
|
163
|
+
() => props.materialHeaders,
|
|
164
|
+
() => props.config?.materialHeaders
|
|
165
|
+
],
|
|
166
|
+
() => {
|
|
167
|
+
const listUrl = props.materialListApiUrl || props.config?.materialListApiUrl || props.materialApiUrl || props.config?.materialApiUrl || ''
|
|
168
|
+
const treeUrl = props.materialTreeApiUrl || props.config?.materialTreeApiUrl || listUrl || ''
|
|
169
|
+
const pageUrl = props.materialPageApiUrl || props.config?.materialPageApiUrl || ''
|
|
170
|
+
const typeUrl = props.materialTypeApiUrl || props.config?.materialTypeApiUrl || ''
|
|
171
|
+
const saveUrl = props.materialSaveApiUrl || props.config?.materialSaveApiUrl || ''
|
|
172
|
+
const headers = { ...(props.config?.materialHeaders || {}), ...(props.materialHeaders || {}) }
|
|
173
|
+
// 只要配置了获取内容的 API,且未显式关闭,就展示并激活素材功能
|
|
174
|
+
const rawEnabled = props.enableMaterial !== undefined ? props.enableMaterial : props.config?.enableMaterial
|
|
175
|
+
const enabled = rawEnabled !== undefined ? rawEnabled : ((treeUrl || pageUrl || listUrl) ? true : undefined)
|
|
176
|
+
setMaterialConfig({
|
|
177
|
+
listApiUrl: listUrl,
|
|
178
|
+
apiUrl: listUrl,
|
|
179
|
+
treeApiUrl: treeUrl,
|
|
180
|
+
pageApiUrl: pageUrl,
|
|
181
|
+
typeApiUrl: typeUrl,
|
|
182
|
+
saveApiUrl: saveUrl,
|
|
183
|
+
headers,
|
|
184
|
+
enable: enabled
|
|
185
|
+
})
|
|
186
|
+
},
|
|
187
|
+
{ immediate: true, deep: true }
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
// 监听外部传入的自定义资源上传函数(优先 props,次选 config 对象)
|
|
191
|
+
watch(
|
|
192
|
+
[
|
|
193
|
+
() => props.customUpload,
|
|
194
|
+
() => props.uploadHandler,
|
|
195
|
+
() => props.config?.customUpload,
|
|
196
|
+
() => props.config?.uploadHandler
|
|
197
|
+
],
|
|
198
|
+
() => {
|
|
199
|
+
const handler = props.customUpload || props.uploadHandler || props.config?.customUpload || props.config?.uploadHandler || null
|
|
200
|
+
setCustomUploadHandler(handler)
|
|
201
|
+
},
|
|
202
|
+
{ immediate: true }
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// 初始化标题
|
|
206
|
+
if (props.config?.title) {
|
|
207
|
+
presentationTitle.value = props.config.title
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 获取当前完整 Deck 工程数据
|
|
211
|
+
function getDeck(): DeckData {
|
|
212
|
+
return {
|
|
213
|
+
title: presentationTitle.value || '未命名演示文稿',
|
|
214
|
+
slides: JSON.parse(JSON.stringify(slides.value || [])),
|
|
215
|
+
slideMasters: JSON.parse(JSON.stringify(slideMasters.value || [])),
|
|
216
|
+
themeClrScheme: themeClrScheme.value,
|
|
217
|
+
slideRatio: slideRatio.value,
|
|
218
|
+
slideHeight: slideHeight.value,
|
|
219
|
+
logicalWidth: logicalWidth.value,
|
|
220
|
+
logicalHeight: logicalHeight.value
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 获取幻灯片列表
|
|
225
|
+
function getSlides(): Slide[] {
|
|
226
|
+
return JSON.parse(JSON.stringify(slides.value || []))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 设置/替换当前幻灯片列表
|
|
230
|
+
function setSlides(newSlides: Slide[]) {
|
|
231
|
+
if (!newSlides || !Array.isArray(newSlides)) return
|
|
232
|
+
isInternalUpdating = true
|
|
233
|
+
importSlides(newSlides)
|
|
234
|
+
nextTick(() => {
|
|
235
|
+
isInternalUpdating = false
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 加载完整 Deck 工程数据
|
|
240
|
+
function loadDeck(deck: DeckData | Slide[]) {
|
|
241
|
+
if (!deck) return
|
|
242
|
+
isInternalUpdating = true
|
|
243
|
+
if (Array.isArray(deck)) {
|
|
244
|
+
importSlides(deck)
|
|
245
|
+
} else {
|
|
246
|
+
if (deck.title) {
|
|
247
|
+
presentationTitle.value = deck.title
|
|
248
|
+
}
|
|
249
|
+
if (deck.slideMasters && Array.isArray(deck.slideMasters)) {
|
|
250
|
+
slideMasters.value = deck.slideMasters
|
|
251
|
+
}
|
|
252
|
+
if (deck.slides && Array.isArray(deck.slides)) {
|
|
253
|
+
importSlides(deck.slides)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
nextTick(() => {
|
|
257
|
+
isInternalUpdating = false
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const headerRef = ref<any>(null)
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 直接获取 JSON 格式的 Blob 对象
|
|
265
|
+
*/
|
|
266
|
+
function getSaveJsonBlob(): Blob {
|
|
267
|
+
const deck = getDeck()
|
|
268
|
+
return new Blob([JSON.stringify(deck, null, 2)], { type: 'application/json' })
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 直接获取带文件名的 JSON File 对象
|
|
273
|
+
*/
|
|
274
|
+
function getSaveJsonFile(filename?: string): File {
|
|
275
|
+
const blob = getSaveJsonBlob()
|
|
276
|
+
const name = `${filename || presentationTitle.value || 'presentation'}.json`
|
|
277
|
+
return new File([blob], name, { type: 'application/json' })
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 异步生成并获取可编辑的 PPTX 二进制 Blob 对象
|
|
282
|
+
*/
|
|
283
|
+
async function getSavePptxBlob(): Promise<Blob | null> {
|
|
284
|
+
if (headerRef.value?.generatePptxBlob) {
|
|
285
|
+
return await headerRef.value.generatePptxBlob()
|
|
286
|
+
}
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 异步生成并获取带文件名的 PPTX File 对象
|
|
292
|
+
*/
|
|
293
|
+
async function getSavePptxFile(filename?: string): Promise<File | null> {
|
|
294
|
+
const blob = await getSavePptxBlob()
|
|
295
|
+
if (!blob) return null
|
|
296
|
+
const name = `${filename || presentationTitle.value || 'presentation'}.pptx`
|
|
297
|
+
return new File([blob], name, {
|
|
298
|
+
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 核心保存 API:供外部使用方直接调用获取保存内容
|
|
304
|
+
* @param options 保存配置
|
|
305
|
+
* @returns 对应格式的内容 (DeckData | Blob | File)
|
|
306
|
+
*/
|
|
307
|
+
async function save(options?: SaveOptions): Promise<any> {
|
|
308
|
+
const format = options?.format || 'json'
|
|
309
|
+
const customName = options?.filename || presentationTitle.value || 'presentation'
|
|
310
|
+
const currentDeck = getDeck()
|
|
311
|
+
|
|
312
|
+
let result: any = null
|
|
313
|
+
|
|
314
|
+
if (format === 'json' || format === 'deck') {
|
|
315
|
+
result = currentDeck
|
|
316
|
+
if (options?.autoDownload) {
|
|
317
|
+
const blob = getSaveJsonBlob()
|
|
318
|
+
saveAs(blob, `${customName}.json`)
|
|
319
|
+
}
|
|
320
|
+
} else if (format === 'json-blob') {
|
|
321
|
+
result = getSaveJsonBlob()
|
|
322
|
+
if (options?.autoDownload) {
|
|
323
|
+
saveAs(result, `${customName}.json`)
|
|
324
|
+
}
|
|
325
|
+
} else if (format === 'json-file') {
|
|
326
|
+
result = getSaveJsonFile(customName)
|
|
327
|
+
if (options?.autoDownload) {
|
|
328
|
+
saveAs(result, result.name)
|
|
329
|
+
}
|
|
330
|
+
} else if (format === 'pptx' || format === 'pptx-blob') {
|
|
331
|
+
const blob = await getSavePptxBlob()
|
|
332
|
+
result = blob
|
|
333
|
+
if (blob && options?.autoDownload) {
|
|
334
|
+
saveAs(blob, `${customName}.pptx`)
|
|
335
|
+
}
|
|
336
|
+
} else if (format === 'pptx-file') {
|
|
337
|
+
const file = await getSavePptxFile(customName)
|
|
338
|
+
result = file
|
|
339
|
+
if (file && options?.autoDownload) {
|
|
340
|
+
saveAs(file, file.name)
|
|
341
|
+
}
|
|
342
|
+
} else if (format === 'images-zip') {
|
|
343
|
+
if (headerRef.value?.generateImagesZipBlob) {
|
|
344
|
+
const zipBlob = await headerRef.value.generateImagesZipBlob()
|
|
345
|
+
result = zipBlob
|
|
346
|
+
if (zipBlob && options?.autoDownload) {
|
|
347
|
+
saveAs(zipBlob, `${customName}.zip`)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
result = currentDeck
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 抛出保存事件,携带最终产物与工程元数据
|
|
355
|
+
emit('save', {
|
|
356
|
+
format,
|
|
357
|
+
data: result,
|
|
358
|
+
deck: currentDeck
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
return result
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 导出相关方法
|
|
365
|
+
async function exportPptx() {
|
|
366
|
+
if (headerRef.value?.downloadAsPptx) {
|
|
367
|
+
await headerRef.value.downloadAsPptx()
|
|
368
|
+
} else {
|
|
369
|
+
exportJson()
|
|
370
|
+
}
|
|
371
|
+
emit('export', { format: 'pptx', title: presentationTitle.value })
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function exportJson() {
|
|
375
|
+
exportSlides()
|
|
376
|
+
emit('export', { format: 'json', title: presentationTitle.value })
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function exportImages() {
|
|
380
|
+
if (headerRef.value?.downloadAllSlidesAsZip) {
|
|
381
|
+
await headerRef.value.downloadAllSlidesAsZip()
|
|
382
|
+
}
|
|
383
|
+
emit('export', { format: 'images', title: presentationTitle.value })
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 通用多源数据与文件加载/渲染方法
|
|
388
|
+
* 支持传入 File 对象、Blob 二进制流、ArrayBuffer、JSON 字符串/对象、XML 字符串
|
|
389
|
+
* 自动探测类型并一键渲染进编辑器画布
|
|
390
|
+
*/
|
|
391
|
+
async function loadFile(source: File | Blob | ArrayBuffer | string | any, options?: LoadSourceOptions): Promise<LoadSourceResult> {
|
|
392
|
+
if (!source) {
|
|
393
|
+
throw new Error('加载失败:未提供任何有效数据源 (source is null or undefined)')
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let detectedType: string = options?.type || 'auto'
|
|
397
|
+
let customTitle = options?.title || ''
|
|
398
|
+
|
|
399
|
+
// 1. 探测 File 类型与获取标题
|
|
400
|
+
if (typeof File !== 'undefined' && source instanceof File) {
|
|
401
|
+
if (!customTitle) {
|
|
402
|
+
customTitle = source.name.replace(/\.[^.]+$/, '')
|
|
403
|
+
}
|
|
404
|
+
if (detectedType === 'auto') {
|
|
405
|
+
const ext = (source.name.split('.').pop() || '').toLowerCase()
|
|
406
|
+
if (ext === 'pptx') detectedType = 'pptx'
|
|
407
|
+
else if (ext === 'json') detectedType = 'json'
|
|
408
|
+
else if (ext === 'xml') detectedType = 'xml'
|
|
409
|
+
else if (source.type.includes('presentation') || source.type.includes('powerpoint')) detectedType = 'pptx'
|
|
410
|
+
else if (source.type.includes('json')) detectedType = 'json'
|
|
411
|
+
else if (source.type.includes('xml')) detectedType = 'xml'
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 2. 探测 Blob 类型
|
|
416
|
+
if (typeof Blob !== 'undefined' && source instanceof Blob && detectedType === 'auto') {
|
|
417
|
+
if (source.type.includes('presentation') || source.type.includes('powerpoint')) {
|
|
418
|
+
detectedType = 'pptx'
|
|
419
|
+
} else if (source.type.includes('json')) {
|
|
420
|
+
detectedType = 'json'
|
|
421
|
+
} else if (source.type.includes('xml')) {
|
|
422
|
+
detectedType = 'xml'
|
|
423
|
+
} else {
|
|
424
|
+
try {
|
|
425
|
+
const headBuffer = await source.slice(0, 4).arrayBuffer()
|
|
426
|
+
const headBytes = new Uint8Array(headBuffer)
|
|
427
|
+
if (headBytes[0] === 0x50 && headBytes[1] === 0x4B) {
|
|
428
|
+
detectedType = 'pptx'
|
|
429
|
+
} else {
|
|
430
|
+
const textSample = (await source.slice(0, 200).text()).trim()
|
|
431
|
+
if (textSample.startsWith('{') || textSample.startsWith('[')) {
|
|
432
|
+
detectedType = 'json'
|
|
433
|
+
} else if (textSample.startsWith('<')) {
|
|
434
|
+
detectedType = 'xml'
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch (e) {
|
|
438
|
+
console.warn('探测 Blob 头部签名失败:', e)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 3. 探测 ArrayBuffer 类型
|
|
444
|
+
if (source instanceof ArrayBuffer && detectedType === 'auto') {
|
|
445
|
+
const headBytes = new Uint8Array(source.slice(0, 4))
|
|
446
|
+
if (headBytes[0] === 0x50 && headBytes[1] === 0x4B) {
|
|
447
|
+
detectedType = 'pptx'
|
|
448
|
+
} else {
|
|
449
|
+
detectedType = 'pptx'
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 4. 探测纯字符串类型 (支持 JSON 文本、XML 文本,或 URL 文件地址)
|
|
454
|
+
if (typeof source === 'string') {
|
|
455
|
+
const trimmed = source.trim()
|
|
456
|
+
const isUrl = /^(https?:\/\/|blob:|data:|\/|\.\/|\.\.\/)/i.test(trimmed) || /\.(pptx|json|xml)(\?.*)?$/i.test(trimmed)
|
|
457
|
+
|
|
458
|
+
if (isUrl && !trimmed.startsWith('{') && !trimmed.startsWith('[') && !trimmed.startsWith('<')) {
|
|
459
|
+
const urlPath = trimmed.split('?')[0].split('#')[0]
|
|
460
|
+
const fileNameFromUrl = urlPath.substring(urlPath.lastIndexOf('/') + 1)
|
|
461
|
+
if (!customTitle && fileNameFromUrl) {
|
|
462
|
+
try {
|
|
463
|
+
customTitle = decodeURIComponent(fileNameFromUrl.replace(/\.[^.]+$/, ''))
|
|
464
|
+
} catch (e) {
|
|
465
|
+
customTitle = fileNameFromUrl.replace(/\.[^.]+$/, '')
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (detectedType === 'auto' && fileNameFromUrl) {
|
|
470
|
+
const ext = (fileNameFromUrl.split('.').pop() || '').toLowerCase()
|
|
471
|
+
if (ext === 'pptx') detectedType = 'pptx'
|
|
472
|
+
else if (ext === 'json') detectedType = 'json'
|
|
473
|
+
else if (ext === 'xml') detectedType = 'xml'
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const res = await fetch(trimmed, options?.fetchOptions)
|
|
477
|
+
if (!res.ok) {
|
|
478
|
+
throw new Error(`加载文件地址失败 (${res.status} ${res.statusText}): ${trimmed}`)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const contentType = res.headers.get('content-type') || ''
|
|
482
|
+
if (detectedType === 'auto') {
|
|
483
|
+
if (contentType.includes('presentation') || contentType.includes('powerpoint')) detectedType = 'pptx'
|
|
484
|
+
else if (contentType.includes('json')) detectedType = 'json'
|
|
485
|
+
else if (contentType.includes('xml')) detectedType = 'xml'
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const downloadedBlob = await res.blob()
|
|
489
|
+
return await loadFile(downloadedBlob, {
|
|
490
|
+
...options,
|
|
491
|
+
type: detectedType !== 'auto' ? detectedType as any : undefined,
|
|
492
|
+
title: customTitle || options?.title
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (detectedType === 'auto') {
|
|
497
|
+
if (trimmed.startsWith('<')) {
|
|
498
|
+
detectedType = 'xml'
|
|
499
|
+
} else if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
500
|
+
detectedType = 'json'
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 5. 探测普通 JS 对象
|
|
506
|
+
if (typeof source === 'object' && !(source instanceof Blob) && !(source instanceof ArrayBuffer) && detectedType === 'auto') {
|
|
507
|
+
detectedType = 'json'
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// 兜底识别
|
|
511
|
+
if (detectedType === 'auto') {
|
|
512
|
+
detectedType = typeof source === 'string' ? 'json' : 'pptx'
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (customTitle) {
|
|
516
|
+
presentationTitle.value = customTitle
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 分发执行解析渲染
|
|
520
|
+
if (detectedType === 'pptx') {
|
|
521
|
+
let buffer: ArrayBuffer
|
|
522
|
+
if (source instanceof ArrayBuffer) {
|
|
523
|
+
buffer = source
|
|
524
|
+
} else if (typeof Blob !== 'undefined' && source instanceof Blob) {
|
|
525
|
+
buffer = await source.arrayBuffer()
|
|
526
|
+
} else {
|
|
527
|
+
throw new Error('PPTX 格式需要 File / Blob / ArrayBuffer 数据源')
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!headerRef.value?.importPptxArrayBuffer) {
|
|
531
|
+
throw new Error('PPTX 解析器模块尚未就绪')
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const success = await headerRef.value.importPptxArrayBuffer(buffer, customTitle)
|
|
535
|
+
if (!success) {
|
|
536
|
+
throw new Error('PPTX 解析导入失败')
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const result: LoadSourceResult = {
|
|
540
|
+
success: true,
|
|
541
|
+
title: presentationTitle.value,
|
|
542
|
+
slidesCount: slides.value?.length || 0,
|
|
543
|
+
type: 'pptx'
|
|
544
|
+
}
|
|
545
|
+
emit('load', result)
|
|
546
|
+
return result
|
|
547
|
+
} else if (detectedType === 'json' || detectedType === 'deck') {
|
|
548
|
+
let data: any
|
|
549
|
+
if (typeof Blob !== 'undefined' && source instanceof Blob) {
|
|
550
|
+
const text = await source.text()
|
|
551
|
+
data = JSON.parse(text)
|
|
552
|
+
} else if (typeof source === 'string') {
|
|
553
|
+
data = JSON.parse(source)
|
|
554
|
+
} else {
|
|
555
|
+
data = source
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (uploadMediaResourcesInSlides) {
|
|
559
|
+
await uploadMediaResourcesInSlides(data)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (Array.isArray(data)) {
|
|
563
|
+
setSlides(data)
|
|
564
|
+
} else if (data && typeof data === 'object' && Array.isArray(data.slides)) {
|
|
565
|
+
loadDeck(data)
|
|
566
|
+
} else {
|
|
567
|
+
throw new Error('无效的 JSON 幻灯片数据格式,缺少 slides 数组')
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (options?.notify !== false) {
|
|
571
|
+
notify('已成功导入幻灯片工程')
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const result: LoadSourceResult = {
|
|
575
|
+
success: true,
|
|
576
|
+
title: presentationTitle.value,
|
|
577
|
+
slidesCount: slides.value?.length || 0,
|
|
578
|
+
type: 'json'
|
|
579
|
+
}
|
|
580
|
+
emit('load', result)
|
|
581
|
+
return result
|
|
582
|
+
} else if (detectedType === 'xml') {
|
|
583
|
+
let xmlText = ''
|
|
584
|
+
if (typeof Blob !== 'undefined' && source instanceof Blob) {
|
|
585
|
+
xmlText = await source.text()
|
|
586
|
+
} else if (typeof source === 'string') {
|
|
587
|
+
xmlText = source
|
|
588
|
+
} else {
|
|
589
|
+
throw new Error('XML 格式需要字符串或文本 Blob 数据源')
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const parser = new DOMParser()
|
|
593
|
+
const doc = parser.parseFromString(xmlText, 'text/xml')
|
|
594
|
+
const parserError = doc.querySelector('parsererror')
|
|
595
|
+
if (parserError) {
|
|
596
|
+
throw new Error(`XML 语法解析错误: ${parserError.textContent}`)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const parsedSlides: Slide[] = []
|
|
600
|
+
const deckNode = doc.querySelector('deck') || doc.querySelector('presentation')
|
|
601
|
+
if (deckNode && deckNode.getAttribute('title')) {
|
|
602
|
+
presentationTitle.value = deckNode.getAttribute('title') || presentationTitle.value
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const slideNodes = doc.querySelectorAll('slide')
|
|
606
|
+
if (slideNodes && slideNodes.length > 0) {
|
|
607
|
+
slideNodes.forEach((sNode, sIdx) => {
|
|
608
|
+
const title = sNode.querySelector('title')?.textContent || `幻灯片 ${sIdx + 1}`
|
|
609
|
+
const subtitle = sNode.querySelector('subtitle')?.textContent || ''
|
|
610
|
+
const background = sNode.getAttribute('background') || sNode.querySelector('background')?.textContent || '#ffffff'
|
|
611
|
+
const elements: any[] = []
|
|
612
|
+
|
|
613
|
+
const elemNodes = sNode.querySelectorAll('element, shape, text, item')
|
|
614
|
+
elemNodes.forEach((eNode, eIdx) => {
|
|
615
|
+
const type = eNode.getAttribute('type') || eNode.tagName.toLowerCase()
|
|
616
|
+
const content = eNode.textContent || ''
|
|
617
|
+
const left = eNode.getAttribute('left') || `${50 + eIdx * 20}px`
|
|
618
|
+
const top = eNode.getAttribute('top') || `${100 + eIdx * 40}px`
|
|
619
|
+
const width = eNode.getAttribute('width') || '400px'
|
|
620
|
+
const height = eNode.getAttribute('height') || '100px'
|
|
621
|
+
|
|
622
|
+
elements.push({
|
|
623
|
+
id: `el_xml_${Date.now()}_${eIdx}`,
|
|
624
|
+
type: type === 'shape' ? 'shape' : 'text',
|
|
625
|
+
content: content || '文本内容',
|
|
626
|
+
style: {
|
|
627
|
+
left,
|
|
628
|
+
top,
|
|
629
|
+
width,
|
|
630
|
+
height,
|
|
631
|
+
color: eNode.getAttribute('color') || '#000000',
|
|
632
|
+
fontSize: eNode.getAttribute('fontSize') || '20px'
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
parsedSlides.push({
|
|
638
|
+
id: `slide_xml_${Date.now()}_${sIdx}`,
|
|
639
|
+
title,
|
|
640
|
+
subtitle,
|
|
641
|
+
background,
|
|
642
|
+
bgType: 'color',
|
|
643
|
+
elements,
|
|
644
|
+
notes: sNode.querySelector('notes')?.textContent || '',
|
|
645
|
+
transition: 'none'
|
|
646
|
+
} as any)
|
|
647
|
+
})
|
|
648
|
+
} else {
|
|
649
|
+
const pSld = doc.querySelector('p\\:sld, sld')
|
|
650
|
+
if (pSld) {
|
|
651
|
+
const elements: any[] = []
|
|
652
|
+
const spList = doc.querySelectorAll('p\\:sp, sp')
|
|
653
|
+
spList.forEach((sp, spIdx) => {
|
|
654
|
+
const tNodes = sp.querySelectorAll('a\\:t, t')
|
|
655
|
+
let textVal = ''
|
|
656
|
+
tNodes.forEach(t => { textVal += t.textContent + ' ' })
|
|
657
|
+
textVal = textVal.trim()
|
|
658
|
+
|
|
659
|
+
const off = sp.querySelector('a\\:off, off')
|
|
660
|
+
const ext = sp.querySelector('a\\:ext, ext')
|
|
661
|
+
const xEmu = off ? parseInt(off.getAttribute('x') || '0', 10) : 0
|
|
662
|
+
const yEmu = off ? parseInt(off.getAttribute('y') || '0', 10) : 0
|
|
663
|
+
const cxEmu = ext ? parseInt(ext.getAttribute('cx') || '0', 10) : 0
|
|
664
|
+
const cyEmu = ext ? parseInt(ext.getAttribute('cy') || '0', 10) : 0
|
|
665
|
+
|
|
666
|
+
const left = `${Math.round(xEmu / 9525)}px`
|
|
667
|
+
const top = `${Math.round(yEmu / 9525)}px`
|
|
668
|
+
const width = cxEmu ? `${Math.round(cxEmu / 9525)}px` : '300px'
|
|
669
|
+
const height = cyEmu ? `${Math.round(cyEmu / 9525)}px` : '80px'
|
|
670
|
+
|
|
671
|
+
elements.push({
|
|
672
|
+
id: `el_xml_${Date.now()}_${spIdx}`,
|
|
673
|
+
type: 'text',
|
|
674
|
+
content: textVal || '形状元素',
|
|
675
|
+
style: {
|
|
676
|
+
left,
|
|
677
|
+
top,
|
|
678
|
+
width,
|
|
679
|
+
height,
|
|
680
|
+
color: '#1e293b',
|
|
681
|
+
fontSize: '18px'
|
|
682
|
+
}
|
|
683
|
+
})
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
parsedSlides.push({
|
|
687
|
+
id: `slide_xml_${Date.now()}`,
|
|
688
|
+
title: customTitle || 'XML 导入幻灯片',
|
|
689
|
+
subtitle: '',
|
|
690
|
+
background: '#ffffff',
|
|
691
|
+
bgType: 'color',
|
|
692
|
+
elements,
|
|
693
|
+
notes: '',
|
|
694
|
+
transition: 'none'
|
|
695
|
+
} as any)
|
|
696
|
+
} else {
|
|
697
|
+
throw new Error('未识别的 XML 幻灯片格式,请提供标准 deck/slide 或 p:sld 格式')
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (parsedSlides.length === 0) {
|
|
702
|
+
throw new Error('XML 解析成功,但未提取到有效幻灯片页面')
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
setSlides(parsedSlides)
|
|
706
|
+
if (options?.notify !== false) {
|
|
707
|
+
notify(`已成功从 XML 渲染 ${parsedSlides.length} 页幻灯片`)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const result: LoadSourceResult = {
|
|
711
|
+
success: true,
|
|
712
|
+
title: presentationTitle.value,
|
|
713
|
+
slidesCount: parsedSlides.length,
|
|
714
|
+
type: 'xml'
|
|
715
|
+
}
|
|
716
|
+
emit('load', result)
|
|
717
|
+
return result
|
|
718
|
+
} else {
|
|
719
|
+
throw new Error(`暂不支持的数据格式类型: ${detectedType}`)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 监听外部传入的 modelValue
|
|
724
|
+
watch(
|
|
725
|
+
() => props.modelValue,
|
|
726
|
+
(val) => {
|
|
727
|
+
if (isInternalUpdating) return
|
|
728
|
+
if (!val) return
|
|
729
|
+
if (val === lastEmittedModelValue) return
|
|
730
|
+
loadDeck(val as any)
|
|
731
|
+
},
|
|
732
|
+
{ immediate: true, deep: true }
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
// 对外 deck 同步防抖:打字/连续微编辑不再每次都整册 getDeck 深拷贝+emit,
|
|
736
|
+
// 300ms 静默后统一发一次;拖拽落沿/翻页/卸载时立即冲刷,宿主侧最终一致
|
|
737
|
+
let emitDeckTimer: ReturnType<typeof setTimeout> | null = null
|
|
738
|
+
let emitDeckPending = false
|
|
739
|
+
|
|
740
|
+
function emitDeckNow() {
|
|
741
|
+
if (emitDeckTimer) {
|
|
742
|
+
clearTimeout(emitDeckTimer)
|
|
743
|
+
emitDeckTimer = null
|
|
744
|
+
}
|
|
745
|
+
if (!emitDeckPending) return
|
|
746
|
+
if (isInternalUpdating) return // 保留 pending,下次调度/冲刷再试
|
|
747
|
+
emitDeckPending = false
|
|
748
|
+
isInternalUpdating = true
|
|
749
|
+
const currentDeck = getDeck()
|
|
750
|
+
lastEmittedModelValue = currentDeck
|
|
751
|
+
emit('update:modelValue', currentDeck)
|
|
752
|
+
emit('change', {
|
|
753
|
+
slides: slides.value,
|
|
754
|
+
activeIndex: activeIndex.value,
|
|
755
|
+
title: presentationTitle.value
|
|
756
|
+
})
|
|
757
|
+
nextTick(() => {
|
|
758
|
+
isInternalUpdating = false
|
|
759
|
+
})
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function scheduleEmitDeck() {
|
|
763
|
+
if (isInternalUpdating) return
|
|
764
|
+
emitDeckPending = true
|
|
765
|
+
if (emitDeckTimer) clearTimeout(emitDeckTimer)
|
|
766
|
+
emitDeckTimer = setTimeout(() => {
|
|
767
|
+
emitDeckTimer = null
|
|
768
|
+
emitDeckNow()
|
|
769
|
+
}, 300)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// 监听内部 slides 变化,向外部同步(拖拽/变形交互期间冻结,避免高频触发几十毫秒的深拷贝序列化)
|
|
773
|
+
watch(
|
|
774
|
+
() => slides.value,
|
|
775
|
+
() => {
|
|
776
|
+
if (isInternalUpdating) return
|
|
777
|
+
if (isCanvasDragging?.value || pauseHistory?.value) return
|
|
778
|
+
scheduleEmitDeck()
|
|
779
|
+
},
|
|
780
|
+
{ deep: true }
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
// 拖拽/变形结束后,立即冲刷防抖中的对外同步
|
|
784
|
+
watch([() => isCanvasDragging?.value, () => pauseHistory?.value], ([isDragging, isPaused], [prevDragging, prevPaused]) => {
|
|
785
|
+
if ((prevDragging && !isDragging) || (prevPaused && !isPaused)) {
|
|
786
|
+
if (!isDragging && !isPaused) {
|
|
787
|
+
emitDeckNow()
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
// 监听当前选中的幻灯片切换(翻页前冲刷防抖中的对外同步,宿主拿到的 deck 与页码一致)
|
|
793
|
+
watch(
|
|
794
|
+
() => activeIndex.value,
|
|
795
|
+
(idx) => {
|
|
796
|
+
emitDeckNow()
|
|
797
|
+
emit('slideChange', {
|
|
798
|
+
index: idx,
|
|
799
|
+
slide: activeSlide.value || null
|
|
800
|
+
})
|
|
801
|
+
}
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
// 公式编辑确认
|
|
805
|
+
function handleFormulaConfirm(latex: string) {
|
|
806
|
+
if (currentEditingFormulaEl.value) {
|
|
807
|
+
currentEditingFormulaEl.value.latex = latex
|
|
808
|
+
currentEditingFormulaEl.value.content = latex
|
|
809
|
+
notify('已更新公式')
|
|
810
|
+
currentEditingFormulaEl.value = null
|
|
811
|
+
} else {
|
|
812
|
+
addElement('text', latex, latex)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// 组装对外暴露的编程式 API 对象
|
|
817
|
+
// 卸载前冲刷防抖中的对外同步,避免最后 300ms 内的编辑丢失
|
|
818
|
+
onBeforeUnmount(() => {
|
|
819
|
+
emitDeckNow()
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
const editorApi: PptEditorApi = {
|
|
823
|
+
save,
|
|
824
|
+
getSaveData: () => getDeck(),
|
|
825
|
+
getSaveJsonBlob,
|
|
826
|
+
getSaveJsonFile,
|
|
827
|
+
getSavePptxBlob,
|
|
828
|
+
getSavePptxFile,
|
|
829
|
+
getSlides,
|
|
830
|
+
getDeck,
|
|
831
|
+
setSlides,
|
|
832
|
+
loadDeck,
|
|
833
|
+
loadFile,
|
|
834
|
+
importFile: loadFile,
|
|
835
|
+
getActiveSlide: () => activeSlide.value || null,
|
|
836
|
+
getActiveIndex: () => activeIndex.value,
|
|
837
|
+
selectSlide: (index: number) => selectSlide(index),
|
|
838
|
+
addSlide: () => addSlide(),
|
|
839
|
+
deleteSlide: (index?: number) => {
|
|
840
|
+
const targetIdx = typeof index === 'number' ? index : activeIndex.value
|
|
841
|
+
deleteSlide(targetIdx)
|
|
842
|
+
},
|
|
843
|
+
exportPptx,
|
|
844
|
+
exportJson,
|
|
845
|
+
exportImages,
|
|
846
|
+
undo,
|
|
847
|
+
redo,
|
|
848
|
+
canUndo,
|
|
849
|
+
canRedo,
|
|
850
|
+
startPresentation,
|
|
851
|
+
stopPresentation,
|
|
852
|
+
notify,
|
|
853
|
+
zoomIn,
|
|
854
|
+
zoomOut,
|
|
855
|
+
resetZoom,
|
|
856
|
+
setAiConfig,
|
|
857
|
+
isAiBeautifyEnabled,
|
|
858
|
+
setMaterialConfig,
|
|
859
|
+
isMaterialEnabled,
|
|
860
|
+
saveMaterial: (meta) => saveCurrentSlideToMaterial(meta),
|
|
861
|
+
setCustomUploadHandler,
|
|
862
|
+
getCustomUploadHandler,
|
|
863
|
+
slidesComposable: slidesState
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// 获取当前插槽实例
|
|
867
|
+
const slots = useSlots()
|
|
868
|
+
|
|
869
|
+
// 顶部工具栏插槽作用域上下文数据
|
|
870
|
+
const headerSlotContext = computed(() => ({
|
|
871
|
+
api: editorApi,
|
|
872
|
+
deck: getDeck(),
|
|
873
|
+
slides: slidesState.slides.value,
|
|
874
|
+
activeSlide: slidesState.activeSlide.value || null,
|
|
875
|
+
activeIndex: slidesState.activeIndex.value,
|
|
876
|
+
title: presentationTitle.value,
|
|
877
|
+
setTitle: (newTitle: string) => {
|
|
878
|
+
presentationTitle.value = newTitle
|
|
879
|
+
},
|
|
880
|
+
save,
|
|
881
|
+
exportPptx,
|
|
882
|
+
exportJson,
|
|
883
|
+
exportImages,
|
|
884
|
+
undo,
|
|
885
|
+
redo,
|
|
886
|
+
canUndo: canUndo.value,
|
|
887
|
+
canRedo: canRedo.value,
|
|
888
|
+
zoomIn,
|
|
889
|
+
zoomOut,
|
|
890
|
+
resetZoom,
|
|
891
|
+
zoomRatio: slidesState.zoomRatio.value,
|
|
892
|
+
isMasterEditingView: slidesState.isMasterEditingView.value
|
|
893
|
+
}))
|
|
894
|
+
|
|
895
|
+
// 对外通过 ref 暴露所有方法
|
|
896
|
+
defineExpose(editorApi)
|
|
897
|
+
|
|
898
|
+
onMounted(async () => {
|
|
899
|
+
// 触发 ready 事件
|
|
900
|
+
emit('ready', editorApi)
|
|
901
|
+
|
|
902
|
+
// 若启用 MCP 自动拉取
|
|
903
|
+
if (props.enableMcp) {
|
|
904
|
+
const params = new URLSearchParams(window.location.search)
|
|
905
|
+
const deckId = params.get('deckId')
|
|
906
|
+
const deckPath = params.get('deck')
|
|
907
|
+
if (deckId || deckPath) {
|
|
908
|
+
try {
|
|
909
|
+
const query = deckId
|
|
910
|
+
? `deckId=${encodeURIComponent(deckId)}`
|
|
911
|
+
: `path=${encodeURIComponent(deckPath || '')}`
|
|
912
|
+
const response = await fetch(`/__mcp_deck?${query}`)
|
|
913
|
+
if (!response.ok) {
|
|
914
|
+
throw new Error(await response.text())
|
|
915
|
+
}
|
|
916
|
+
const deck = await response.json()
|
|
917
|
+
loadDeck(deck)
|
|
918
|
+
notify('已从 MCP 打开可编辑稿')
|
|
919
|
+
} catch (error: any) {
|
|
920
|
+
notify(`打开 MCP 稿件失败:${error?.message || error}`)
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
})
|
|
925
|
+
</script>
|
|
926
|
+
|
|
927
|
+
<template>
|
|
928
|
+
<div class="app-container jvs-ppt-editor-root" :class="{ 'is-readonly': readonly }">
|
|
929
|
+
<!-- Main top Navigation bar (支持完整 header 插槽替换或精细区域插槽定制) -->
|
|
930
|
+
<slot name="header" v-bind="headerSlotContext">
|
|
931
|
+
<Header ref="headerRef" v-show="showHeader" :slot-props="headerSlotContext">
|
|
932
|
+
<!-- 左侧前置 (Logo、返回按钮等) -->
|
|
933
|
+
<template v-if="slots['header-left-prefix']" #left-prefix="sp">
|
|
934
|
+
<slot name="header-left-prefix" v-bind="sp"></slot>
|
|
935
|
+
</template>
|
|
936
|
+
|
|
937
|
+
<!-- 左侧主体(若外部传了 header-left,则覆盖默认汉堡菜单和标题) -->
|
|
938
|
+
<template v-if="slots['header-left']" #left="sp">
|
|
939
|
+
<slot name="header-left" v-bind="sp"></slot>
|
|
940
|
+
</template>
|
|
941
|
+
|
|
942
|
+
<!-- 左侧后置 (文档副标题、状态标签等) -->
|
|
943
|
+
<template v-if="slots['header-left-suffix']" #left-suffix="sp">
|
|
944
|
+
<slot name="header-left-suffix" v-bind="sp"></slot>
|
|
945
|
+
</template>
|
|
946
|
+
|
|
947
|
+
<!-- 中间工具栏主体(若外部传了 toolbar 或 header-center,则覆盖默认 Toolbar) -->
|
|
948
|
+
<template v-if="slots['toolbar'] || slots['header-center']" #center="sp">
|
|
949
|
+
<slot name="toolbar" v-bind="sp">
|
|
950
|
+
<slot name="header-center" v-bind="sp"></slot>
|
|
951
|
+
</slot>
|
|
952
|
+
</template>
|
|
953
|
+
|
|
954
|
+
<!-- 工具栏前置 (插入到撤销按钮前) -->
|
|
955
|
+
<template v-if="slots['toolbar-prefix']" #toolbar-prefix="sp">
|
|
956
|
+
<slot name="toolbar-prefix" v-bind="sp"></slot>
|
|
957
|
+
</template>
|
|
958
|
+
|
|
959
|
+
<!-- 工具栏后置 (插入到一键美化按钮后) -->
|
|
960
|
+
<template v-if="slots['toolbar-suffix']" #toolbar-suffix="sp">
|
|
961
|
+
<slot name="toolbar-suffix" v-bind="sp"></slot>
|
|
962
|
+
</template>
|
|
963
|
+
|
|
964
|
+
<!-- 右侧前置 (缩放/放映前) -->
|
|
965
|
+
<template v-if="slots['header-right-prefix']" #right-prefix="sp">
|
|
966
|
+
<slot name="header-right-prefix" v-bind="sp"></slot>
|
|
967
|
+
</template>
|
|
968
|
+
|
|
969
|
+
<!-- 右侧主体 (保存、导出、分享等自定义按钮) -->
|
|
970
|
+
<template v-if="slots['header-right']" #right="sp">
|
|
971
|
+
<slot name="header-right" v-bind="sp"></slot>
|
|
972
|
+
</template>
|
|
973
|
+
|
|
974
|
+
<!-- 右侧后置 (最右侧,放映按钮后) -->
|
|
975
|
+
<template v-if="slots['header-right-suffix']" #right-suffix="sp">
|
|
976
|
+
<slot name="header-right-suffix" v-bind="sp"></slot>
|
|
977
|
+
</template>
|
|
978
|
+
</Header>
|
|
979
|
+
</slot>
|
|
980
|
+
|
|
981
|
+
<!-- Main Workspace consisting of SidebarLeft, Center Canvas, and SidebarRight -->
|
|
982
|
+
<main class="app-workspace" :class="{ 'hide-right-sidebar': !activeTab || !showSidebarRight }">
|
|
983
|
+
<!-- 左侧幻灯片列表 -->
|
|
984
|
+
<SidebarLeft v-if="showSidebarLeft" class="sidebar-left" />
|
|
985
|
+
|
|
986
|
+
<!-- 中间主画布区 -->
|
|
987
|
+
<CanvasArea class="canvas-area" />
|
|
988
|
+
|
|
989
|
+
<!-- 右侧属性配置栏 -->
|
|
990
|
+
<SidebarRight v-if="showSidebarRight && !readonly" />
|
|
991
|
+
</main>
|
|
992
|
+
|
|
993
|
+
<!-- Fullscreen slide deck display panel -->
|
|
994
|
+
<PresentationMode v-if="isPresenting" />
|
|
995
|
+
|
|
996
|
+
<!-- Action feedback notifications banner -->
|
|
997
|
+
<ToastNotification />
|
|
998
|
+
|
|
999
|
+
<!-- Global LaTeX Formula Editor Dialog -->
|
|
1000
|
+
<FormulaDialog
|
|
1001
|
+
v-if="!readonly && showFormulaDialog"
|
|
1002
|
+
v-model:visible="formulaDialogVisible"
|
|
1003
|
+
:initialLatex="currentEditingFormulaEl?.latex || ''"
|
|
1004
|
+
@confirm="handleFormulaConfirm"
|
|
1005
|
+
/>
|
|
1006
|
+
|
|
1007
|
+
<!-- Global DSL JSON Input Dialog -->
|
|
1008
|
+
<DslInputDialog
|
|
1009
|
+
v-if="!readonly && showDslInputDialog"
|
|
1010
|
+
v-model:visible="dslInputDialogVisible"
|
|
1011
|
+
@confirm="importSlides"
|
|
1012
|
+
/>
|
|
1013
|
+
|
|
1014
|
+
<!-- Global AI PPT Streaming Dialog -->
|
|
1015
|
+
<AIPptDialog
|
|
1016
|
+
v-if="!readonly && showAiPptDialog"
|
|
1017
|
+
v-model:visible="aiPptDialogVisible"
|
|
1018
|
+
/>
|
|
1019
|
+
|
|
1020
|
+
<!-- Global Unsplash AI Image Search & Insertion Dialog -->
|
|
1021
|
+
<UnsplashImageDialog v-if="!readonly && showUnsplashDialog" />
|
|
1022
|
+
</div>
|
|
1023
|
+
</template>
|
|
1024
|
+
|
|
1025
|
+
<style scoped>
|
|
1026
|
+
.jvs-ppt-editor-root {
|
|
1027
|
+
width: 100%;
|
|
1028
|
+
height: 100%;
|
|
1029
|
+
position: relative;
|
|
1030
|
+
overflow: hidden;
|
|
1031
|
+
display: flex;
|
|
1032
|
+
flex-direction: column;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/* 只读模式下的禁用样式微调 */
|
|
1036
|
+
.jvs-ppt-editor-root.is-readonly :deep(.action-btn-disabled) {
|
|
1037
|
+
pointer-events: none;
|
|
1038
|
+
opacity: 0.6;
|
|
1039
|
+
}
|
|
1040
|
+
</style>
|