CatPtain commited on
Commit
f5b4781
·
verified ·
1 Parent(s): 4db4b63

Upload 71 files

Browse files
frontend/src/assets/styles/font.scss CHANGED
@@ -1,9 +1,9 @@
1
- $fonts: 'SourceHanSans', 'SourceHanSerif', 'FangZhengHeiTi', 'FangZhengKaiTi', 'FangZhengShuSong', 'FangZhengFangSong', 'AlibabaPuHuiTi', 'ZhuQueFangSong', 'LXGWWenKai', 'WenDingPLKaiTi', 'DeYiHei', 'MiSans', 'CangerXiaowanzi', 'YousheTitleBlack', 'FengguangMingrui', 'ShetuModernSquare', 'ZcoolHappy', 'ZizhiQuXiMai', 'SucaiJishiKangkang', 'SucaiJishiCoolSquare', 'TuniuRounded', 'RuiziZhenyan';
2
-
3
- @each $font in $fonts {
4
- @font-face {
5
- font-display: swap;
6
- font-family: $font;
7
- src: url('https://asset.pptist.cn/font/#{$font}.woff2') format('woff2');
8
- }
9
  }
 
1
+ $fonts: 'SourceHanSans', 'SourceHanSerif', 'FangZhengHeiTi', 'FangZhengKaiTi', 'FangZhengShuSong', 'FangZhengFangSong', 'AlibabaPuHuiTi', 'ZhuQueFangSong', 'LXGWWenKai', 'WenDingPLKaiTi', 'DeYiHei', 'MiSans', 'CangerXiaowanzi', 'YousheTitleBlack', 'FengguangMingrui', 'ShetuModernSquare', 'ZcoolHappy', 'ZizhiQuXiMai', 'SucaiJishiKangkang', 'SucaiJishiCoolSquare', 'TuniuRounded', 'RuiziZhenyan';
2
+
3
+ @each $font in $fonts {
4
+ @font-face {
5
+ font-display: swap;
6
+ font-family: $font;
7
+ src: url('https://asset.pptist.cn/font/#{$font}.woff2') format('woff2');
8
+ }
9
  }
frontend/src/configs/animation.ts CHANGED
@@ -1,234 +1,234 @@
1
- import type { TurningMode } from '@/types/slides'
2
-
3
- export const ANIMATION_DEFAULT_DURATION = 1000
4
- export const ANIMATION_DEFAULT_TRIGGER = 'click'
5
- export const ANIMATION_CLASS_PREFIX = 'animate__'
6
-
7
- export const ENTER_ANIMATIONS = [
8
- {
9
- type: 'bounce',
10
- name: '弹跳',
11
- children: [
12
- { name: '弹入', value: 'bounceIn' },
13
- { name: '向右弹入', value: 'bounceInLeft' },
14
- { name: '向左弹入', value: 'bounceInRight' },
15
- { name: '向上弹入', value: 'bounceInUp' },
16
- { name: '向下弹入', value: 'bounceInDown' },
17
- ],
18
- },
19
- {
20
- type: 'fade',
21
- name: '浮现',
22
- children: [
23
- { name: '浮入', value: 'fadeIn' },
24
- { name: '向下浮入', value: 'fadeInDown' },
25
- { name: '向下长距浮入', value: 'fadeInDownBig' },
26
- { name: '向右浮入', value: 'fadeInLeft' },
27
- { name: '向右长距浮入', value: 'fadeInLeftBig' },
28
- { name: '向左浮入', value: 'fadeInRight' },
29
- { name: '向左长距浮入', value: 'fadeInRightBig' },
30
- { name: '向上浮入', value: 'fadeInUp' },
31
- { name: '向上长距浮入', value: 'fadeInUpBig' },
32
- { name: '从左上浮入', value: 'fadeInTopLeft' },
33
- { name: '从右上浮入', value: 'fadeInTopRight' },
34
- { name: '从左下浮入', value: 'fadeInBottomLeft' },
35
- { name: '从右下浮入', value: 'fadeInBottomRight' },
36
- ],
37
- },
38
- {
39
- type: 'rotate',
40
- name: '旋转',
41
- children: [
42
- { name: '旋转进入', value: 'rotateIn' },
43
- { name: '绕左下进入', value: 'rotateInDownLeft' },
44
- { name: '绕右下进入', value: 'rotateInDownRight' },
45
- { name: '绕左上进入', value: 'rotateInUpLeft' },
46
- { name: '绕右上进入', value: 'rotateInUpRight' },
47
- ],
48
- },
49
- {
50
- type: 'zoom',
51
- name: '缩放',
52
- children: [
53
- { name: '放大进入', value: 'zoomIn' },
54
- { name: '向下放大进入', value: 'zoomInDown' },
55
- { name: '从左放大进入', value: 'zoomInLeft' },
56
- { name: '从右放大进入', value: 'zoomInRight' },
57
- { name: '向上放大进入', value: 'zoomInUp' },
58
- ],
59
- },
60
- {
61
- type: 'slide',
62
- name: '滑入',
63
- children: [
64
- { name: '向下滑入', value: 'slideInDown' },
65
- { name: '从右滑入', value: 'slideInLeft' },
66
- { name: '从左滑入', value: 'slideInRight' },
67
- { name: '向上滑入', value: 'slideInUp' },
68
- ],
69
- },
70
- {
71
- type: 'flip',
72
- name: '翻转',
73
- children: [
74
- { name: 'X轴翻转进入', value: 'flipInX' },
75
- { name: 'Y轴翻转进入', value: 'flipInY' },
76
- ],
77
- },
78
- {
79
- type: 'back',
80
- name: '放大滑入',
81
- children: [
82
- { name: '向下放大滑入', value: 'backInDown' },
83
- { name: '从左放大滑入', value: 'backInLeft' },
84
- { name: '从右放大滑入', value: 'backInRight' },
85
- { name: '向上放大滑入', value: 'backInUp' },
86
- ],
87
- },
88
- {
89
- type: 'lightSpeed',
90
- name: '飞入',
91
- children: [
92
- { name: '从右飞入', value: 'lightSpeedInRight' },
93
- { name: '从左飞入', value: 'lightSpeedInLeft' },
94
- ],
95
- },
96
- ]
97
-
98
- export const EXIT_ANIMATIONS = [
99
- {
100
- type: 'bounce',
101
- name: '弹跳',
102
- children: [
103
- { name: '弹出', value: 'bounceOut' },
104
- { name: '向左弹出', value: 'bounceOutLeft' },
105
- { name: '向右弹出', value: 'bounceOutRight' },
106
- { name: '向上弹出', value: 'bounceOutUp' },
107
- { name: '向下弹出', value: 'bounceOutDown' },
108
- ],
109
- },
110
- {
111
- type: 'fade',
112
- name: '浮现',
113
- children: [
114
- { name: '浮出', value: 'fadeOut' },
115
- { name: '向下浮出', value: 'fadeOutDown' },
116
- { name: '向下长距浮出', value: 'fadeOutDownBig' },
117
- { name: '向左浮出', value: 'fadeOutLeft' },
118
- { name: '向左长距浮出', value: 'fadeOutLeftBig' },
119
- { name: '向右浮出', value: 'fadeOutRight' },
120
- { name: '向右长距浮出', value: 'fadeOutRightBig' },
121
- { name: '向上浮出', value: 'fadeOutUp' },
122
- { name: '向上长距浮出', value: 'fadeOutUpBig' },
123
- { name: '从左上浮出', value: 'fadeOutTopLeft' },
124
- { name: '从右上浮出', value: 'fadeOutTopRight' },
125
- { name: '从左下浮出', value: 'fadeOutBottomLeft' },
126
- { name: '从右下浮出', value: 'fadeOutBottomRight' },
127
- ],
128
- },
129
- {
130
- type: 'rotate',
131
- name: '旋转',
132
- children: [
133
- { name: '旋转退出', value: 'rotateOut' },
134
- { name: '绕左下退出', value: 'rotateOutDownLeft' },
135
- { name: '绕右下退出', value: 'rotateOutDownRight' },
136
- { name: '绕左上退出', value: 'rotateOutUpLeft' },
137
- { name: '绕右上退出', value: 'rotateOutUpRight' },
138
- ],
139
- },
140
- {
141
- type: 'zoom',
142
- name: '缩放',
143
- children: [
144
- { name: '缩小退出', value: 'zoomOut' },
145
- { name: '向下缩小退出', value: 'zoomOutDown' },
146
- { name: '从左缩小退出', value: 'zoomOutLeft' },
147
- { name: '从右缩小退出', value: 'zoomOutRight' },
148
- { name: '向上缩小退出', value: 'zoomOutUp' },
149
- ],
150
- },
151
- {
152
- type: 'slide',
153
- name: '滑出',
154
- children: [
155
- { name: '向下滑出', value: 'slideOutDown' },
156
- { name: '从左滑出', value: 'slideOutLeft' },
157
- { name: '从右滑出', value: 'slideOutRight' },
158
- { name: '向上滑出', value: 'slideOutUp' },
159
- ],
160
- },
161
- {
162
- type: 'flip',
163
- name: '翻转',
164
- children: [
165
- { name: 'X轴翻转退出', value: 'flipOutX' },
166
- { name: 'Y轴翻转退出', value: 'flipOutY' },
167
- ],
168
- },
169
- {
170
- type: 'back',
171
- name: '缩小滑出',
172
- children: [
173
- { name: '向下缩小滑出', value: 'backOutDown' },
174
- { name: '从左缩小滑出', value: 'backOutLeft' },
175
- { name: '从右缩小滑出', value: 'backOutRight' },
176
- { name: '向上缩小滑出', value: 'backOutUp' },
177
- ],
178
- },
179
- {
180
- type: 'lightSpeed',
181
- name: '飞出',
182
- children: [
183
- { name: '从右飞出', value: 'lightSpeedOutRight' },
184
- { name: '从左飞出', value: 'lightSpeedOutLeft' },
185
- ],
186
- },
187
- ]
188
-
189
- export const ATTENTION_ANIMATIONS = [
190
- {
191
- type: 'shake',
192
- name: '晃动',
193
- children: [
194
- { name: '左右摇晃', value: 'shakeX' },
195
- { name: '上下摇晃', value: 'shakeY' },
196
- { name: '摇头', value: 'headShake' },
197
- { name: '摆动', value: 'swing' },
198
- { name: '晃动', value: 'wobble' },
199
- { name: '惊恐', value: 'tada' },
200
- { name: '果冻', value: 'jello' },
201
- ],
202
- },
203
- {
204
- type: 'other',
205
- name: '其他',
206
- children: [
207
- { name: '弹跳', value: 'bounce' },
208
- { name: '闪烁', value: 'flash' },
209
- { name: '脉搏', value: 'pulse' },
210
- { name: '橡皮筋', value: 'rubberBand' },
211
- { name: '心跳(快)', value: 'heartBeat' },
212
- ],
213
- },
214
- ]
215
-
216
- interface SlideAnimation {
217
- label: string
218
- value: TurningMode
219
- }
220
-
221
- export const SLIDE_ANIMATIONS: SlideAnimation[] = [
222
- { label: '无', value: 'no' },
223
- { label: '随机', value: 'random' },
224
- { label: '左右推移', value: 'slideX' },
225
- { label: '上下推移', value: 'slideY' },
226
- { label: '左右推移(3D)', value: 'slideX3D' },
227
- { label: '上下推移(3D)', value: 'slideY3D' },
228
- { label: '淡入淡出', value: 'fade' },
229
- { label: '旋转', value: 'rotate' },
230
- { label: '上下展开', value: 'scaleY' },
231
- { label: '左右展开', value: 'scaleX' },
232
- { label: '放大', value: 'scale' },
233
- { label: '缩小', value: 'scaleReverse' },
234
  ]
 
