jvs-ppt-ui 0.0.14 → 0.0.16

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.
Files changed (67) hide show
  1. package/dist/lib/check_imgs.html +18 -0
  2. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.css +187 -27
  3. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.js +1 -1
  4. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.json +345 -65
  5. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.ttf +0 -0
  6. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff +0 -0
  7. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff2 +0 -0
  8. package/dist/lib/jvs-ppt-ui/img/al.png +0 -0
  9. package/dist/lib/jvs-ppt-ui/img/d.png +0 -0
  10. package/dist/lib/jvs-ppt-ui/img/dl.png +0 -0
  11. package/dist/lib/jvs-ppt-ui/img/l.png +0 -0
  12. package/dist/lib/jvs-ppt-ui.es.js +44973 -43349
  13. package/dist/lib/jvs-ppt-ui.umd.js +102 -102
  14. package/dist/lib/style.css +1 -1
  15. package/package.json +1 -1
  16. package/scripts/check-icons.mjs +22 -0
  17. package/scripts/patch-anim-icons.mjs +45 -0
  18. package/src/assets/iconfont/iconfont.css +187 -27
  19. package/src/assets/iconfont/iconfont.js +1 -1
  20. package/src/assets/iconfont/iconfont.json +345 -65
  21. package/src/assets/iconfont/iconfont.ttf +0 -0
  22. package/src/assets/iconfont/iconfont.woff +0 -0
  23. package/src/assets/iconfont/iconfont.woff2 +0 -0
  24. package/src/components/CanvasArea.vue +218 -193
  25. package/src/components/ChartTypeSelector.vue +509 -148
  26. package/src/components/Header.vue +327 -44
  27. package/src/components/PresentationGroupChild.vue +44 -13
  28. package/src/components/PresentationMode.vue +84 -33
  29. package/src/components/QuickBorderPopover.vue +51 -11
  30. package/src/components/QuickColorPopover.vue +65 -21
  31. package/src/components/SidebarLeft.vue +286 -107
  32. package/src/components/SidebarRight.vue +45 -38
  33. package/src/components/TemplateGallery.vue +1110 -1237
  34. package/src/components/ToastNotification.vue +2 -2
  35. package/src/components/Toolbar.vue +825 -733
  36. package/src/components/canvas/CanvasQuickToolbar.vue +17 -30
  37. package/src/components/canvas/elements/CanvasGroupElement.vue +6 -0
  38. package/src/components/canvas/elements/CanvasShapeElement.vue +198 -38
  39. package/src/components/canvas/elements/CanvasTextElement.vue +4 -3
  40. package/src/components/sidebar/common/SidebarAlignGroup.vue +104 -50
  41. package/src/components/sidebar/common/SidebarBorderAccordion.vue +375 -148
  42. package/src/components/sidebar/common/SidebarPositionGroup.vue +358 -149
  43. package/src/components/sidebar/common/SidebarReflectionAccordion.vue +429 -203
  44. package/src/components/sidebar/common/SidebarShadowAccordion.vue +844 -416
  45. package/src/components/sidebar/panels/GradientEditor.vue +671 -316
  46. package/src/components/sidebar/panels/LayoutThumbSvg.vue +33 -16
  47. package/src/components/sidebar/panels/SidebarAnimationPanel.vue +701 -138
  48. package/src/components/sidebar/panels/SidebarChartFormatPanel.vue +3 -3
  49. package/src/components/sidebar/panels/SidebarDesignPanel.vue +1754 -416
  50. package/src/components/sidebar/panels/SidebarMediaFormatPanel.vue +12 -0
  51. package/src/components/sidebar/panels/SidebarShapeFormatPanel.vue +813 -174
  52. package/src/components/sidebar/panels/SidebarTableFormatPanel.vue +2 -2
  53. package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +1938 -1427
  54. package/src/components/sidebar/panels/SidebarTransitionPanel.vue +791 -210
  55. package/src/composables/canvas/useCanvasShortcuts.ts +20 -6
  56. package/src/composables/canvas/useCanvasText.ts +1 -1
  57. package/src/composables/canvas/useCanvasTransform.ts +44 -0
  58. package/src/composables/presetThemes.ts +43 -44
  59. package/src/composables/sidebar/useSidebarAnimation.ts +101 -25
  60. package/src/composables/sidebar/useSidebarDesign.ts +30 -11
  61. package/src/composables/sidebar/useSidebarElementTransform.ts +10 -1
  62. package/src/composables/sidebar/useSidebarImage.ts +10 -2
  63. package/src/composables/sidebar/useSidebarShapeFormat.ts +562 -107
  64. package/src/composables/useSlides.ts +331 -153
  65. package/src/main.ts +2 -0
  66. package/src/style.css +926 -359
  67. package/src/utils/canvas/elementStyles.ts +42 -1