1
+ import type { TurningMode } from '@/types/slides'
2
+
3
+ export const ANIMATION_DEFAULT_DURATION = 1000
4
+ export const ANIMATION_DEFAULT_TRIGGER = 'click'
5
+ export const ANIMATION_CLASS_PREFIX = 'animate__'
6
+
7
+ export const ENTER_ANIMATIONS = [
8
+ {
9
+ type: 'bounce',
10
+ name: '弹跳',
11
+ children: [
12
+ { name: '弹入', value: 'bounceIn' },
13
+ { name: '向右弹入', value: 'bounceInLeft' },
14
+ { name: '向左弹入', value: 'bounceInRight' },
15
+ { name: '向上弹入', value: 'bounceInUp' },
16
+ { name: '向下弹入', value: 'bounceInDown' },
17
+ ],
18
+ },
19
+ {
20
+ type: 'fade',
21
+ name: '浮现',
22
+ children: [
23
+ { name: '浮入', value: 'fadeIn' },
24
+ { name: '向下浮入', value: 'fadeInDown' },
25
+ { name: '向下长距浮入', value: 'fadeInDownBig' },
26
+ { name: '向右浮入', value: 'fadeInLeft' },
27
+ { name: '向右长距浮入', value: 'fadeInLeftBig' },
28
+ { name: '向左浮入', value: 'fadeInRight' },
29
+ { name: '向左长距浮入', value: 'fadeInRightBig' },
30
+ { name: '向上浮入', value: 'fadeInUp' },
31
+ { name: '向上长距浮入', value: 'fadeInUpBig' },
32
+ { name: '从左上浮入', value: 'fadeInTopLeft' },
33
+ { name: '从右上浮入', value: 'fadeInTopRight' },
34
+ { name: '从左下浮入', value: 'fadeInBottomLeft' },
35
+ { name: '从右下浮入', value: 'fadeInBottomRight' },
36
+ ],
37
+ },
38
+ {
39
+ type: 'rotate',
40
+ name: '旋转',
41
+ children: [
42
+ { name: '旋转进入', value: 'rotateIn' },
43
+ { name: '绕左下进入', value: 'rotateInDownLeft' },
44
+ { name: '绕右下进入', value: 'rotateInDownRight' },
45
+ { name: '绕左上进入', value: 'rotateInUpLeft' },
46
+ { name: '绕右上进入', value: 'rotateInUpRight' },
47
+ ],
48
+ },
49
+ {
50
+ type: 'zoom',
51
+ name: '缩放',
52
+ children: [
53
+ { name: '放大进入', value: 'zoomIn' },
54
+ { name: '向下放大进入', value: 'zoomInDown' },
55
+ { name: '从左放大进入', value: 'zoomInLeft' },
56
+ { name: '从右放大进入', value: 'zoomInRight' },
57
+ { name: '向上放大进入', value: 'zoomInUp' },
58
+ ],
59
+ },
60
+ {
61
+ type: 'slide',
62
+ name: '滑入',
63
+ children: [
64
+ { name: '向下滑入', value: 'slideInDown' },
65
+ { name: '从右滑入', value: 'slideInLeft' },
66
+ { name: '从左滑入', value: 'slideInRight' },
67
+ { name: '向上滑入', value: 'slideInUp' },
68
+ ],
69
+ },
70
+ {
71
+ type: 'flip',
72
+ name: '翻转',
73
+ children: [
74
+ { name: 'X轴翻转进入', value: 'flipInX' },
75
+ { name: 'Y轴翻转进入', value: 'flipInY' },
76
+ ],
77
+ },
78
+ {
79
+ type: 'back',
80
+ name: '放大滑入',
81
+ children: [
82
+ { name: '向下放大滑入', value: 'backInDown' },
83
+ { name: '从左放大滑入', value: 'backInLeft' },
84
+ { name: '从右放大滑入', value: 'backInRight' },
85
+ { name: '向上放大滑入', value: 'backInUp' },
86
+ ],
87
+ },
88
+ {
89
+ type: 'lightSpeed',
90
+ name: '飞入',
91
+ children: [
92
+ { name: '从右飞入', value: 'lightSpeedInRight' },
93
+ { name: '从左飞入', value: 'lightSpeedInLeft' },
94
+ ],
95
+ },
96
+ ]
97
+
98
+ export const EXIT_ANIMATIONS = [
99
+ {
100
+ type: 'bounce',
101
+ name: '弹跳',
102
+ children: [
103
+ { name: '弹出', value: 'bounceOut' },
104
+ { name: '向左弹出', value: 'bounceOutLeft' },
105
+ { name: '向右弹出', value: 'bounceOutRight' },
106
+ { name: '向上弹出', value: 'bounceOutUp' },
107
+ { name: '向下弹出', value: 'bounceOutDown' },
108
+ ],
109
+ },
110
+ {
111
+ type: 'fade',
112
+ name: '浮现',
113
+ children: [
114
+ { name: '浮出', value: 'fadeOut' },
115
+ { name: '向下浮出', value: 'fadeOutDown' },
116
+ { name: '向下长距浮出', value: 'fadeOutDownBig' },
117
+ { name: '向左浮出', value: 'fadeOutLeft' },
118
+ { name: '向左长距浮出', value: 'fadeOutLeftBig' },
119
+ { name: '向右浮出', value: 'fadeOutRight' },
120
+ { name: '向右长距浮出', value: 'fadeOutRightBig' },
121
+ { name: '向上浮出', value: 'fadeOutUp' },
122
+ { name: '向上长距浮出', value: 'fadeOutUpBig' },
123
+ { name: '从左上浮出', value: 'fadeOutTopLeft' },
124
+ { name: '从右上浮出', value: 'fadeOutTopRight' },
125
+ { name: '从左下浮出', value: 'fadeOutBottomLeft' },
126
+ { name: '从右下浮出', value: 'fadeOutBottomRight' },
127
+ ],
128
+ },
129
+ {
130
+ type: 'rotate',
131
+ name: '旋转',
132
+ children: [
133
+ { name: '旋转退出', value: 'rotateOut' },
134
+ { name: '绕左下退出', value: 'rotateOutDownLeft' },
135
+ { name: '绕右下退出', value: 'rotateOutDownRight' },
136
+ { name: '绕左上退出', value: 'rotateOutUpLeft' },
137
+ { name: '绕右上退出', value: 'rotateOutUpRight' },
138
+ ],
139
+ },
140
+ {
141
+ type: 'zoom',
142
+ name: '缩放',
143
+ children: [
144
+ { name: '缩小退出', value: 'zoomOut' },
145
+ { name: '向下缩小退出', value: 'zoomOutDown' },
146
+ { name: '从左缩小退出', value: 'zoomOutLeft' },
147
+ { name: '从右缩小退出', value: 'zoomOutRight' },
148
+ { name: '向上缩小退出', value: 'zoomOutUp' },
149
+ ],
150
+ },
151
+ {
152
+ type: 'slide',
153
+ name: '滑出',
154
+ children: [
155
+ { name: '向下滑出', value: 'slideOutDown' },
156
+ { name: '从左滑出', value: 'slideOutLeft' },
157
+ { name: '从右滑出', value: 'slideOutRight' },
158
+ { name: '向上滑出', value: 'slideOutUp' },
159
+ ],
160
+ },
161
+ {
162
+ type: 'flip',
163
+ name: '翻转',
164
+ children: [
165
+ { name: 'X轴翻转退出', value: 'flipOutX' },
166
+ { name: 'Y轴翻转退出', value: 'flipOutY' },
167
+ ],
168
+ },
169
+ {
170
+ type: 'back',
171
+ name: '缩小滑出',
172
+ children: [
173
+ { name: '向下缩小滑出', value: 'backOutDown' },
174
+ { name: '从左缩小滑出', value: 'backOutLeft' },
175
+ { name: '从右缩小滑出', value: 'backOutRight' },
176
+ { name: '向上缩小滑出', value: 'backOutUp' },
177
+ ],
178
+ },
179
+ {
180
+ type: 'lightSpeed',
181
+ name: '飞出',
182
+ children: [
183
+ { name: '从右飞出', value: 'lightSpeedOutRight' },
184
+ { name: '从左飞出', value: 'lightSpeedOutLeft' },
185
+ ],
186
+ },
187
+ ]
188
+
189
+ export const ATTENTION_ANIMATIONS = [
190
+ {
191
+ type: 'shake',
192
+ name: '晃动',
193
+ children: [
194
+ { name: '左右摇晃', value: 'shakeX' },
195
+ { name: '上下摇晃', value: 'shakeY' },
196
+ { name: '摇头', value: 'headShake' },
197
+ { name: '摆动', value: 'swing' },
198
+ { name: '晃动', value: 'wobble' },
199
+ { name: '惊恐', value: 'tada' },
200
+ { name: '果冻', value: 'jello' },
201
+ ],
202
+ },
203
+ {
204
+ type: 'other',
205
+ name: '其他',
206
+ children: [
207
+ { name: '弹跳', value: 'bounce' },
208
+ { name: '闪烁', value: 'flash' },
209
+ { name: '脉搏', value: 'pulse' },
210
+ { name: '橡皮筋', value: 'rubberBand' },
211
+ { name: '心跳(快)', value: 'heartBeat' },
212
+ ],
213
+ },
214
+ ]
215
+
216
+ interface SlideAnimation {
217
+ label: string
218
+ value: TurningMode
219
+ }
220
+
221
+ export const SLIDE_ANIMATIONS: SlideAnimation[] = [
222
+ { label: '无', value: 'no' },
223
+ { label: '随机', value: 'random' },
224
+ { label: '左右推移', value: 'slideX' },
225
+ { label: '上下推移', value: 'slideY' },
226
+ { label: '左右推移(3D)', value: 'slideX3D' },
227
+ { label: '上下推移(3D)', value: 'slideY3D' },
228
+ { label: '淡入淡出', value: 'fade' },
229
+ { label: '旋转', value: 'rotate' },
230
+ { label: '上下展开', value: 'scaleY' },
231
+ { label: '左右展开', value: 'scaleX' },
232
+ { label: '放大', value: 'scale' },
233
+ { label: '缩小', value: 'scaleReverse' },
234
  ]
frontend/src/configs/theme.ts CHANGED
@@ -1,122 +1,122 @@
1
- export interface PresetTheme {
2
- background: string
3
- fontColor: string
4
- borderColor: string
5
- fontname: string
6
- colors: string[]
7
- }
8
-
9
- export const PRESET_THEMES: PresetTheme[] = [
10
- {
11
- background: '#ffffff',
12
- fontColor: '#333333',
13
- borderColor: '#41719c',
14
- fontname: '',
15
- colors: ['#5b9bd5', '#ed7d31', '#a5a5a5', '#ffc000', '#4472c4', '#70ad47'],
16
- },
17
- {
18
- background: '#ffffff',
19
- fontColor: '#333333',
20
- borderColor: '#5f6f1c',
21
- fontname: '',
22
- colors: ['#83992a', '#3c9670', '#44709d', '#a23b32', '#d87728', '#deb340'],
23
- },
24
- {
25
- background: '#ffffff',
26
- fontColor: '#333333',
27
- borderColor: '#a75f0a',
28
- fontname: '',
29
- colors: ['#e48312', '#bd582c', '#865640', '#9b8357', '#c2bc80', '#94a088'],
30
- },
31
- {
32
- background: '#ffffff',
33
- fontColor: '#333333',
34
- borderColor: '#7c91a8',
35
- fontname: '',
36
- colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
37
- },
38
- {
39
- background: '#ffffff',
40
- fontColor: '#333333',
41
- borderColor: '#688e19',
42
- fontname: '',
43
- colors: ['#90c225', '#54a121', '#e6b91e', '#e86618', '#c42f19', '#918756'],
44
- },
45
- {
46
- background: '#ffffff',
47
- fontColor: '#333333',
48
- borderColor: '#4495b0',
49
- fontname: '',
50
- colors: ['#1cade4', '#2683c6', '#27ced7', '#42ba97', '#3e8853', '#62a39f'],
51
- },
52
- {
53
- background: '#e9efd6',
54
- fontColor: '#333333',
55
- borderColor: '#782009',
56
- fontname: '',
57
- colors: ['#a5300f', '#de7e18', '#9f8351', '#728653', '#92aa4c', '#6aac91'],
58
- },
59
- {
60
- background: '#17444e',
61
- fontColor: '#ffffff',
62
- borderColor: '#800c0b',
63
- fontname: '',
64
- colors: ['#b01513', '#ea6312', '#e6b729', '#6bab90', '#55839a', '#9e5d9d'],
65
- },
66
- {
67
- background: '#36234d',
68
- fontColor: '#ffffff',
69
- borderColor: '#830949',
70
- fontname: '',
71
- colors: ['#b31166', '#e33d6f', '#e45f3c', '#e9943a', '#9b6bf2', '#d63cd0'],
72
- },
73
- {
74
- background: '#247fad',
75
- fontColor: '#ffffff',
76
- borderColor: '#032e45',
77
- fontname: '',
78
- colors: ['#052f61', '#a50e82', '#14967c', '#6a9e1f', '#e87d37', '#c62324'],
79
- },
80
- {
81
- background: '#103f55',
82
- fontColor: '#ffffff',
83
- borderColor: '#2d7f8a',
84
- fontname: '',
85
- colors: ['#40aebd', '#97e8d5', '#a1cf49', '#628f3e', '#f2df3a', '#fcb01c'],
86
- },
87
- {
88
- background: '#242367',
89
- fontColor: '#ffffff',
90
- borderColor: '#7d2b8d',
91
- fontname: '',
92
- colors: ['#ac3ec1', '#477bd1', '#46b298', '#90ba4c', '#dd9d31', '#e25345'],
93
- },
94
- {
95
- background: '#e4b75e',
96
- fontColor: '#333333',
97
- borderColor: '#b68317',
98
- fontname: '',
99
- colors: ['#a5644e', '#b58b80', '#c3986d', '#a19574', '#c17529', '#826277'],
100
- },
101
- {
102
- background: '#333333',
103
- fontColor: '#ffffff',
104
- borderColor: '#7c91a8',
105
- fontname: '',
106
- colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
107
- },
108
- {
109
- background: '#2b2b2d',
110
- fontColor: '#ffffff',
111
- borderColor: '#893011',
112
- fontname: '',
113
- colors: ['#bc451b', '#d3ba68', '#bb8640', '#ad9277', '#a55a43', '#ad9d7b'],
114
- },
115
- {
116
- background: '#171b1e',
117
- fontColor: '#ffffff',
118
- borderColor: '#505050',
119
- fontname: '',
120
- colors: ['#6f6f6f', '#bfbfa5', '#dbd084', '#e7bf5f', '#e9a039', '#cf7133'],
121
- },
122
  ]
 
1
+ export interface PresetTheme {
2
+ background: string
3
+ fontColor: string
4
+ borderColor: string
5
+ fontname: string
6
+ colors: string[]
7
+ }
8
+
9
+ export const PRESET_THEMES: PresetTheme[] = [
10
+ {
11
+ background: '#ffffff',
12
+ fontColor: '#333333',
13
+ borderColor: '#41719c',
14
+ fontname: '',
15
+ colors: ['#5b9bd5', '#ed7d31', '#a5a5a5', '#ffc000', '#4472c4', '#70ad47'],
16
+ },
17
+ {
18
+ background: '#ffffff',
19
+ fontColor: '#333333',
20
+ borderColor: '#5f6f1c',
21
+ fontname: '',
22
+ colors: ['#83992a', '#3c9670', '#44709d', '#a23b32', '#d87728', '#deb340'],
23
+ },
24
+ {
25
+ background: '#ffffff',
26
+ fontColor: '#333333',
27
+ borderColor: '#a75f0a',
28
+ fontname: '',
29
+ colors: ['#e48312', '#bd582c', '#865640', '#9b8357', '#c2bc80', '#94a088'],
30
+ },
31
+ {
32
+ background: '#ffffff',
33
+ fontColor: '#333333',
34
+ borderColor: '#7c91a8',
35
+ fontname: '',
36
+ colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
37
+ },
38
+ {
39
+ background: '#ffffff',
40
+ fontColor: '#333333',
41
+ borderColor: '#688e19',
42
+ fontname: '',
43
+ colors: ['#90c225', '#54a121', '#e6b91e', '#e86618', '#c42f19', '#918756'],
44
+ },
45
+ {
46
+ background: '#ffffff',
47
+ fontColor: '#333333',
48
+ borderColor: '#4495b0',
49
+ fontname: '',
50
+ colors: ['#1cade4', '#2683c6', '#27ced7', '#42ba97', '#3e8853', '#62a39f'],
51
+ },
52
+ {
53
+ background: '#e9efd6',
54
+ fontColor: '#333333',
55
+ borderColor: '#782009',
56
+ fontname: '',
57
+ colors: ['#a5300f', '#de7e18', '#9f8351', '#728653', '#92aa4c', '#6aac91'],
58
+ },
59
+ {
60
+ background: '#17444e',
61
+ fontColor: '#ffffff',
62
+ borderColor: '#800c0b',
63
+ fontname: '',
64
+ colors: ['#b01513', '#ea6312', '#e6b729', '#6bab90', '#55839a', '#9e5d9d'],
65
+ },
66
+ {
67
+ background: '#36234d',
68
+ fontColor: '#ffffff',
69
+ borderColor: '#830949',
70
+ fontname: '',
71
+ colors: ['#b31166', '#e33d6f', '#e45f3c', '#e9943a', '#9b6bf2', '#d63cd0'],
72
+ },
73
+ {
74
+ background: '#247fad',
75
+ fontColor: '#ffffff',
76
+ borderColor: '#032e45',
77
+ fontname: '',
78
+ colors: ['#052f61', '#a50e82', '#14967c', '#6a9e1f', '#e87d37', '#c62324'],
79
+ },
80
+ {
81
+ background: '#103f55',
82
+ fontColor: '#ffffff',
83
+ borderColor: '#2d7f8a',
84
+ fontname: '',
85
+ colors: ['#40aebd', '#97e8d5', '#a1cf49', '#628f3e', '#f2df3a', '#fcb01c'],
86
+ },
87
+ {
88
+ background: '#242367',
89
+ fontColor: '#ffffff',
90
+ borderColor: '#7d2b8d',
91
+ fontname: '',
92
+ colors: ['#ac3ec1', '#477bd1', '#46b298', '#90ba4c', '#dd9d31', '#e25345'],
93
+ },
94
+ {
95
+ background: '#e4b75e',
96
+ fontColor: '#333333',
97
+ borderColor: '#b68317',
98
+ fontname: '',
99
+ colors: ['#a5644e', '#b58b80', '#c3986d', '#a19574', '#c17529', '#826277'],
100
+ },
101
+ {
102
+ background: '#333333',
103
+ fontColor: '#ffffff',
104
+ borderColor: '#7c91a8',
105
+ fontname: '',
106
+ colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
107
+ },
108
+ {
109
+ background: '#2b2b2d',
110
+ fontColor: '#ffffff',
111
+ borderColor: '#893011',
112
+ fontname: '',
113
+ colors: ['#bc451b', '#d3ba68', '#bb8640', '#ad9277', '#a55a43', '#ad9d7b'],
114
+ },
115
+ {
116
+ background: '#171b1e',
117
+ fontColor: '#ffffff',
118
+ borderColor: '#505050',
119
+ fontname: '',
120
+ colors: ['#6f6f6f', '#bfbfa5', '#dbd084', '#e7bf5f', '#e9a039', '#cf7133'],
121
+ },
122
  ]
frontend/src/hooks/useAlignActiveElement.ts CHANGED
@@ -1,177 +1,177 @@
1
- import { storeToRefs } from 'pinia'
2
- import { useMainStore, useSlidesStore } from '@/store'
3
- import type { PPTElement } from '@/types/slides'
4
- import { ElementAlignCommands } from '@/types/edit'
5
- import { getElementListRange, getRectRotatedOffset } from '@/utils/element'
6
- import useHistorySnapshot from './useHistorySnapshot'
7
-
8
- interface RangeMap {
9
- [id: string]: ReturnType<typeof getElementListRange>
10
- }
11
-
12
- export default () => {
13
- const slidesStore = useSlidesStore()
14
- const { activeElementIdList, activeElementList } = storeToRefs(useMainStore())
15
- const { currentSlide } = storeToRefs(slidesStore)
16
-
17
- const { addHistorySnapshot } = useHistorySnapshot()
18
-
19
- /**
20
- * 对齐选中的元素
21
- * @param command 对齐方向
22
- */
23
- const alignActiveElement = (command: ElementAlignCommands) => {
24
- const { minX, maxX, minY, maxY } = getElementListRange(activeElementList.value)
25
- const elementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
26
-
27
- // 如果所选择的元素为组合元素的成员,需要计算该组合的整体范围
28
- const groupElementRangeMap: RangeMap = {}
29
- for (const activeElement of activeElementList.value) {
30
- if (activeElement.groupId && !groupElementRangeMap[activeElement.groupId]) {
31
- const groupElements = activeElementList.value.filter(item => item.groupId === activeElement.groupId)
32
- groupElementRangeMap[activeElement.groupId] = getElementListRange(groupElements)
33
- }
34
- }
35
-
36
- // 根据不同的命令,计算对齐的位置
37
- if (command === ElementAlignCommands.LEFT) {
38
- elementList.forEach(element => {
39
- if (activeElementIdList.value.includes(element.id)) {
40
- if (!element.groupId) {
41
- if ('rotate' in element && element.rotate) {
42
- const { offsetX } = getRectRotatedOffset({
43
- left: element.left,
44
- top: element.top,
45
- width: element.width,
46
- height: element.height,
47
- rotate: element.rotate,
48
- })
49
- element.left = minX - offsetX
50
- }
51
- else element.left = minX
52
- }
53
- else {
54
- const range = groupElementRangeMap[element.groupId]
55
- const offset = range.minX - minX
56
- element.left = element.left - offset
57
- }
58
- }
59
- })
60
- }
61
- else if (command === ElementAlignCommands.RIGHT) {
62
- elementList.forEach(element => {
63
- if (activeElementIdList.value.includes(element.id)) {
64
- if (!element.groupId) {
65
- const elWidth = element.type === 'line' ? Math.max(element.start[0], element.end[0]) : element.width
66
- if ('rotate' in element && element.rotate) {
67
- const { offsetX } = getRectRotatedOffset({
68
- left: element.left,
69
- top: element.top,
70
- width: element.width,
71
- height: element.height,
72
- rotate: element.rotate,
73
- })
74
- element.left = maxX - elWidth + offsetX
75
- }
76
- else element.left = maxX - elWidth
77
- }
78
- else {
79
- const range = groupElementRangeMap[element.groupId]
80
- const offset = range.maxX - maxX
81
- element.left = element.left - offset
82
- }
83
- }
84
- })
85
- }
86
- else if (command === ElementAlignCommands.TOP) {
87
- elementList.forEach(element => {
88
- if (activeElementIdList.value.includes(element.id)) {
89
- if (!element.groupId) {
90
- if ('rotate' in element && element.rotate) {
91
- const { offsetY } = getRectRotatedOffset({
92
- left: element.left,
93
- top: element.top,
94
- width: element.width,
95
- height: element.height,
96
- rotate: element.rotate,
97
- })
98
- element.top = minY - offsetY
99
- }
100
- else element.top = minY
101
- }
102
- else {
103
- const range = groupElementRangeMap[element.groupId]
104
- const offset = range.minY - minY
105
- element.top = element.top - offset
106
- }
107
- }
108
- })
109
- }
110
- else if (command === ElementAlignCommands.BOTTOM) {
111
- elementList.forEach(element => {
112
- if (activeElementIdList.value.includes(element.id)) {
113
- if (!element.groupId) {
114
- const elHeight = element.type === 'line' ? Math.max(element.start[1], element.end[1]) : element.height
115
- if ('rotate' in element && element.rotate) {
116
- const { offsetY } = getRectRotatedOffset({
117
- left: element.left,
118
- top: element.top,
119
- width: element.width,
120
- height: element.height,
121
- rotate: element.rotate,
122
- })
123
- element.top = maxY - elHeight + offsetY
124
- }
125
- else element.top = maxY - elHeight
126
- }
127
- else {
128
- const range = groupElementRangeMap[element.groupId]
129
- const offset = range.maxY - maxY
130
- element.top = element.top - offset
131
- }
132
- }
133
- })
134
- }
135
- else if (command === ElementAlignCommands.HORIZONTAL) {
136
- const horizontalCenter = (minX + maxX) / 2
137
- elementList.forEach(element => {
138
- if (activeElementIdList.value.includes(element.id)) {
139
- if (!element.groupId) {
140
- const elWidth = element.type === 'line' ? Math.max(element.start[0], element.end[0]) : element.width
141
- element.left = horizontalCenter - elWidth / 2
142
- }
143
- else {
144
- const range = groupElementRangeMap[element.groupId]
145
- const center = (range.maxX + range.minX) / 2
146
- const offset = center - horizontalCenter
147
- element.left = element.left - offset
148
- }
149
- }
150
- })
151
- }
152
- else if (command === ElementAlignCommands.VERTICAL) {
153
- const verticalCenter = (minY + maxY) / 2
154
- elementList.forEach(element => {
155
- if (activeElementIdList.value.includes(element.id)) {
156
- if (!element.groupId) {
157
- const elHeight = element.type === 'line' ? Math.max(element.start[1], element.end[1]) : element.height
158
- element.top = verticalCenter - elHeight / 2
159
- }
160
- else {
161
- const range = groupElementRangeMap[element.groupId]
162
- const center = (range.maxY + range.minY) / 2
163
- const offset = center - verticalCenter
164
- element.top = element.top - offset
165
- }
166
- }
167
- })
168
- }
169
-
170
- slidesStore.updateSlide({ elements: elementList })
171
- addHistorySnapshot()
172
- }
173
-
174
- return {
175
- alignActiveElement,
176
- }
177
  }
 