@@ -1,210 +1,791 @@
1
- <script setup lang="ts">
2
- defineProps<{
3
- transitionOptions: Array<{ name: string; value: string }>
4
- activeSlideTransition: string
5
- }>()
6
-
7
- const emit = defineEmits<{
8
- (e: 'update:activeSlideTransition', val: string): void
9
- (e: 'applyTransitionToAll'): void
10
- }>()
11
- </script>
12
-
13
- <template>
14
- <div class="sidebar-transition-panel flex-column" style="display: flex; flex-direction: column; gap: 14px; padding: 16px;">
15
- <div class="transition-grid">
16
- <div
17
- v-for="item in transitionOptions"
18
- :key="item.value"
19
- class="transition-card"
20
- :class="[item.value, { active: activeSlideTransition === item.value }]"
21
- @click="emit('update:activeSlideTransition', item.value)"
22
- >
23
- <div class="transition-mockup" :class="item.value">P</div>
24
- <div class="transition-name">{{ item.name }}</div>
25
- </div>
26
- </div>
27
- <button class="transition-apply-all-btn" @click="emit('applyTransitionToAll')">
28
- ✓ 应用到全部
29
- </button>
30
- </div>
31
- </template>
32
-
33
- <style scoped lang="scss">
34
- .transition-grid {
35
- display: grid;
36
- grid-template-columns: 1fr 1fr;
37
- gap: 12px;
38
- padding: 4px 0;
39
- }
40
-
41
- .transition-card {
42
- border: 1px solid var(--border-color);
43
- background: #ffffff;
44
- border-radius: 6px;
45
- padding: 12px;
46
- cursor: pointer;
47
- display: flex;
48
- flex-direction: column;
49
- align-items: center;
50
- gap: 8px;
51
- transition: all 0.2s ease;
52
- box-sizing: border-box;
53
- }
54
-
55
- .transition-card:hover {
56
- border-color: var(--accent-primary);
57
- box-shadow: var(--shadow-sm);
58
- }
59
-
60
- .transition-card.active {
61
- border-color: #1E6FFF !important;
62
- background-color: #f0f6ff !important;
63
- }
64
-
65
- .transition-mockup {
66
- position: relative;
67
- width: 60px;
68
- height: 38px;
69
- background-color: #555555;
70
- border-radius: 3px;
71
- display: flex;
72
- align-items: center;
73
- justify-content: center;
74
- user-select: none;
75
- overflow: hidden;
76
- color: #ffffff;
77
- font-size: 14px;
78
- font-weight: 700;
79
- font-family: var(--font-sans);
80
- }
81
-
82
- .transition-mockup:not(.none):not(.random):hover::after {
83
- content: "JVS-PPT";
84
- width: 100%;
85
- height: 100%;
86
- position: absolute;
87
- left: 0;
88
- top: 0;
89
- background-color: #1E6FFF;
90
- color: #ffffff;
91
- display: flex;
92
- justify-content: center;
93
- align-items: center;
94
- font-size: 9px;
95
- font-weight: 700;
96
- white-space: nowrap;
97
- }
98
-
99
- @keyframes preview-slideX {
100
- 0% { transform: translateX(100%); }
101
- 100% { transform: translateX(0); }
102
- }
103
- .transition-mockup.push-left-right:hover::after {
104
- animation: preview-slideX 0.3s linear;
105
- }
106
-
107
- @keyframes preview-slideY {
108
- 0% { transform: translateY(100%); }
109
- 100% { transform: translateY(0); }
110
- }
111
- .transition-mockup.push-up-down:hover::after {
112
- animation: preview-slideY 0.3s linear;
113
- }
114
-
115
- @keyframes preview-slideX3D {
116
- 0% { transform: translateX(100%) scale(0.5); }
117
- 100% { transform: translateX(0); }
118
- }
119
- .transition-mockup.push-left-right-3d:hover::after {
120
- animation: preview-slideX3D 0.3s linear;
121
- }
122
-
123
- @keyframes preview-slideY3D {
124
- 0% { transform: translateY(100%) scale(0.5); }
125
- 100% { transform: translateY(0); }
126
- }
127
- .transition-mockup.push-up-down-3d:hover::after {
128
- animation: preview-slideY3D 0.3s linear;
129
- }
130
-
131
- @keyframes preview-fade {
132
- 0% { opacity: 0; }
133
- 100% { opacity: 1; }
134
- }
135
- .transition-mockup.fade:hover::after {
136
- animation: preview-fade 0.3s linear;
137
- }
138
-
139
- @keyframes preview-rotate {
140
- 0% { transform: rotate(-90deg); }
141
- 100% { transform: rotate(0); }
142
- }
143
- .transition-mockup.rotate:hover::after {
144
- transform-origin: 0 0;
145
- animation: preview-rotate 0.3s linear;
146
- }
147
-
148
- @keyframes preview-scaleY {
149
- 0% { transform: scaleY(0.1); }
150
- 100% { transform: scaleY(1); }
151
- }
152
- .transition-mockup.expand-up-down:hover::after {
153
- animation: preview-scaleY 0.3s linear;
154
- }
155
-
156
- @keyframes preview-scaleX {
157
- 0% { transform: scaleX(0.1); }
158
- 100% { transform: scaleX(1); }
159
- }
160
- .transition-mockup.expand-left-right:hover::after {
161
- animation: preview-scaleX 0.3s linear;
162
- }
163
-
164
- @keyframes preview-scale {
165
- 0% { transform: scale(0.25); }
166
- 100% { transform: scale(1); }
167
- }
168
- .transition-mockup.zoom-in:hover::after {
169
- animation: preview-scale 0.3s linear;
170
- }
171
-
172
- @keyframes preview-scaleReverse {
173
- 0% { transform: scale(2); }
174
- 100% { transform: scale(1); }
175
- }
176
- .transition-mockup.zoom-out:hover::after {
177
- animation: preview-scaleReverse 0.3s linear;
178
- }
179
-
180
- .transition-name {
181
- font-size: 11.5px;
182
- color: var(--text-primary);
183
- font-weight: 500;
184
- text-align: center;
185
- }
186
-
187
- .transition-apply-all-btn {
188
- background: #ffffff;
189
- border: 1px solid var(--border-color);
190
- border-radius: 6px;
191
- font-size: 12px;
192
- color: var(--text-primary);
193
- cursor: pointer;
194
- padding: 10px 0;
195
- width: 100%;
196
- display: flex;
197
- align-items: center;
198
- justify-content: center;
199
- gap: 6px;
200
- transition: all var(--transition-fast);
201
- font-weight: 500;
202
- margin-top: 14px;
203
- }
204
-
205
- .transition-apply-all-btn:hover {
206
- background: var(--bg-active);
207
- border-color: var(--accent-primary);
208
- color: var(--accent-primary);
209
- }
210
- </style>
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
3
+ import { useSlides } from '../../../composables/useSlides'
4
+ import '../../../assets/iconfont/iconfont.js'
5
+
6
+ const props = defineProps<{
7
+ transitionOptions: Array<{ name: string; value: string }>
8
+ activeSlideTransition: string
9
+ }>()
10
+
11
+ const emit = defineEmits<{
12
+ (e: 'update:activeSlideTransition', val: string): void
13
+ (e: 'applyTransitionToAll', options: any): void
14
+ }>()
15
+
16
+ const { activeSlide, zoomRatio } = useSlides()
17
+
18
+ // 持续时间配置 (秒)
19
+ const duration = ref<number>(activeSlide.value?.transitionDuration ?? 1)
20
+ // 自动切页开关与延时
21
+ const autoAdvance = ref<boolean>(Boolean(activeSlide.value?.autoAdvance))
22
+ const autoAdvanceDelay = ref<number>(activeSlide.value?.autoAdvanceDelay ?? 0.5)
23
+ // 单击鼠标时换片
24
+ const clickAdvance = ref<boolean>(activeSlide.value?.clickAdvance ?? false)
25
+
26
+ // 持续时间下拉 Popover 状态
27
+ const isDurationPopoverVisible = ref(false)
28
+ const durationTriggerRef = ref<HTMLElement | null>(null)
29
+ const durationTriggerWidth = ref(258)
30
+
31
+ const durationOptions = [
32
+ { label: '0.5秒(非常快)', value: 0.5 },
33
+ { label: '1秒(快)', value: 1 },
34
+ { label: '1.5秒(中等)', value: 1.5 },
35
+ { label: '2秒(慢)', value: 2 },
36
+ { label: '3秒(非常慢)', value: 3 }
37
+ ]
38
+
39
+ const currentDurationLabel = computed(() => {
40
+ const match = durationOptions.find(opt => opt.value === duration.value)
41
+ return match ? match.label : `${duration.value}秒`
42
+ })
43
+
44
+ function onDurationPopoverShow() {
45
+ if (durationTriggerRef.value) {
46
+ durationTriggerWidth.value = durationTriggerRef.value.offsetWidth
47
+ }
48
+ }
49
+
50
+ function selectDuration(val: number) {
51
+ duration.value = val
52
+ isDurationPopoverVisible.value = false
53
+ }
54
+
55
+ // 响应切换选中 slide
56
+ watch(activeSlide, (slide) => {
57
+ if (slide) {
58
+ duration.value = slide.transitionDuration ?? 1
59
+ autoAdvance.value = Boolean(slide.autoAdvance)
60
+ autoAdvanceDelay.value = slide.autoAdvanceDelay ?? 0.5
61
+ clickAdvance.value = slide.clickAdvance ?? false
62
+ }
63
+ }, { immediate: true })
64
+
65
+ // 同步写入 activeSlide
66
+ watch(duration, (val) => {
67
+ if (activeSlide.value) activeSlide.value.transitionDuration = val
68
+ })
69
+ watch(autoAdvance, (val) => {
70
+ if (activeSlide.value) activeSlide.value.autoAdvance = val
71
+ })
72
+ watch(autoAdvanceDelay, (val) => {
73
+ if (activeSlide.value) activeSlide.value.autoAdvanceDelay = val
74
+ })
75
+ watch(clickAdvance, (val) => {
76
+ if (activeSlide.value) activeSlide.value.clickAdvance = val
77
+ })
78
+
79
+ // 数字步进器操作
80
+ function stepUp() {
81
+ if (!autoAdvance.value) return
82
+ autoAdvanceDelay.value = +(Math.min(60, Number((autoAdvanceDelay.value + 0.5).toFixed(1))))
83
+ }
84
+
85
+ function stepDown() {
86
+ if (!autoAdvance.value) return
87
+ autoAdvanceDelay.value = +(Math.max(0.1, Number((autoAdvanceDelay.value - 0.5).toFixed(1))))
88
+ }
89
+
90
+ // 恢复可能受损的 main-slide transform 缩放
91
+ function restoreSlideTransform() {
92
+ const slideEl = document.querySelector('.canvas-area .main-slide') as HTMLElement
93
+ if (slideEl) {
94
+ const scale = (zoomRatio?.value || 100) / 100
95
+ slideEl.style.transform = `scale(${scale})`
96
+ slideEl.style.transformOrigin = 'center center'
97
+ }
98
+ }
99
+
100
+ onMounted(() => {
101
+ restoreSlideTransform()
102
+ })
103
+
104
+ onBeforeUnmount(() => {
105
+ if (activeAnimation) {
106
+ activeAnimation.cancel()
107
+ activeAnimation = null
108
+ }
109
+ restoreSlideTransform()
110
+ })
111
+
112
+ watch(zoomRatio, () => {
113
+ restoreSlideTransform()
114
+ })
115
+
116
+ // 预览动画播放系统 (直接作用于 main-slide,严格合成当前 scale 矩阵,执行后绝不改变大小)
117
+ let activeAnimation: Animation | null = null
118
+
119
+ function playPreview(transitionType?: string) {
120
+ const t = transitionType || props.activeSlideTransition || 'push-up-down'
121
+ const slideEl = document.querySelector('.canvas-area .main-slide') as HTMLElement
122
+ if (!slideEl) return
123
+
124
+ const scale = (zoomRatio?.value || 100) / 100
125
+
126
+ if (activeAnimation) {
127
+ activeAnimation.onfinish = null
128
+ activeAnimation.oncancel = null
129
+ activeAnimation.cancel()
130
+ activeAnimation = null
131
+ }
132
+
133
+ if (t === 'none') {
134
+ restoreSlideTransform()
135
+ return
136
+ }
137
+
138
+ let actualType = t
139
+ if (t === 'random') {
140
+ const pool = [
141
+ 'push-left-right', 'push-up-down', 'push-left-right-3d', 'push-up-down-3d',
142
+ 'fade', 'rotate', 'expand-up-down', 'expand-left-right', 'zoom-in', 'zoom-out'
143
+ ]
144
+ actualType = pool[Math.floor(Math.random() * pool.length)]
145
+ }
146
+
147
+ const dur = (duration.value || 1) * 1000
148
+ let keyframes: Keyframe[] = []
149
+
150
+ switch (actualType) {
151
+ case 'push-left-right':
152
+ keyframes = [
153
+ { transform: `scale(${scale}) translateX(100%)`, opacity: 0.3 },
154
+ { transform: `scale(${scale}) translateX(0)`, opacity: 1 }
155
+ ]
156
+ break
157
+ case 'push-up-down':
158
+ keyframes = [
159
+ { transform: `scale(${scale}) translateY(100%)`, opacity: 0.3 },
160
+ { transform: `scale(${scale}) translateY(0)`, opacity: 1 }
161
+ ]
162
+ break
163
+ case 'push-left-right-3d':
164
+ keyframes = [
165
+ { transform: `scale(${scale * 0.7}) perspective(1000px) rotateY(-35deg) translateX(70%)`, opacity: 0.2 },
166
+ { transform: `scale(${scale}) perspective(1000px) rotateY(0deg) translateX(0)`, opacity: 1 }
167
+ ]
168
+ break
169
+ case 'push-up-down-3d':
170
+ keyframes = [
171
+ { transform: `scale(${scale * 0.7}) perspective(1000px) rotateX(35deg) translateY(70%)`, opacity: 0.2 },
172
+ { transform: `scale(${scale}) perspective(1000px) rotateX(0deg) translateY(0)`, opacity: 1 }
173
+ ]
174
+ break
175
+ case 'fade':
176
+ keyframes = [
177
+ { transform: `scale(${scale})`, opacity: 0 },
178
+ { transform: `scale(${scale})`, opacity: 1 }
179
+ ]
180
+ break
181
+ case 'rotate':
182
+ keyframes = [
183
+ { transform: `scale(${scale * 0.5}) rotate(-180deg)`, opacity: 0.2 },
184
+ { transform: `scale(${scale}) rotate(0deg)`, opacity: 1 }
185
+ ]
186
+ break
187
+ case 'expand-up-down':
188
+ keyframes = [
189
+ { transform: `scale(${scale}) scaleY(0)`, opacity: 0.2 },
190
+ { transform: `scale(${scale}) scaleY(1)`, opacity: 1 }
191
+ ]
192
+ break
193
+ case 'expand-left-right':
194
+ keyframes = [
195
+ { transform: `scale(${scale}) scaleX(0)`, opacity: 0.2 },
196
+ { transform: `scale(${scale}) scaleX(1)`, opacity: 1 }
197
+ ]
198
+ break
199
+ case 'zoom-in':
200
+ keyframes = [
201
+ { transform: `scale(${scale * 0.3})`, opacity: 0 },
202
+ { transform: `scale(${scale})`, opacity: 1 }
203
+ ]
204
+ break
205
+ case 'zoom-out':
206
+ keyframes = [
207
+ { transform: `scale(${scale * 1.8})`, opacity: 0 },
208
+ { transform: `scale(${scale})`, opacity: 1 }
209
+ ]
210
+ break
211
+ default:
212
+ keyframes = [
213
+ { transform: `scale(${scale})`, opacity: 0.8 },
214
+ { transform: `scale(${scale})`, opacity: 1 }
215
+ ]
216
+ break
217
+ }
218
+
219
+ // 临时防止滚动条跳动
220
+ const wrapper = document.querySelector('.canvas-slide-wrapper') as HTMLElement
221
+ if (wrapper) {
222
+ wrapper.style.overflow = 'hidden'
223
+ }
224
+
225
+ slideEl.style.transformOrigin = 'center center'
226
+ slideEl.style.pointerEvents = 'none'
227
+
228
+ const currentAnim = slideEl.animate(keyframes, {
229
+ duration: dur,
230
+ easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)'
231
+ })
232
+ activeAnimation = currentAnim
233
+
234
+ const cleanup = () => {
235
+ if (activeAnimation !== currentAnim) return
236
+ activeAnimation = null
237
+ if (slideEl) {
238
+ // 关键:强制保留当前缩放比例 scale(scale),绝不能置空清除!
239
+ slideEl.style.transform = `scale(${scale})`
240
+ slideEl.style.transformOrigin = 'center center'
241
+ slideEl.style.opacity = ''
242
+ slideEl.style.clipPath = ''
243
+ slideEl.style.pointerEvents = ''
244
+ }
245
+ if (wrapper) {
246
+ wrapper.style.overflow = ''
247
+ }
248
+ }
249
+
250
+ currentAnim.onfinish = cleanup
251
+ currentAnim.oncancel = cleanup
252
+ }
253
+
254
+ const hoveredTransition = ref<string | null>(null)
255
+
256
+ function onHoverTransition(val: string) {
257
+ if (hoveredTransition.value === val) return
258
+ hoveredTransition.value = val
259
+ playPreview(val)
260
+ }
261
+
262
+ function onLeaveTransition(val: string) {
263
+ if (hoveredTransition.value === val) {
264
+ hoveredTransition.value = null
265
+ }
266
+ }
267
+
268
+ function selectTransition(val: string) {
269
+ emit('update:activeSlideTransition', val)
270
+ }
271
+
272
+ const transitionIcons: Record<string, { default: string; selected: string }> = {
273
+ 'none': { default: 'icon-wudonghua', selected: 'icon-wudonghua-xuanzhong' },
274
+ 'random': { default: 'icon-suiji', selected: 'icon-suiji-xuanzhong' },
275
+ 'push-left-right': { default: 'icon-zuoyoutuiyi', selected: 'icon-zuoyoutuiyi-xuanzhong' },
276
+ 'push-up-down': { default: 'icon-shangxiatuiyi', selected: 'icon-shangxiatuiyi-xuanzhong' },
277
+ 'push-left-right-3d': { default: 'icon-a-zuoyoutuiyi3D', selected: 'icon-a-zuoyoutuiyi3D-xuanzhong' },
278
+ 'push-up-down-3d': { default: 'icon-a-shangxiatuiyi3D', selected: 'icon-a-shangxiatuiyi3D-xuanzhong' },
279
+ 'fade': { default: 'icon-danrudanchu', selected: 'icon-danrudanchu-xuanzhong' },
280
+ 'rotate': { default: 'icon-xuanzhuan', selected: 'icon-xuanzhuan-xuanzhong' },
281
+ 'expand-up-down': { default: 'icon-shangxiazhankai', selected: 'icon-shangxiazhankai-xuanzhong' },
282
+ 'expand-left-right': { default: 'icon-zuoyouzhankai', selected: 'icon-zuoyouzhankai-xuanzhong' },
283
+ 'zoom-in': { default: 'icon-fangda1', selected: 'icon-fangda-xuanzhong' },
284
+ 'zoom-out': { default: 'icon-suoxiao1', selected: 'icon-suoxiao-xuanzhong' }
285
+ }
286
+
287
+ function getTransitionIcon(type: string, isSelected: boolean): string {
288
+ const iconPair = transitionIcons[type]
289
+ if (!iconPair) return isSelected ? 'icon-suiji-xuanzhong' : 'icon-suiji'
290
+ return isSelected ? iconPair.selected : iconPair.default
291
+ }
292
+
293
+ function handleApplyAll() {
294
+ emit('applyTransitionToAll', {
295
+ transition: props.activeSlideTransition,
296
+ duration: duration.value,
297
+ autoAdvance: autoAdvance.value,
298
+ autoAdvanceDelay: autoAdvanceDelay.value,
299
+ clickAdvance: clickAdvance.value
300
+ })
301
+ }
302
+ </script>
303
+
304
+ <template>
305
+ <div class="sidebar-transition-panel">
306
+ <!-- 效果区域标题及预览按钮 -->
307
+ <div class="transition-section-header">
308
+ <span class="section-title">效果</span>
309
+ <button type="button" class="preview-btn" @click="playPreview()">
310
+ <svg viewBox="0 0 24 24" class="play-icon">
311
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" />
312
+ <polygon points="10,8 16,12 10,16" fill="currentColor" />
313
+ </svg>
314
+ <span>预览</span>
315
+ </button>
316
+ </div>
317
+
318
+ <!-- 12个动画效果卡片网格 (2列布局) -->
319
+ <div class="transition-grid">
320
+ <div
321
+ v-for="item in transitionOptions"
322
+ :key="item.value"
323
+ class="transition-item"
324
+ :class="{ active: activeSlideTransition === item.value }"
325
+ @mouseenter="onHoverTransition(item.value)"
326
+ @mouseleave="onLeaveTransition(item.value)"
327
+ @click="selectTransition(item.value)"
328
+ >
329
+ <div class="transition-card">
330
+ <svg viewBox="0 0 1024 1024" class="transition-icon-svg" aria-hidden="true">
331
+ <use
332
+ :xlink:href="`#${getTransitionIcon(item.value, activeSlideTransition === item.value)}`"
333
+ :href="`#${getTransitionIcon(item.value, activeSlideTransition === item.value)}`"
334
+ />
335
+ </svg>
336
+ </div>
337
+ <span class="transition-name">{{ item.name }}</span>
338
+ </div>
339
+ </div>
340
+
341
+ <!-- 底部配置区域 -->
342
+ <div class="transition-settings-section">
343
+ <!-- 持续时间 (统一下拉风格) -->
344
+ <div class="setting-item">
345
+ <div class="setting-label">持续时间</div>
346
+ <el-popover
347
+ v-model:visible="isDurationPopoverVisible"
348
+ placement="bottom-start"
349
+ :width="durationTriggerWidth"
350
+ trigger="click"
351
+ popper-class="autofit-popover-panel"
352
+ :show-arrow="false"
353
+ :offset="4"
354
+ @before-enter="onDurationPopoverShow"
355
+ >
356
+ <template #reference>
357
+ <div
358
+ ref="durationTriggerRef"
359
+ class="custom-select-trigger"
360
+ :class="{ 'is-active': isDurationPopoverVisible }"
361
+ >
362
+ <span class="select-trigger-text">{{ currentDurationLabel }}</span>
363
+ <svg
364
+ viewBox="0 0 1024 1024"
365
+ width="10"
366
+ height="10"
367
+ fill="currentColor"
368
+ class="select-trigger-arrow"
369
+ :class="{ 'is-open': isDurationPopoverVisible }"
370
+ >
371
+ <path d="M831.872 340.777L512 652.672 192.128 340.777a31.968 31.968 0 0 0-45.12 45.312l342.4 333.568a32 32 0 0 0 45.184 0l342.4-333.568a31.968 31.968 0 0 0-45.12-45.312z"></path>
372
+ </svg>
373
+ </div>
374
+ </template>
375
+
376
+ <div class="autofit-menu-container">
377
+ <div
378
+ v-for="opt in durationOptions"
379
+ :key="opt.value"
380
+ class="autofit-menu-item"
381
+ :class="{ 'is-selected': duration === opt.value }"
382
+ @click="selectDuration(opt.value)"
383
+ >
384
+ <span class="autofit-item-label">{{ opt.label }}</span>
385
+ <svg
386
+ v-if="duration === opt.value"
387
+ viewBox="0 0 24 24"
388
+ width="15"
389
+ height="15"
390
+ fill="none"
391
+ stroke="currentColor"
392
+ stroke-width="2.5"
393
+ stroke-linecap="round"
394
+ stroke-linejoin="round"
395
+ class="autofit-check-icon"
396
+ >
397
+ <polyline points="20 6 9 17 4 12" />
398
+ </svg>
399
+ </div>
400
+ </div>
401
+ </el-popover>
402
+ </div>
403
+
404
+ <!-- 自动切页 + 步进输入框 -->
405
+ <div class="setting-row">
406
+ <el-checkbox v-model="autoAdvance" class="transition-checkbox">自动切页</el-checkbox>
407
+ <div class="stepper-box" :class="{ disabled: !autoAdvance }">
408
+ <input
409
+ type="number"
410
+ class="stepper-input"
411
+ v-model.number="autoAdvanceDelay"
412
+ :disabled="!autoAdvance"
413
+ step="0.5"
414
+ min="0.1"
415
+ max="60"
416
+ />
417
+ <span class="stepper-unit">秒</span>
418
+ <div class="stepper-arrows">
419
+ <button type="button" class="stepper-btn" :disabled="!autoAdvance" @click.stop="stepUp">
420
+ <svg viewBox="0 0 1024 1024" width="8" height="8" fill="currentColor">
421
+ <path d="M512 320l320 384H192z" />
422
+ </svg>
423
+ </button>
424
+ <button type="button" class="stepper-btn" :disabled="!autoAdvance" @click.stop="stepDown">
425
+ <svg viewBox="0 0 1024 1024" width="8" height="8" fill="currentColor">
426
+ <path d="M512 704l-320-384h640z" />
427
+ </svg>
428
+ </button>
429
+ </div>
430
+ </div>
431
+ </div>
432
+
433
+ <!-- 单击鼠标时换片 -->
434
+ <div class="setting-row">
435
+ <el-checkbox v-model="clickAdvance" class="transition-checkbox">单击鼠标时换片</el-checkbox>
436
+ </div>
437
+
438
+ <!-- 应用于所有幻灯片 -->
439
+ <button type="button" class="transition-apply-all-btn" @click="handleApplyAll">
440
+ 应用于所有幻灯片
441
+ </button>
442
+ </div>
443
+ </div>
444
+ </template>
445
+
446
+ <style scoped lang="scss">
447
+ .sidebar-transition-panel {
448
+ display: flex;
449
+ flex-direction: column;
450
+ padding: 16px 15px;
451
+ box-sizing: border-box;
452
+ overflow-y: auto;
453
+ max-height: 100%;
454
+ width: 288px;
455
+ }
456
+
457
+ .transition-section-header {
458
+ display: flex;
459
+ align-items: center;
460
+ justify-content: space-between;
461
+ margin-bottom: 12px;
462
+
463
+ .section-title {
464
+ font-size: 14px;
465
+ font-weight: 600;
466
+ color: #1e293b;
467
+ letter-spacing: 0.2px;
468
+ }
469
+
470
+ .preview-btn {
471
+ display: flex;
472
+ align-items: center;
473
+ gap: 4px;
474
+ background: transparent;
475
+ border: none;
476
+ cursor: pointer;
477
+ color: #1e6fff;
478
+ font-size: 13px;
479
+ font-weight: 500;
480
+ padding: 2px 4px;
481
+ transition: opacity 0.15s ease;
482
+
483
+ &:hover {
484
+ opacity: 0.85;
485
+ }
486
+
487
+ .play-icon {
488
+ width: 14px;
489
+ height: 14px;
490
+ }
491
+ }
492
+ }
493
+
494
+ .transition-grid {
495
+ display: grid;
496
+ grid-template-columns: repeat(2, 124px);
497
+ gap: 12px 10px;
498
+ justify-content: space-between;
499
+ }
500
+
501
+ .transition-item {
502
+ display: flex;
503
+ flex-direction: column;
504
+ align-items: center;
505
+ cursor: pointer;
506
+ user-select: none;
507
+ width: 124px;
508
+
509
+ &:hover {
510
+ .transition-card {
511
+ border-color: #93c5fd;
512
+ background: #ffffff;
513
+ }
514
+ .transition-name {
515
+ color: #1e6fff;
516
+ }
517
+ }
518
+
519
+ &.active {
520
+ .transition-card {
521
+ border: 1.5px solid #1e6fff !important;
522
+ background: #ffffff !important;
523
+ box-shadow: 0 0 0 1px rgba(30, 111, 255, 0.08);
524
+ }
525
+
526
+ .transition-name {
527
+ color: #1e6fff;
528
+ font-weight: 600;
529
+ }
530
+ }
531
+ }
532
+
533
+ .transition-card {
534
+ width: 124px;
535
+ height: 72px;
536
+ border: 1px solid #e2e8f0;
537
+ background: #fafafc;
538
+ border-radius: 8px;
539
+ display: flex;
540
+ align-items: center;
541
+ justify-content: center;
542
+ transition: all 0.15s ease;
543
+ box-sizing: border-box;
544
+ flex-shrink: 0;
545
+ }
546
+
547
+ .transition-icon-svg {
548
+ width: 40px;
549
+ height: 40px;
550
+ flex-shrink: 0;
551
+ display: block;
552
+ }
553
+
554
+ .transition-name {
555
+ font-size: 12px;
556
+ color: #334155;
557
+ font-weight: 500;
558
+ text-align: center;
559
+ line-height: 1.2;
560
+ margin-top: 6px;
561
+ white-space: nowrap;
562
+ transition: color 0.15s ease;
563
+ }
564
+
565
+ .transition-settings-section {
566
+ border-top: 1px solid #f1f5f9;
567
+ padding-top: 16px;
568
+ margin-top: 16px;
569
+ display: flex;
570
+ flex-direction: column;
571
+ gap: 14px;
572
+ }
573
+
574
+ .setting-item {
575
+ display: flex;
576
+ flex-direction: column;
577
+ gap: 6px;
578
+
579
+ .setting-label {
580
+ font-size: 13px;
581
+ font-weight: 500;
582
+ color: #1e293b;
583
+ }
584
+ }
585
+
586
+ /* Custom Select Trigger for Duration */
587
+ .custom-select-trigger {
588
+ display: flex;
589
+ align-items: center;
590
+ justify-content: space-between;
591
+ width: 100%;
592
+ height: 34px;
593
+ background-color: #fafafa;
594
+ border: 1px solid #e2e8f0;
595
+ border-radius: 6px;
596
+ padding: 0 10px;
597
+ box-sizing: border-box;
598
+ cursor: pointer;
599
+ transition: all 0.15s ease;
600
+ user-select: none;
601
+
602
+ &:hover {
603
+ border-color: #cbd5e1;
604
+ }
605
+
606
+ &.is-active {
607
+ border-color: #1e6fff;
608
+ background-color: #ffffff;
609
+ box-shadow: 0 0 0 1px #1e6fff;
610
+ }
611
+ }
612
+
613
+ .select-trigger-text {
614
+ font-size: 13px;
615
+ font-weight: 500;
616
+ color: #1e293b;
617
+ }
618
+
619
+ .select-trigger-arrow {
620
+ color: #94a3b8;
621
+ transition: transform 0.2s ease;
622
+
623
+ &.is-open {
624
+ transform: rotate(180deg);
625
+ }
626
+ }
627
+
628
+ .autofit-menu-container {
629
+ display: flex;
630
+ flex-direction: column;
631
+ gap: 2px;
632
+ }
633
+
634
+ .autofit-menu-item {
635
+ display: flex;
636
+ align-items: center;
637
+ justify-content: space-between;
638
+ height: 34px;
639
+ padding: 0 10px;
640
+ border-radius: 6px;
641
+ cursor: pointer;
642
+ transition: all 0.15s ease;
643
+ user-select: none;
644
+
645
+ &:hover {
646
+ background-color: #f8fafc;
647
+ }
648
+
649
+ &.is-selected {
650
+ background-color: #eef2ff;
651
+
652
+ .autofit-item-label {
653
+ color: #1e6fff;
654
+ font-weight: 500;
655
+ }
656
+ }
657
+ }
658
+
659
+ .autofit-item-label {
660
+ font-size: 13px;
661
+ color: #1e293b;
662
+ transition: color 0.15s ease;
663
+ }
664
+
665
+ .autofit-check-icon {
666
+ color: #1e6fff;
667
+ flex-shrink: 0;
668
+ }
669
+
670
+ .setting-row {
671
+ display: flex;
672
+ align-items: center;
673
+ justify-content: space-between;
674
+ height: 32px;
675
+ }
676
+
677
+ .transition-checkbox {
678
+ :deep(.el-checkbox__label) {
679
+ font-size: 13px;
680
+ color: #1e293b;
681
+ }
682
+
683
+ :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
684
+ background-color: #1e6fff;
685
+ border-color: #1e6fff;
686
+ }
687
+ }
688
+
689
+ .stepper-box {
690
+ display: flex;
691
+ align-items: center;
692
+ border: 1px solid #e2e8f0;
693
+ border-radius: 6px;
694
+ background: #ffffff;
695
+ height: 30px;
696
+ padding: 0 4px 0 8px;
697
+ box-sizing: border-box;
698
+ width: 104px;
699
+ transition: all 0.15s ease;
700
+
701
+ &.disabled {
702
+ opacity: 0.5;
703
+ pointer-events: none;
704
+ background: #f8fafc;
705
+ }
706
+
707
+ &:hover:not(.disabled),
708
+ &:focus-within:not(.disabled) {
709
+ border-color: #1e6fff;
710
+ }
711
+
712
+ .stepper-input {
713
+ width: 38px;
714
+ border: none;
715
+ outline: none;
716
+ background: transparent;
717
+ font-size: 13px;
718
+ color: #1e293b;
719
+ text-align: center;
720
+ -moz-appearance: textfield;
721
+
722
+ &::-webkit-outer-spin-button,
723
+ &::-webkit-inner-spin-button {
724
+ -webkit-appearance: none;
725
+ margin: 0;
726
+ }
727
+ }
728
+
729
+ .stepper-unit {
730
+ font-size: 12px;
731
+ color: #64748b;
732
+ margin: 0 4px;
733
+ user-select: none;
734
+ }
735
+
736
+ .stepper-arrows {
737
+ display: flex;
738
+ flex-direction: column;
739
+ justify-content: center;
740
+ height: 100%;
741
+ margin-left: auto;
742
+ }
743
+
744
+ .stepper-btn {
745
+ border: none;
746
+ background: transparent;
747
+ cursor: pointer;
748
+ padding: 0;
749
+ height: 11px;
750
+ display: flex;
751
+ align-items: center;
752
+ justify-content: center;
753
+ color: #94a3b8;
754
+ line-height: 1;
755
+
756
+ &:hover:not(:disabled) {
757
+ color: #1e6fff;
758
+ }
759
+
760
+ &:disabled {
761
+ cursor: not-allowed;
762
+ }
763
+ }
764
+ }
765
+
766
+ .transition-apply-all-btn {
767
+ width: 100%;
768
+ height: 38px;
769
+ border-radius: 8px;
770
+ background: #1e6fff;
771
+ color: #ffffff;
772
+ font-size: 13.5px;
773
+ font-weight: 500;
774
+ border: none;
775
+ cursor: pointer;
776
+ display: flex;
777
+ align-items: center;
778
+ justify-content: center;
779
+ transition: background-color 0.15s ease;
780
+ margin-top: 4px;
781
+ user-select: none;
782
+
783
+ &:hover {
784
+ background: #1656cb;
785
+ }
786
+
787
+ &:active {
788
+ background: #0e4bb5;
789
+ }
790
+ }
791
+ </style>