1
+ import { storeToRefs } from 'pinia'
2
+ import { useMainStore, useSlidesStore } from '@/store'
3
+ import type { PPTElement } from '@/types/slides'
4
+ import { ElementAlignCommands } from '@/types/edit'
5
+ import { getElementListRange, getRectRotatedOffset } from '@/utils/element'
6
+ import useHistorySnapshot from './useHistorySnapshot'
7
+
8
+ interface RangeMap {
9
+ [id: string]: ReturnType<typeof getElementListRange>
10
+ }
11
+
12
+ export default () => {
13
+ const slidesStore = useSlidesStore()
14
+ const { activeElementIdList, activeElementList } = storeToRefs(useMainStore())
15
+ const { currentSlide } = storeToRefs(slidesStore)
16
+
17
+ const { addHistorySnapshot } = useHistorySnapshot()
18
+
19
+ /**
20
+ * 对齐选中的元素
21
+ * @param command 对齐方向
22
+ */
23
+ const alignActiveElement = (command: ElementAlignCommands) => {
24
+ const { minX, maxX, minY, maxY } = getElementListRange(activeElementList.value)
25
+ const elementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
26
+
27
+ // 如果所选择的元素为组合元素的成员,需要计算该组合的整体范围
28
+ const groupElementRangeMap: RangeMap = {}
29
+ for (const activeElement of activeElementList.value) {
30
+ if (activeElement.groupId && !groupElementRangeMap[activeElement.groupId]) {
31
+ const groupElements = activeElementList.value.filter(item => item.groupId === activeElement.groupId)
32
+ groupElementRangeMap[activeElement.groupId] = getElementListRange(groupElements)
33
+ }
34
+ }
35
+
36
+ // 根据不同的命令,计算对齐的位置
37
+ if (command === ElementAlignCommands.LEFT) {
38
+ elementList.forEach(element => {
39
+ if (activeElementIdList.value.includes(element.id)) {
40
+ if (!element.groupId) {
41
+ if ('rotate' in element && element.rotate) {
42
+ const { offsetX } = getRectRotatedOffset({
43
+ left: element.left,
44
+ top: element.top,
45
+ width: element.width,
46
+ height: element.height,
47
+ rotate: element.rotate,
48
+ })
49
+ element.left = minX - offsetX
50
+ }
51
+ else element.left = minX
52
+ }
53
+ else {
54
+ const range = groupElementRangeMap[element.groupId]
55
+ const offset = range.minX - minX
56
+ element.left = element.left - offset
57
+ }
58
+ }
59
+ })
60
+ }
61
+ else if (command === ElementAlignCommands.RIGHT) {
62
+ elementList.forEach(element => {
63
+ if (activeElementIdList.value.includes(element.id)) {
64
+ if (!element.groupId) {
65
+ const elWidth = element.type === 'line' ? Math.max(element.start[0], element.end[0]) : element.width
66
+ if ('rotate' in element && element.rotate) {
67
+ const { offsetX } = getRectRotatedOffset({
68
+ left: element.left,
69
+ top: element.top,
70
+ width: element.width,
71
+ height: element.height,
72
+ rotate: element.rotate,
73
+ })
74
+ element.left = maxX - elWidth + offsetX
75
+ }
76
+ else element.left = maxX - elWidth
77
+ }
78
+ else {
79
+ const range = groupElementRangeMap[element.groupId]
80
+ const offset = range.maxX - maxX
81
+ element.left = element.left - offset
82
+ }
83
+ }
84
+ })
85
+ }
86
+ else if (command === ElementAlignCommands.TOP) {
87
+ elementList.forEach(element => {
88
+ if (activeElementIdList.value.includes(element.id)) {
89
+ if (!element.groupId) {
90
+ if ('rotate' in element && element.rotate) {
91
+ const { offsetY } = getRectRotatedOffset({
92
+ left: element.left,
93
+ top: element.top,
94
+ width: element.width,
95
+ height: element.height,
96
+ rotate: element.rotate,
97
+ })
98
+ element.top = minY - offsetY
99
+ }
100
+ else element.top = minY
101
+ }
102
+ else {
103
+ const range = groupElementRangeMap[element.groupId]
104
+ const offset = range.minY - minY
105
+ element.top = element.top - offset
106
+ }
107
+ }
108
+ })
109
+ }
110
+ else if (command === ElementAlignCommands.BOTTOM) {
111
+ elementList.forEach(element => {
112
+ if (activeElementIdList.value.includes(element.id)) {
113
+ if (!element.groupId) {
114
+ const elHeight = element.type === 'line' ? Math.max(element.start[1], element.end[1]) : element.height
115
+ if ('rotate' in element && element.rotate) {
116
+ const { offsetY } = getRectRotatedOffset({
117
+ left: element.left,
118
+ top: element.top,
119
+ width: element.width,
120
+ height: element.height,
121
+ rotate: element.rotate,
122
+ })
123
+ element.top = maxY - elHeight + offsetY
124
+ }
125
+ else element.top = maxY - elHeight
126
+ }
127
+ else {
128
+ const range = groupElementRangeMap[element.groupId]
129
+ const offset = range.maxY - maxY
130
+ element.top = element.top - offset
131
+ }
132
+ }
133
+ })
134
+ }
135
+ else if (command === ElementAlignCommands.HORIZONTAL) {
136
+ const horizontalCenter = (minX + maxX) / 2
137
+ elementList.forEach(element => {
138
+ if (activeElementIdList.value.includes(element.id)) {
139
+ if (!element.groupId) {
140
+ const elWidth = element.type === 'line' ? Math.max(element.start[0], element.end[0]) : element.width
141
+ element.left = horizontalCenter - elWidth / 2
142
+ }
143
+ else {
144
+ const range = groupElementRangeMap[element.groupId]
145
+ const center = (range.maxX + range.minX) / 2
146
+ const offset = center - horizontalCenter
147
+ element.left = element.left - offset
148
+ }
149
+ }
150
+ })
151
+ }
152
+ else if (command === ElementAlignCommands.VERTICAL) {
153
+ const verticalCenter = (minY + maxY) / 2
154
+ elementList.forEach(element => {
155
+ if (activeElementIdList.value.includes(element.id)) {
156
+ if (!element.groupId) {
157
+ const elHeight = element.type === 'line' ? Math.max(element.start[1], element.end[1]) : element.height
158
+ element.top = verticalCenter - elHeight / 2
159
+ }
160
+ else {
161
+ const range = groupElementRangeMap[element.groupId]
162
+ const center = (range.maxY + range.minY) / 2
163
+ const offset = center - verticalCenter
164
+ element.top = element.top - offset
165
+ }
166
+ }
167
+ })
168
+ }
169
+
170
+ slidesStore.updateSlide({ elements: elementList })
171
+ addHistorySnapshot()
172
+ }
173
+
174
+ return {
175
+ alignActiveElement,
176
+ }
177
  }
frontend/src/hooks/useExport.ts CHANGED
@@ -80,11 +80,439 @@ export default () => {
80
  saveAs(blob, `${title.value}.json`)
81
  }
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  // 格式化颜色值为 透明度 + HexString,供pptxgenjs使用
84
  const formatColor = (_color: string) => {
85
- if (!_color) return {
86
- alpha: 0,
87
- color: '#000000',
 
 
88
  }
89
 
90
  const c = tinycolor(_color)
@@ -910,6 +1338,8 @@ export default () => {
910
  exportImage,
911
  exportJSON,
912
  exportSpecificFile,
 
913
  exportPPTX,
 
914
  }
915
  }
 
80
  saveAs(blob, `${title.value}.json`)
81
  }
82
 
83
+ // 新增:导出为HTML网页
84
+ const exportHTML = (_slides: Slide[], options: any = {}) => {
85
+ exporting.value = true
86
+
87
+ try {
88
+ const {
89
+ includeInteractivity = false,
90
+ standalone = true,
91
+ includeCSS = true
92
+ } = options
93
+
94
+ // 生成HTML内容
95
+ const htmlContent = generateHTMLPresentation(_slides, {
96
+ title: title.value,
97
+ theme: theme.value,
98
+ viewportSize: viewportSize.value,
99
+ viewportRatio: viewportRatio.value,
100
+ includeInteractivity,
101
+ standalone,
102
+ includeCSS
103
+ })
104
+
105
+ // 创建并下载HTML文件
106
+ const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' })
107
+ saveAs(blob, `${title.value}.html`)
108
+
109
+ message.success('HTML网页导出成功')
110
+ }
111
+ catch (error: any) {
112
+ message.error('HTML网页导出失败')
113
+ }
114
+ finally {
115
+ exporting.value = false
116
+ }
117
+ }
118
+
119
+ // 生成完整的HTML演示文稿
120
+ const generateHTMLPresentation = (slides: Slide[], config: any) => {
121
+ const { title, theme, viewportSize, viewportRatio, includeInteractivity, includeCSS } = config
122
+
123
+ const slideHeight = Math.round(viewportSize * viewportRatio)
124
+
125
+ // CSS样式
126
+ const css = includeCSS ? `
127
+ <style>
128
+ * {
129
+ margin: 0;
130
+ padding: 0;
131
+ box-sizing: border-box;
132
+ }
133
+
134
+ body {
135
+ font-family: '${theme.fontName}', 'Microsoft YaHei', Arial, sans-serif;
136
+ background: ${theme.backgroundColor};
137
+ color: ${theme.fontColor};
138
+ overflow: hidden;
139
+ }
140
+
141
+ .presentation-container {
142
+ width: 100vw;
143
+ height: 100vh;
144
+ display: flex;
145
+ justify-content: center;
146
+ align-items: center;
147
+ background: #000;
148
+ }
149
+
150
+ .slide-container {
151
+ width: ${viewportSize}px;
152
+ height: ${slideHeight}px;
153
+ background: #fff;
154
+ position: relative;
155
+ overflow: hidden;
156
+ transform-origin: center;
157
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
158
+ }
159
+
160
+ .slide {
161
+ width: 100%;
162
+ height: 100%;
163
+ position: absolute;
164
+ top: 0;
165
+ left: 0;
166
+ display: none;
167
+ }
168
+
169
+ .slide.active {
170
+ display: block;
171
+ }
172
+
173
+ .element {
174
+ position: absolute;
175
+ word-wrap: break-word;
176
+ }
177
+
178
+ .text-element {
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: center;
182
+ text-align: center;
183
+ }
184
+
185
+ .image-element img {
186
+ width: 100%;
187
+ height: 100%;
188
+ object-fit: contain;
189
+ }
190
+
191
+ .shape-element {
192
+ border-radius: var(--border-radius, 0);
193
+ }
194
+
195
+ .navigation {
196
+ position: fixed;
197
+ bottom: 30px;
198
+ left: 50%;
199
+ transform: translateX(-50%);
200
+ display: flex;
201
+ gap: 10px;
202
+ z-index: 1000;
203
+ }
204
+
205
+ .nav-btn {
206
+ padding: 12px 20px;
207
+ background: rgba(255,255,255,0.9);
208
+ border: none;
209
+ border-radius: 25px;
210
+ cursor: pointer;
211
+ font-size: 14px;
212
+ font-weight: bold;
213
+ transition: all 0.3s ease;
214
+ box-shadow: 0 4px 15px rgba(0,0,0,0.2);
215
+ }
216
+
217
+ .nav-btn:hover {
218
+ background: rgba(255,255,255,1);
219
+ transform: translateY(-2px);
220
+ }
221
+
222
+ .nav-btn:disabled {
223
+ opacity: 0.5;
224
+ cursor: not-allowed;
225
+ }
226
+
227
+ .slide-counter {
228
+ position: fixed;
229
+ top: 30px;
230
+ right: 30px;
231
+ background: rgba(0,0,0,0.7);
232
+ color: white;
233
+ padding: 8px 16px;
234
+ border-radius: 20px;
235
+ font-size: 14px;
236
+ z-index: 1000;
237
+ }
238
+
239
+ .fullscreen-btn {
240
+ position: fixed;
241
+ top: 30px;
242
+ left: 30px;
243
+ background: rgba(0,0,0,0.7);
244
+ color: white;
245
+ border: none;
246
+ padding: 8px 16px;
247
+ border-radius: 20px;
248
+ cursor: pointer;
249
+ font-size: 14px;
250
+ z-index: 1000;
251
+ }
252
+
253
+ /* 响应式缩放 */
254
+ @media (max-width: 1200px) {
255
+ .slide-container {
256
+ transform: scale(0.8);
257
+ }
258
+ }
259
+
260
+ @media (max-width: 800px) {
261
+ .slide-container {
262
+ transform: scale(0.6);
263
+ }
264
+ }
265
+
266
+ @media (max-width: 600px) {
267
+ .slide-container {
268
+ transform: scale(0.4);
269
+ }
270
+ }
271
+ </style>
272
+ ` : ''
273
+
274
+ // JavaScript交互功能
275
+ const javascript = includeInteractivity ? `
276
+ <script>
277
+ class PPTViewer {
278
+ constructor() {
279
+ this.currentSlide = 0;
280
+ this.totalSlides = ${slides.length};
281
+ this.init();
282
+ }
283
+
284
+ init() {
285
+ this.updateSlide();
286
+ this.bindEvents();
287
+ this.autoScale();
288
+ window.addEventListener('resize', () => this.autoScale());
289
+ }
290
+
291
+ bindEvents() {
292
+ document.addEventListener('keydown', (e) => {
293
+ switch(e.key) {
294
+ case 'ArrowLeft':
295
+ case 'ArrowUp':
296
+ this.prevSlide();
297
+ break;
298
+ case 'ArrowRight':
299
+ case 'ArrowDown':
300
+ case ' ':
301
+ this.nextSlide();
302
+ break;
303
+ case 'Home':
304
+ this.goToSlide(0);
305
+ break;
306
+ case 'End':
307
+ this.goToSlide(this.totalSlides - 1);
308
+ break;
309
+ case 'F11':
310
+ this.toggleFullscreen();
311
+ break;
312
+ }
313
+ });
314
+ }
315
+
316
+ nextSlide() {
317
+ if (this.currentSlide < this.totalSlides - 1) {
318
+ this.currentSlide++;
319
+ this.updateSlide();
320
+ }
321
+ }
322
+
323
+ prevSlide() {
324
+ if (this.currentSlide > 0) {
325
+ this.currentSlide--;
326
+ this.updateSlide();
327
+ }
328
+ }
329
+
330
+ goToSlide(index) {
331
+ if (index >= 0 && index < this.totalSlides) {
332
+ this.currentSlide = index;
333
+ this.updateSlide();
334
+ }
335
+ }
336
+
337
+ updateSlide() {
338
+ // 隐藏所有幻灯片
339
+ document.querySelectorAll('.slide').forEach(slide => {
340
+ slide.classList.remove('active');
341
+ });
342
+
343
+ // 显示当前幻灯片
344
+ const currentSlideEl = document.getElementById('slide-' + this.currentSlide);
345
+ if (currentSlideEl) {
346
+ currentSlideEl.classList.add('active');
347
+ }
348
+
349
+ // 更新计数器
350
+ const counter = document.querySelector('.slide-counter');
351
+ if (counter) {
352
+ counter.textContent = \`\${this.currentSlide + 1} / \${this.totalSlides}\`;
353
+ }
354
+
355
+ // 更新导航按钮状态
356
+ const prevBtn = document.getElementById('prevBtn');
357
+ const nextBtn = document.getElementById('nextBtn');
358
+ if (prevBtn) prevBtn.disabled = this.currentSlide === 0;
359
+ if (nextBtn) nextBtn.disabled = this.currentSlide === this.totalSlides - 1;
360
+ }
361
+
362
+ autoScale() {
363
+ const container = document.querySelector('.slide-container');
364
+ if (!container) return;
365
+
366
+ const windowWidth = window.innerWidth;
367
+ const windowHeight = window.innerHeight;
368
+ const slideWidth = ${viewportSize};
369
+ const slideHeight = ${slideHeight};
370
+
371
+ const scaleX = (windowWidth * 0.9) / slideWidth;
372
+ const scaleY = (windowHeight * 0.9) / slideHeight;
373
+ const scale = Math.min(scaleX, scaleY, 1);
374
+
375
+ container.style.transform = \`scale(\${scale})\`;
376
+ }
377
+
378
+ toggleFullscreen() {
379
+ if (!document.fullscreenElement) {
380
+ document.documentElement.requestFullscreen();
381
+ } else {
382
+ document.exitFullscreen();
383
+ }
384
+ }
385
+ }
386
+
387
+ // 初始化PPT查看器
388
+ document.addEventListener('DOMContentLoaded', () => {
389
+ new PPTViewer();
390
+ });
391
+ </script>
392
+ ` : ''
393
+
394
+ // 生成幻灯片HTML
395
+ const slidesHTML = slides.map((slide, index) => {
396
+ const slideBackground = formatSlideBackground(slide.background)
397
+ const elementsHTML = slide.elements.map(element => formatElement(element)).join('')
398
+
399
+ return `
400
+ <div id="slide-${index}" class="slide ${index === 0 ? 'active' : ''}" style="${slideBackground}">
401
+ ${elementsHTML}
402
+ </div>
403
+ `
404
+ }).join('')
405
+
406
+ // 导航控件HTML
407
+ const navigationHTML = includeInteractivity ? `
408
+ <div class="navigation">
409
+ <button id="prevBtn" class="nav-btn" onclick="window.pptViewer?.prevSlide()">上一页</button>
410
+ <button id="nextBtn" class="nav-btn" onclick="window.pptViewer?.nextSlide()">下一页</button>
411
+ </div>
412
+ <div class="slide-counter">1 / ${slides.length}</div>
413
+ <button class="fullscreen-btn" onclick="window.pptViewer?.toggleFullscreen()">全屏</button>
414
+ ` : ''
415
+
416
+ // 组装完整HTML
417
+ const html = `<!DOCTYPE html>
418
+ <html lang="zh-CN">
419
+ <head>
420
+ <meta charset="UTF-8">
421
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
422
+ <title>${title}</title>
423
+ <meta name="description" content="PPTist导出的HTML演示��稿">
424
+ <meta name="generator" content="PPTist">
425
+ ${css}
426
+ </head>
427
+ <body>
428
+ <div class="presentation-container">
429
+ <div class="slide-container">
430
+ ${slidesHTML}
431
+ </div>
432
+ </div>
433
+ ${navigationHTML}
434
+ ${javascript}
435
+ </body>
436
+ </html>`
437
+
438
+ return html
439
+ }
440
+
441
+ // 格式化幻灯片背景
442
+ const formatSlideBackground = (background: any) => {
443
+ if (!background) {
444
+ return 'background: #ffffff;'
445
+ }
446
+
447
+ if (background.type === 'solid') {
448
+ return `background: ${background.color || '#ffffff'};`
449
+ }
450
+
451
+ if (background.type === 'gradient') {
452
+ const { gradientType, colors } = background
453
+ if (gradientType === 'linear') {
454
+ return `background: linear-gradient(${background.gradientRotate || 0}deg, ${colors.map((c: any) => c.color).join(', ')});`
455
+ }
456
+ return `background: radial-gradient(${colors.map((c: any) => c.color).join(', ')});`
457
+ }
458
+
459
+ if (background.type === 'image' && background.image) {
460
+ return `background-image: url(${background.image.src}); background-size: cover; background-position: center;`
461
+ }
462
+
463
+ return 'background: #ffffff;'
464
+ }
465
+
466
+ // 格式化元素
467
+ const formatElement = (element: any) => {
468
+ const baseStyle = `
469
+ left: ${element.left || 0}px;
470
+ top: ${element.top || 0}px;
471
+ width: ${element.width || 100}px;
472
+ height: ${element.height || 100}px;
473
+ transform: rotate(${element.rotate || 0}deg);
474
+ `
475
+
476
+ if (element.type === 'text') {
477
+ const textStyle = `
478
+ font-size: ${element.fontSize || 16}px;
479
+ font-family: ${element.fontName || 'Microsoft YaHei'};
480
+ color: ${element.color || '#000000'};
481
+ font-weight: ${element.bold ? 'bold' : 'normal'};
482
+ font-style: ${element.italic ? 'italic' : 'normal'};
483
+ text-decoration: ${element.underline ? 'underline' : 'none'};
484
+ text-align: ${element.align || 'left'};
485
+ line-height: ${element.lineHeight || 1.5};
486
+ `
487
+
488
+ return `<div class="element text-element" style="${baseStyle}${textStyle}">${element.content || ''}</div>`
489
+ }
490
+
491
+ if (element.type === 'image' && element.src) {
492
+ return `<div class="element image-element" style="${baseStyle}"><img src="${element.src}" alt="图片" /></div>`
493
+ }
494
+
495
+ if (element.type === 'shape') {
496
+ const shapeStyle = `
497
+ background: ${element.fill || '#ffffff'};
498
+ border: ${element.outline?.width || 0}px solid ${element.outline?.color || '#000000'};
499
+ border-radius: ${element.borderRadius || 0}px;
500
+ `
501
+
502
+ return `<div class="element shape-element" style="${baseStyle}${shapeStyle}"></div>`
503
+ }
504
+
505
+ // 其他元素类型的基本处理
506
+ return `<div class="element" style="${baseStyle}"></div>`
507
+ }
508
+
509
  // 格式化颜色值为 透明度 + HexString,供pptxgenjs使用
510
  const formatColor = (_color: string) => {
511
+ if (!_color) {
512
+ return {
513
+ alpha: 0,
514
+ color: '#000000',
515
+ }
516
  }
517
 
518
  const c = tinycolor(_color)
 
1338
  exportImage,
1339
  exportJSON,
1340
  exportSpecificFile,
1341
+ exportHTML,
1342
  exportPPTX,
1343
+ generateHTMLPresentation,
1344
  }
1345
  }
frontend/src/hooks/useMoveElement.ts CHANGED
@@ -1,61 +1,61 @@
1
- import { storeToRefs } from 'pinia'
2
- import { useMainStore, useSlidesStore } from '@/store'
3
- import type { PPTElement } from '@/types/slides'
4
- import { KEYS } from '@/configs/hotkey'
5
- import useHistorySnapshot from '@/hooks/useHistorySnapshot'
6
-
7
- export default () => {
8
- const slidesStore = useSlidesStore()
9
- const { activeElementIdList, activeGroupElementId } = storeToRefs(useMainStore())
10
- const { currentSlide } = storeToRefs(slidesStore)
11
-
12
- const { addHistorySnapshot } = useHistorySnapshot()
13
-
14
- /**
15
- * 将元素向指定方向移动指定的距离
16
- * 组合元素成员中,存在被选中可独立操作的元素时,优先移动该元素。否则默认移动所有被选中的元素
17
- * @param command 移动方向
18
- * @param step 移动距离
19
- */
20
- const moveElement = (command: string, step = 1) => {
21
- let newElementList: PPTElement[] = []
22
-
23
- const move = (el: PPTElement) => {
24
- let { left, top } = el
25
- switch (command) {
26
- case KEYS.LEFT:
27
- left = left - step
28
- break
29
- case KEYS.RIGHT:
30
- left = left + step
31
- break
32
- case KEYS.UP:
33
- top = top - step
34
- break
35
- case KEYS.DOWN:
36
- top = top + step
37
- break
38
- default: break
39
- }
40
- return { ...el, left, top }
41
- }
42
-
43
- if (activeGroupElementId.value) {
44
- newElementList = currentSlide.value.elements.map(el => {
45
- return activeGroupElementId.value === el.id ? move(el) : el
46
- })
47
- }
48
- else {
49
- newElementList = currentSlide.value.elements.map(el => {
50
- return activeElementIdList.value.includes(el.id) ? move(el) : el
51
- })
52
- }
53
-
54
- slidesStore.updateSlide({ elements: newElementList })
55
- addHistorySnapshot()
56
- }
57
-
58
- return {
59
- moveElement,
60
- }
61
  }
 
1
+ import { storeToRefs } from 'pinia'
2
+ import { useMainStore, useSlidesStore } from '@/store'
3
+ import type { PPTElement } from '@/types/slides'
4
+ import { KEYS } from '@/configs/hotkey'
5
+ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
6
+
7
+ export default () => {
8
+ const slidesStore = useSlidesStore()
9
+ const { activeElementIdList, activeGroupElementId } = storeToRefs(useMainStore())
10
+ const { currentSlide } = storeToRefs(slidesStore)
11
+
12
+ const { addHistorySnapshot } = useHistorySnapshot()
13
+
14
+ /**
15
+ * 将元素向指定方向移动指定的距离
16
+ * 组合元素成员中,存在被选中可独立操作的元素时,优先移动该元素。否则默认移动所有被选中的元素
17
+ * @param command 移动方向
18
+ * @param step 移动距离
19
+ */
20
+ const moveElement = (command: string, step = 1) => {
21
+ let newElementList: PPTElement[] = []
22
+
23
+ const move = (el: PPTElement) => {
24
+ let { left, top } = el
25
+ switch (command) {
26
+ case KEYS.LEFT:
27
+ left = left - step
28
+ break
29
+ case KEYS.RIGHT:
30
+ left = left + step
31
+ break
32
+ case KEYS.UP:
33
+ top = top - step
34
+ break
35
+ case KEYS.DOWN:
36
+ top = top + step
37
+ break
38
+ default: break
39
+ }
40
+ return { ...el, left, top }
41
+ }
42
+
43
+ if (activeGroupElementId.value) {
44
+ newElementList = currentSlide.value.elements.map(el => {
45
+ return activeGroupElementId.value === el.id ? move(el) : el
46
+ })
47
+ }
48
+ else {
49
+ newElementList = currentSlide.value.elements.map(el => {
50
+ return activeElementIdList.value.includes(el.id) ? move(el) : el
51
+ })
52
+ }
53
+
54
+ slidesStore.updateSlide({ elements: newElementList })
55
+ addHistorySnapshot()
56
+ }
57
+
58
+ return {
59
+ moveElement,
60
+ }
61
  }
frontend/src/hooks/usePPTManager.ts CHANGED
@@ -1,141 +1,141 @@
1
- import { onMounted, ref } from 'vue'
2
- import { storeToRefs } from 'pinia'
3
- import { useAuthStore, useSlidesStore } from '@/store'
4
- import dataSyncService from '@/services/dataSyncService'
5
- import api from '@/services'
6
- import message from '@/utils/message'
7
-
8
- export default () => {
9
- const authStore = useAuthStore()
10
- const slidesStore = useSlidesStore()
11
- const { title } = storeToRefs(slidesStore)
12
-
13
- const pptList = ref<any[]>([])
14
- const loading = ref(false)
15
- const showPPTManager = ref(false)
16
-
17
- // 获取PPT列表
18
- const refreshPPTList = async () => {
19
- if (!authStore.isLoggedIn) return
20
-
21
- loading.value = true
22
- try {
23
- pptList.value = await dataSyncService.getPPTList()
24
- } catch (error) {
25
- message.error('获取PPT列表失败')
26
- } finally {
27
- loading.value = false
28
- }
29
- }
30
-
31
- // 创建新PPT
32
- const createNewPPT = async (title: string = '新建演示文稿') => {
33
- try {
34
- loading.value = true
35
- const pptId = await dataSyncService.createNewPPT(title)
36
- if (pptId) {
37
- message.success('创建成功')
38
- await refreshPPTList()
39
- return pptId
40
- }
41
- } catch (error) {
42
- message.error('创建失败')
43
- throw error
44
- } finally {
45
- loading.value = false
46
- }
47
- }
48
-
49
- // 加载PPT
50
- const loadPPT = async (pptId: string) => {
51
- try {
52
- loading.value = true
53
- await dataSyncService.loadPPT(pptId)
54
- message.success('PPT加载成功')
55
- } catch (error) {
56
- message.error('PPT加载失败')
57
- throw error
58
- } finally {
59
- loading.value = false
60
- }
61
- }
62
-
63
- // 删除PPT
64
- const deletePPT = async (pptId: string) => {
65
- try {
66
- loading.value = true
67
- await dataSyncService.deletePPT(pptId)
68
- message.success('删除成功')
69
- await refreshPPTList()
70
- } catch (error) {
71
- message.error('删除失败')
72
- throw error
73
- } finally {
74
- loading.value = false
75
- }
76
- }
77
-
78
- // 保存当前PPT
79
- const saveCurrentPPT = async () => {
80
- try {
81
- const success = await dataSyncService.manualSave()
82
- if (success) {
83
- message.success('保存成功')
84
- } else {
85
- message.error('保存失败')
86
- }
87
- return success
88
- } catch (error) {
89
- message.error('保存失败')
90
- return false
91
- }
92
- }
93
-
94
- // 生成分享链接
95
- const generateShareLinks = async (slideIndex = 0) => {
96
- try {
97
- const links = await dataSyncService.generateShareLink(slideIndex)
98
- message.success('分享链接生成成功')
99
- return links
100
- } catch (error) {
101
- message.error('生成分享链接失败')
102
- throw error
103
- }
104
- }
105
-
106
- // 复制PPT
107
- const copyPPT = async (pptId: string, newTitle: string) => {
108
- try {
109
- loading.value = true
110
- const response = await api.copyPPT(pptId, newTitle)
111
- message.success('复制成功')
112
- await refreshPPTList()
113
- return response.pptId
114
- } catch (error) {
115
- message.error('复制失败')
116
- throw error
117
- } finally {
118
- loading.value = false
119
- }
120
- }
121
-
122
- // 组件挂载时初始化
123
- onMounted(() => {
124
- if (authStore.isLoggedIn) {
125
- refreshPPTList()
126
- }
127
- })
128
-
129
- return {
130
- pptList,
131
- loading,
132
- showPPTManager,
133
- refreshPPTList,
134
- createNewPPT,
135
- loadPPT,
136
- deletePPT,
137
- saveCurrentPPT,
138
- generateShareLinks,
139
- copyPPT
140
- }
141
  }
 
1
+ import { onMounted, ref } from 'vue'
2
+ import { storeToRefs } from 'pinia'
3
+ import { useAuthStore, useSlidesStore } from '@/store'
4
+ import dataSyncService from '@/services/dataSyncService'
5
+ import api from '@/services'
6
+ import message from '@/utils/message'
7
+
8
+ export default () => {
9
+ const authStore = useAuthStore()
10
+ const slidesStore = useSlidesStore()
11
+ const { title } = storeToRefs(slidesStore)
12
+
13
+ const pptList = ref<any[]>([])
14
+ const loading = ref(false)
15
+ const showPPTManager = ref(false)
16
+
17
+ // 获取PPT列表
18
+ const refreshPPTList = async () => {
19
+ if (!authStore.isLoggedIn) return
20
+
21
+ loading.value = true
22
+ try {
23
+ pptList.value = await dataSyncService.getPPTList()
24
+ } catch (error) {
25
+ message.error('获取PPT列表失败')
26
+ } finally {
27
+ loading.value = false
28
+ }
29
+ }
30
+
31
+ // 创建新PPT
32
+ const createNewPPT = async (title: string = '新建演示文稿') => {
33
+ try {
34
+ loading.value = true
35
+ const pptId = await dataSyncService.createNewPPT(title)
36
+ if (pptId) {
37
+ message.success('创建成功')
38
+ await refreshPPTList()
39
+ return pptId
40
+ }
41
+ } catch (error) {
42
+ message.error('创建失败')
43
+ throw error
44
+ } finally {
45
+ loading.value = false
46
+ }
47
+ }
48
+
49
+ // 加载PPT
50
+ const loadPPT = async (pptId: string) => {
51
+ try {
52
+ loading.value = true
53
+ await dataSyncService.loadPPT(pptId)
54
+ message.success('PPT加载成功')
55
+ } catch (error) {
56
+ message.error('PPT加载失败')
57
+ throw error
58
+ } finally {
59
+ loading.value = false
60
+ }
61
+ }
62
+
63
+ // 删除PPT
64
+ const deletePPT = async (pptId: string) => {
65
+ try {
66
+ loading.value = true
67
+ await dataSyncService.deletePPT(pptId)
68
+ message.success('删除成功')
69
+ await refreshPPTList()
70
+ } catch (error) {
71
+ message.error('删除失败')
72
+ throw error
73
+ } finally {
74
+ loading.value = false
75
+ }
76
+ }
77
+
78
+ // 保存当前PPT
79
+ const saveCurrentPPT = async () => {
80
+ try {
81
+ const success = await dataSyncService.manualSave()
82
+ if (success) {
83
+ message.success('保存成功')
84
+ } else {
85
+ message.error('保存失败')
86
+ }
87
+ return success
88
+ } catch (error) {
89
+ message.error('保存失败')
90
+ return false
91
+ }
92
+ }
93
+
94
+ // 生成分享链接
95
+ const generateShareLinks = async (slideIndex = 0) => {
96
+ try {
97
+ const links = await dataSyncService.generateShareLink(slideIndex)
98
+ message.success('分享链接生成成功')
99
+ return links
100
+ } catch (error) {
101
+ message.error('生成分享链接失败')
102
+ throw error
103
+ }
104
+ }
105
+
106
+ // 复制PPT
107
+ const copyPPT = async (pptId: string, newTitle: string) => {
108
+ try {
109
+ loading.value = true
110
+ const response = await api.copyPPT(pptId, newTitle)
111
+ message.success('复制成功')
112
+ await refreshPPTList()
113
+ return response.pptId
114
+ } catch (error) {
115
+ message.error('复制失败')
116
+ throw error
117
+ } finally {
118
+ loading.value = false
119
+ }
120
+ }
121
+
122
+ // 组件挂载时初始化
123
+ onMounted(() => {
124
+ if (authStore.isLoggedIn) {
125
+ refreshPPTList()
126
+ }
127
+ })
128
+
129
+ return {
130
+ pptList,
131
+ loading,
132
+ showPPTManager,
133
+ refreshPPTList,
134
+ createNewPPT,
135
+ loadPPT,
136
+ deletePPT,
137
+ saveCurrentPPT,
138
+ generateShareLinks,
139
+ copyPPT
140
+ }
141
  }
frontend/src/hooks/useUniformDisplayElement.ts CHANGED
@@ -1,261 +1,261 @@
1
- import { computed } from 'vue'
2
- import { storeToRefs } from 'pinia'
3
- import { useMainStore, useSlidesStore } from '@/store'
4
- import type { PPTElement } from '@/types/slides'
5
- import { getElementRange, getElementListRange, getRectRotatedOffset } from '@/utils/element'
6
- import useHistorySnapshot from './useHistorySnapshot'
7
-
8
- interface ElementItem {
9
- min: number
10
- max: number
11
- el: PPTElement
12
- }
13
-
14
- interface GroupItem {
15
- groupId: string
16
- els: PPTElement[]
17
- }
18
-
19
- interface GroupElementsItem {
20
- min: number
21
- max: number
22
- els: PPTElement[]
23
- }
24
-
25
- type Item = ElementItem | GroupElementsItem
26
-
27
- interface ElementWithPos {
28
- pos: number
29
- el: PPTElement
30
- }
31
-
32
- interface LastPos {
33
- min: number
34
- max: number
35
- }
36
-
37
- export default () => {
38
- const slidesStore = useSlidesStore()
39
- const { activeElementIdList, activeElementList } = storeToRefs(useMainStore())
40
- const { currentSlide } = storeToRefs(slidesStore)
41
-
42
- const { addHistorySnapshot } = useHistorySnapshot()
43
-
44
- const displayItemCount = computed(() => {
45
- let count = 0
46
- const groupIdList: string[] = []
47
- for (const el of activeElementList.value) {
48
- if (!el.groupId) count += 1
49
- else if (!groupIdList.includes(el.groupId)) {
50
- groupIdList.push(el.groupId)
51
- count += 1
52
- }
53
- }
54
- return count
55
- })
56
- // 水平均匀排列
57
- const uniformHorizontalDisplay = () => {
58
- const { minX, maxX } = getElementListRange(activeElementList.value)
59
- const copyOfActiveElementList: PPTElement[] = JSON.parse(JSON.stringify(activeElementList.value))
60
- const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
61
-
62
- // 分别获取普通元素和组合元素集合,并记录下每一项的范围
63
- const singleElemetList: ElementItem[] = []
64
- let groupList: GroupItem[] = []
65
- for (const el of copyOfActiveElementList) {
66
- if (!el.groupId) {
67
- const { minX, maxX } = getElementRange(el)
68
- singleElemetList.push({ min: minX, max: maxX, el })
69
- }
70
- else {
71
- const groupEl = groupList.find(item => item.groupId === el.groupId)
72
- if (!groupEl) groupList.push({ groupId: el.groupId, els: [el] })
73
- else {
74
- groupList = groupList.map(item => item.groupId === el.groupId ? { ...item, els: [...item.els, el] } : item)
75
- }
76
- }
77
- }
78
- const formatedGroupList: GroupElementsItem[] = []
79
- for (const groupItem of groupList) {
80
- const { minX, maxX } = getElementListRange(groupItem.els)
81
- formatedGroupList.push({ min: minX, max: maxX, els: groupItem.els })
82
- }
83
-
84
- // 将普通元素和组合元素集合组合在一起,然后将每一项按位置(从左到右)排序
85
- const list: Item[] = [...singleElemetList, ...formatedGroupList]
86
- list.sort((itemA, itemB) => itemA.min - itemB.min)
87
-
88
- // 计算元素均匀分布所需要的间隔:
89
- // (所选元素整体范围 - 所有所选元素宽度和) / (所选元素数 - 1)
90
- let totalWidth = 0
91
- for (const item of list) {
92
- const width = item.max - item.min
93
- totalWidth += width
94
- }
95
- const span = ((maxX - minX) - totalWidth) / (list.length - 1)
96
-
97
- // 按位置顺序依次计算每一个元素的目标位置
98
- // 第一项中的元素即为起点,无需计算
99
- // 从第二项开始,每一项的位置应该为:上一项位置 + 上一项宽度 + 间隔
100
- // 注意此处计算的位置(pos)并非元素最终的left值,而是目标位置范围最小值(元素旋转后的left值 ≠ 范围最小值)
101
- const sortedElementData: ElementWithPos[] = []
102
-
103
- const firstItem = list[0]
104
- let lastPos: LastPos = { min: firstItem.min, max: firstItem.max }
105
-
106
- if ('el' in firstItem) {
107
- sortedElementData.push({ pos: firstItem.min, el: firstItem.el })
108
- }
109
- else {
110
- for (const el of firstItem.els) {
111
- const { minX: pos } = getElementRange(el)
112
- sortedElementData.push({ pos, el })
113
- }
114
- }
115
-
116
- for (let i = 1; i < list.length; i++) {
117
- const item = list[i]
118
- const lastWidth = lastPos.max - lastPos.min
119
- const currentPos = lastPos.min + lastWidth + span
120
- const currentWidth = item.max - item.min
121
- lastPos = { min: currentPos, max: currentPos + currentWidth }
122
-
123
- if ('el' in item) {
124
- sortedElementData.push({ pos: currentPos, el: item.el })
125
- }
126
- else {
127
- for (const el of item.els) {
128
- const { minX } = getElementRange(el)
129
- const offset = minX - item.min
130
- sortedElementData.push({ pos: currentPos + offset, el })
131
- }
132
- }
133
- }
134
-
135
- // 根据目标位置计算元素最终目标left值
136
- // 对于旋转后的元素,需要计算旋转前后left的偏移来做校正
137
- for (const element of newElementList) {
138
- if (!activeElementIdList.value.includes(element.id)) continue
139
-
140
- for (const sortedItem of sortedElementData) {
141
- if (sortedItem.el.id === element.id) {
142
- if ('rotate' in element && element.rotate) {
143
- const { offsetX } = getRectRotatedOffset({
144
- left: element.left,
145
- top: element.top,
146
- width: element.width,
147
- height: element.height,
148
- rotate: element.rotate,
149
- })
150
- element.left = sortedItem.pos - offsetX
151
- }
152
- else element.left = sortedItem.pos
153
- }
154
- }
155
- }
156
-
157
- slidesStore.updateSlide({ elements: newElementList })
158
- addHistorySnapshot()
159
- }
160
-
161
- // 垂直均匀排列(逻辑类似水平均匀排列方法)
162
- const uniformVerticalDisplay = () => {
163
- const { minY, maxY } = getElementListRange(activeElementList.value)
164
- const copyOfActiveElementList: PPTElement[] = JSON.parse(JSON.stringify(activeElementList.value))
165
- const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
166
-
167
- const singleElemetList: ElementItem[] = []
168
- let groupList: GroupItem[] = []
169
- for (const el of copyOfActiveElementList) {
170
- if (!el.groupId) {
171
- const { minY, maxY } = getElementRange(el)
172
- singleElemetList.push({ min: minY, max: maxY, el })
173
- }
174
- else {
175
- const groupEl = groupList.find(item => item.groupId === el.groupId)
176
- if (!groupEl) groupList.push({ groupId: el.groupId, els: [el] })
177
- else {
178
- groupList = groupList.map(item => item.groupId === el.groupId ? { ...item, els: [...item.els, el] } : item)
179
- }
180
- }
181
- }
182
- const formatedGroupList: GroupElementsItem[] = []
183
- for (const groupItem of groupList) {
184
- const { minY, maxY } = getElementListRange(groupItem.els)
185
- formatedGroupList.push({ min: minY, max: maxY, els: groupItem.els })
186
- }
187
-
188
- const list: Item[] = [...singleElemetList, ...formatedGroupList]
189
- list.sort((itemA, itemB) => itemA.min - itemB.min)
190
-
191
- let totalHeight = 0
192
- for (const item of list) {
193
- const height = item.max - item.min
194
- totalHeight += height
195
- }
196
- const span = ((maxY - minY) - totalHeight) / (list.length - 1)
197
-
198
- const sortedElementData: ElementWithPos[] = []
199
-
200
- const firstItem = list[0]
201
- let lastPos: LastPos = { min: firstItem.min, max: firstItem.max }
202
-
203
- if ('el' in firstItem) {
204
- sortedElementData.push({ pos: firstItem.min, el: firstItem.el })
205
- }
206
- else {
207
- for (const el of firstItem.els) {
208
- const { minY: pos } = getElementRange(el)
209
- sortedElementData.push({ pos, el })
210
- }
211
- }
212
-
213
- for (let i = 1; i < list.length; i++) {
214
- const item = list[i]
215
- const lastHeight = lastPos.max - lastPos.min
216
- const currentPos = lastPos.min + lastHeight + span
217
- const currentHeight = item.max - item.min
218
- lastPos = { min: currentPos, max: currentPos + currentHeight }
219
-
220
- if ('el' in item) {
221
- sortedElementData.push({ pos: currentPos, el: item.el })
222
- }
223
- else {
224
- for (const el of item.els) {
225
- const { minY } = getElementRange(el)
226
- const offset = minY - item.min
227
- sortedElementData.push({ pos: currentPos + offset, el })
228
- }
229
- }
230
- }
231
-
232
- for (const element of newElementList) {
233
- if (!activeElementIdList.value.includes(element.id)) continue
234
-
235
- for (const sortedItem of sortedElementData) {
236
- if (sortedItem.el.id === element.id) {
237
- if ('rotate' in element && element.rotate) {
238
- const { offsetY } = getRectRotatedOffset({
239
- left: element.left,
240
- top: element.top,
241
- width: element.width,
242
- height: element.height,
243
- rotate: element.rotate,
244
- })
245
- element.top = sortedItem.pos - offsetY
246
- }
247
- else element.top = sortedItem.pos
248
- }
249
- }
250
- }
251
-
252
- slidesStore.updateSlide({ elements: newElementList })
253
- addHistorySnapshot()
254
- }
255
-
256
- return {
257
- displayItemCount,
258
- uniformHorizontalDisplay,
259
- uniformVerticalDisplay,
260
- }
261
  }
 
1
+ import { computed } from 'vue'
2
+ import { storeToRefs } from 'pinia'
3
+ import { useMainStore, useSlidesStore } from '@/store'
4
+ import type { PPTElement } from '@/types/slides'
5
+ import { getElementRange, getElementListRange, getRectRotatedOffset } from '@/utils/element'
6
+ import useHistorySnapshot from './useHistorySnapshot'
7
+
8
+ interface ElementItem {
9
+ min: number
10
+ max: number
11
+ el: PPTElement
12
+ }
13
+
14
+ interface GroupItem {
15
+ groupId: string
16
+ els: PPTElement[]
17
+ }
18
+
19
+ interface GroupElementsItem {
20
+ min: number
21
+ max: number
22
+ els: PPTElement[]
23
+ }
24
+
25
+ type Item = ElementItem | GroupElementsItem
26
+
27
+ interface ElementWithPos {
28
+ pos: number
29
+ el: PPTElement
30
+ }
31
+
32
+ interface LastPos {
33
+ min: number
34
+ max: number
35
+ }
36
+
37
+ export default () => {
38
+ const slidesStore = useSlidesStore()
39
+ const { activeElementIdList, activeElementList } = storeToRefs(useMainStore())
40
+ const { currentSlide } = storeToRefs(slidesStore)
41
+
42
+ const { addHistorySnapshot } = useHistorySnapshot()
43
+
44
+ const displayItemCount = computed(() => {
45
+ let count = 0
46
+ const groupIdList: string[] = []
47
+ for (const el of activeElementList.value) {
48
+ if (!el.groupId) count += 1
49
+ else if (!groupIdList.includes(el.groupId)) {
50
+ groupIdList.push(el.groupId)
51
+ count += 1
52
+ }
53
+ }
54
+ return count
55
+ })
56
+ // 水平均匀排列
57
+ const uniformHorizontalDisplay = () => {
58
+ const { minX, maxX } = getElementListRange(activeElementList.value)
59
+ const copyOfActiveElementList: PPTElement[] = JSON.parse(JSON.stringify(activeElementList.value))
60
+ const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
61
+
62
+ // 分别获取普通元素和组合元素集合,并记录下每一项的范围
63
+ const singleElemetList: ElementItem[] = []
64
+ let groupList: GroupItem[] = []
65
+ for (const el of copyOfActiveElementList) {
66
+ if (!el.groupId) {
67
+ const { minX, maxX } = getElementRange(el)
68
+ singleElemetList.push({ min: minX, max: maxX, el })
69
+ }
70
+ else {
71
+ const groupEl = groupList.find(item => item.groupId === el.groupId)
72
+ if (!groupEl) groupList.push({ groupId: el.groupId, els: [el] })
73
+ else {
74
+ groupList = groupList.map(item => item.groupId === el.groupId ? { ...item, els: [...item.els, el] } : item)
75
+ }
76
+ }
77
+ }
78
+ const formatedGroupList: GroupElementsItem[] = []
79
+ for (const groupItem of groupList) {
80
+ const { minX, maxX } = getElementListRange(groupItem.els)
81
+ formatedGroupList.push({ min: minX, max: maxX, els: groupItem.els })
82
+ }
83
+
84
+ // 将普通元素和组合元素集合组合在一起,然后将每一项按位置(从左到右)排序
85
+ const list: Item[] = [...singleElemetList, ...formatedGroupList]
86
+ list.sort((itemA, itemB) => itemA.min - itemB.min)
87
+
88
+ // 计算元素均匀分布所需要的间隔:
89
+ // (所选元素整体范围 - 所有所选元素宽度和) / (所选元素数 - 1)
90
+ let totalWidth = 0
91
+ for (const item of list) {
92
+ const width = item.max - item.min
93
+ totalWidth += width
94
+ }
95
+ const span = ((maxX - minX) - totalWidth) / (list.length - 1)
96
+
97
+ // 按位置顺序依次计算每一个元素的目标位置
98
+ // 第一项中的元素即为起点,无需计算
99
+ // 从第二项开始,每一项的位置应该为:上一项位置 + 上一项宽度 + 间隔
100
+ // 注意此处计算的位置(pos)并非元素最终的left值,而是目标位置范围最小值(元素旋转后的left值 ≠ 范围最小值)
101
+ const sortedElementData: ElementWithPos[] = []
102
+
103
+ const firstItem = list[0]
104
+ let lastPos: LastPos = { min: firstItem.min, max: firstItem.max }
105
+
106
+ if ('el' in firstItem) {
107
+ sortedElementData.push({ pos: firstItem.min, el: firstItem.el })
108
+ }
109
+ else {
110
+ for (const el of firstItem.els) {
111
+ const { minX: pos } = getElementRange(el)
112
+ sortedElementData.push({ pos, el })
113
+ }
114
+ }
115
+
116
+ for (let i = 1; i < list.length; i++) {
117
+ const item = list[i]
118
+ const lastWidth = lastPos.max - lastPos.min
119
+ const currentPos = lastPos.min + lastWidth + span
120
+ const currentWidth = item.max - item.min
121
+ lastPos = { min: currentPos, max: currentPos + currentWidth }
122
+
123
+ if ('el' in item) {
124
+ sortedElementData.push({ pos: currentPos, el: item.el })
125
+ }
126
+ else {
127
+ for (const el of item.els) {
128
+ const { minX } = getElementRange(el)
129
+ const offset = minX - item.min
130
+ sortedElementData.push({ pos: currentPos + offset, el })
131
+ }
132
+ }
133
+ }
134
+
135
+ // 根据目标位置计算元素最终目标left值
136
+ // 对于旋转后的元素,需要计算旋转前后left的偏移来做校正
137
+ for (const element of newElementList) {
138
+ if (!activeElementIdList.value.includes(element.id)) continue
139
+
140
+ for (const sortedItem of sortedElementData) {
141
+ if (sortedItem.el.id === element.id) {
142
+ if ('rotate' in element && element.rotate) {
143
+ const { offsetX } = getRectRotatedOffset({
144
+ left: element.left,
145
+ top: element.top,
146
+ width: element.width,
147
+ height: element.height,
148
+ rotate: element.rotate,
149
+ })
150
+ element.left = sortedItem.pos - offsetX
151
+ }
152
+ else element.left = sortedItem.pos
153
+ }
154
+ }
155
+ }
156
+
157
+ slidesStore.updateSlide({ elements: newElementList })
158
+ addHistorySnapshot()
159
+ }
160
+
161
+ // 垂直均匀排列(逻辑类似水平均匀排列方法)
162
+ const uniformVerticalDisplay = () => {
163
+ const { minY, maxY } = getElementListRange(activeElementList.value)
164
+ const copyOfActiveElementList: PPTElement[] = JSON.parse(JSON.stringify(activeElementList.value))
165
+ const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
166
+
167
+ const singleElemetList: ElementItem[] = []
168
+ let groupList: GroupItem[] = []
169
+ for (const el of copyOfActiveElementList) {
170
+ if (!el.groupId) {
171
+ const { minY, maxY } = getElementRange(el)
172
+ singleElemetList.push({ min: minY, max: maxY, el })
173
+ }
174
+ else {
175
+ const groupEl = groupList.find(item => item.groupId === el.groupId)
176
+ if (!groupEl) groupList.push({ groupId: el.groupId, els: [el] })
177
+ else {
178
+ groupList = groupList.map(item => item.groupId === el.groupId ? { ...item, els: [...item.els, el] } : item)
179
+ }
180
+ }
181
+ }
182
+ const formatedGroupList: GroupElementsItem[] = []
183
+ for (const groupItem of groupList) {
184
+ const { minY, maxY } = getElementListRange(groupItem.els)
185
+ formatedGroupList.push({ min: minY, max: maxY, els: groupItem.els })
186
+ }
187
+
188
+ const list: Item[] = [...singleElemetList, ...formatedGroupList]
189
+ list.sort((itemA, itemB) => itemA.min - itemB.min)
190
+
191
+ let totalHeight = 0
192
+ for (const item of list) {
193
+ const height = item.max - item.min
194
+ totalHeight += height
195
+ }
196
+ const span = ((maxY - minY) - totalHeight) / (list.length - 1)
197
+
198
+ const sortedElementData: ElementWithPos[] = []
199
+
200
+ const firstItem = list[0]
201
+ let lastPos: LastPos = { min: firstItem.min, max: firstItem.max }
202
+
203
+ if ('el' in firstItem) {
204
+ sortedElementData.push({ pos: firstItem.min, el: firstItem.el })
205
+ }
206
+ else {
207
+ for (const el of firstItem.els) {
208
+ const { minY: pos } = getElementRange(el)
209
+ sortedElementData.push({ pos, el })
210
+ }
211
+ }
212
+
213
+ for (let i = 1; i < list.length; i++) {
214
+ const item = list[i]
215
+ const lastHeight = lastPos.max - lastPos.min
216
+ const currentPos = lastPos.min + lastHeight + span
217
+ const currentHeight = item.max - item.min
218
+ lastPos = { min: currentPos, max: currentPos + currentHeight }
219
+
220
+ if ('el' in item) {
221
+ sortedElementData.push({ pos: currentPos, el: item.el })
222
+ }
223
+ else {
224
+ for (const el of item.els) {
225
+ const { minY } = getElementRange(el)
226
+ const offset = minY - item.min
227
+ sortedElementData.push({ pos: currentPos + offset, el })
228
+ }
229
+ }
230
+ }
231
+
232
+ for (const element of newElementList) {
233
+ if (!activeElementIdList.value.includes(element.id)) continue
234
+
235
+ for (const sortedItem of sortedElementData) {
236
+ if (sortedItem.el.id === element.id) {
237
+ if ('rotate' in element && element.rotate) {
238
+ const { offsetY } = getRectRotatedOffset({
239
+ left: element.left,
240
+ top: element.top,
241
+ width: element.width,
242
+ height: element.height,
243
+ rotate: element.rotate,
244
+ })
245
+ element.top = sortedItem.pos - offsetY
246
+ }
247
+ else element.top = sortedItem.pos
248
+ }
249
+ }
250
+ }
251
+
252
+ slidesStore.updateSlide({ elements: newElementList })
253
+ addHistorySnapshot()
254
+ }
255
+
256
+ return {
257
+ displayItemCount,
258
+ uniformHorizontalDisplay,
259
+ uniformVerticalDisplay,
260
+ }
261
  }