@dolphinweex/weex-harmony 0.1.48 → 0.1.50

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.
@@ -27,64 +27,56 @@
27
27
  @touchcancel="endTouch"
28
28
  >
29
29
  <div
30
- v-if="
31
- item.bgAnimView && item.bgAnimView.animUrl && !data.isEditing
32
- "
30
+ v-if="hasAnimation(item, 'bgAnimView')"
33
31
  class="lottie-container bg-anim"
34
- :style="applyNormalAttribute(item.bgAnimView)"
32
+ :style="applyNormalAttribute(item.bgAnimView, 'bgAnimView')"
35
33
  :ref="`bgAnim-${item.itemView.id}`"
36
34
  ></div>
37
35
  <div
38
- v-if="
39
- item.frontAnimView &&
40
- item.frontAnimView.animUrl &&
41
- !data.isEditing
42
- "
36
+ v-if="hasAnimation(item, 'frontAnimView')"
43
37
  class="lottie-container front-anim"
44
- :style="applyNormalAttribute(item.frontAnimView)"
38
+ :style="applyNormalAttribute(item.frontAnimView, 'frontAnimView')"
45
39
  :ref="`frontAnim-${item.itemView.id}`"
46
40
  ></div>
47
41
  <div
48
- :style="applyNormalAttribute(item.itemView)"
42
+ :style="applyNormalAttribute(item.itemView, 'itemView')"
49
43
  class="grid-box"
50
44
  :class="{ shaking: data.isEditing }"
51
45
  @click.stop.prevent="(e) => itemViewClick(e, item, index)"
52
46
  >
53
47
  <div class="flexBox">
54
48
  <img
55
- :src="item.iconImage.url"
56
- :style="applyNormalAttribute(item.iconImage)"
49
+ :src="getUrl(item, 'iconImage')"
50
+ :style="applyNormalAttribute(item.iconImage, 'iconImage')"
57
51
  />
58
52
  <div>
59
53
  <div @click.stop.prevent v-if="!data.isEditing">
60
54
  <button
61
55
  class="actionButton"
62
56
  @click.stop.prevent="(e) => actionClick(e, item, index)"
63
- :style="applyNormalAttribute(item.actionButton)"
57
+ :style="applyNormalAttribute(item.actionButton, 'actionButton')"
64
58
  >
65
- {{ item.actionButton.text }}
59
+ {{ getText(item, 'actionButton') }}
66
60
  </button>
67
61
  <div
68
- v-if="
69
- item.loadingAnimView && item.loadingAnimView.animUrl
70
- "
62
+ v-if="getUrl(item, 'loadingAnimView', 'animUrl')"
71
63
  class="loading-anim"
72
- :style="applyNormalAttribute(item.loadingAnimView)"
64
+ :style="applyNormalAttribute(item.loadingAnimView, 'loadingAnimView')"
73
65
  :ref="`loadingAnim-${item.itemView.id}`"
74
66
  ></div>
75
67
  </div>
76
68
  <div @click.stop.prevent v-if="!data.isEditing">
77
69
  <img
78
- :src="item.rightIcon.url"
79
- :style="applyNormalAttribute(item.rightIcon)"
70
+ :src="getUrl(item, 'rightIcon')"
71
+ :style="applyNormalAttribute(item.rightIcon, 'rightIcon')"
80
72
  @click.stop.prevent="(e) => rightIconClick(e)"
81
73
  />
82
74
  </div>
83
75
  <div @click.stop.prevent v-if="!data.isEditing">
84
76
  <img
85
77
  class="leftTopIcon"
86
- :src="item.leftTopIcon.url"
87
- :style="applyNormalAttribute(item.leftTopIcon)"
78
+ :src="getUrl(item, 'leftTopIcon')"
79
+ :style="applyNormalAttribute(item.leftTopIcon, 'leftTopIcon')"
88
80
  @click.stop.prevent
89
81
  />
90
82
  </div>
@@ -96,16 +88,16 @@
96
88
  "
97
89
  :src="
98
90
  item.itemView.isSelected
99
- ? item.selectIcon.selectedUrl
100
- : item.selectIcon.url
91
+ ? getUrl(item, 'selectIcon', 'selectedUrl')
92
+ : getUrl(item, 'selectIcon')
101
93
  "
102
- :style="applyNormalAttribute(item.selectIcon)"
94
+ :style="applyNormalAttribute(item.selectIcon, 'selectIcon')"
103
95
  />
104
96
  </div>
105
97
  </div>
106
98
  </div>
107
- <div class="itemText" :style="[applyNormalAttribute(item.text)]">
108
- {{ item.text.text }}
99
+ <div class="itemText" :style="[applyNormalAttribute(item.text, 'text')]">
100
+ {{ getText(item, 'text') }}
109
101
  </div>
110
102
  </div>
111
103
  </div>
@@ -120,6 +112,27 @@
120
112
  <script>
121
113
  import lottie from 'lottie-web';
122
114
  const scla = 2;
115
+
116
+ // 常量配置
117
+ const REQUIRED_TYPES = [
118
+ 'itemView',
119
+ 'text',
120
+ 'actionButton',
121
+ 'rightIcon',
122
+ 'leftTopIcon',
123
+ 'iconImage',
124
+ 'selectIcon',
125
+ 'loadingAnimView',
126
+ 'bgAnimView',
127
+ 'frontAnimView',
128
+ ];
129
+
130
+ const ANIMATION_TYPES = [
131
+ { type: 'bg', view: 'bgAnimView' },
132
+ { type: 'loading', view: 'loadingAnimView' },
133
+ { type: 'front', view: 'frontAnimView' }
134
+ ];
135
+
123
136
  export default {
124
137
  name: 'EditableGrid',
125
138
  props: {
@@ -147,6 +160,10 @@ export default {
147
160
  isTouchDragging: false,
148
161
  lastSwapTime: 0,
149
162
  isAnimating: false,
163
+ lastSwapPair: null, // 记录上次交换的元素对
164
+ stablePositionTime: 0, // 稳定位置时间
165
+ lastTouchX: 0, // 上次触摸X坐标
166
+ lastTouchY: 0, // 上次触摸Y坐标
150
167
  touchStartX: 0,
151
168
  touchStartY: 0,
152
169
  draggedItemRect: null,
@@ -163,6 +180,15 @@ export default {
163
180
  fixedItemHeight: 0,
164
181
  animationFrameId: null,
165
182
  dragCloneElement: null, // 拖拽克隆DOM元素
183
+ // 性能优化缓存
184
+ gridItemsCache: [],
185
+ lastCacheTime: 0,
186
+ cacheValidDuration: 1000, // 缓存有效期1秒
187
+ listChangeDebounced: null,
188
+ updatePositionThrottled: null,
189
+ // 避免频繁DOM查询
190
+ containerElement: null,
191
+ isUpdatingPosition: false,
166
192
  };
167
193
  },
168
194
  computed: {
@@ -181,6 +207,263 @@ export default {
181
207
  },
182
208
  },
183
209
  methods: {
210
+ // 添加工具方法
211
+ isNil(value) {
212
+ return value === null || value === undefined;
213
+ },
214
+
215
+ // 获取属性值,支持回退到全局数据和默认值
216
+ getValue(itemData, globalData, property, defaultValue = null) {
217
+ // 安全地获取属性值,避免访问undefined对象的属性
218
+ let itemValue;
219
+ let globalValue;
220
+
221
+ try {
222
+ itemValue = itemData && typeof itemData === 'object' ? itemData[property] : undefined;
223
+ } catch (e) {
224
+ itemValue = undefined;
225
+ }
226
+
227
+ try {
228
+ globalValue = globalData && typeof globalData === 'object' ? globalData[property] : undefined;
229
+ } catch (e) {
230
+ globalValue = undefined;
231
+ }
232
+
233
+ if (!this.isNil(itemValue)) {
234
+ return itemValue;
235
+ }
236
+ if (!this.isNil(globalValue)) {
237
+ return globalValue;
238
+ }
239
+ return defaultValue;
240
+ },
241
+
242
+ // 简化的URL获取方法
243
+ getUrl(item, type, property = 'url') {
244
+ return this.getValue(item[type], this.data.globalData[type], property, '');
245
+ },
246
+
247
+ // 简化的文本获取方法
248
+ getText(item, type, property = 'text') {
249
+ return this.getValue(item[type], this.data.globalData[type], property, '');
250
+ },
251
+
252
+ // 简化的动画URL检查方法
253
+ hasAnimation(item, type) {
254
+ return this.getValue(item[type], this.data.globalData[type], 'animUrl', '') && !this.data.isEditing;
255
+ },
256
+
257
+ // 性能优化工具方法
258
+ debounce(func, wait) {
259
+ let timeout;
260
+ return function executedFunction(...args) {
261
+ const later = () => {
262
+ clearTimeout(timeout);
263
+ func(...args);
264
+ };
265
+ clearTimeout(timeout);
266
+ timeout = setTimeout(later, wait);
267
+ };
268
+ },
269
+
270
+ throttle(func, limit) {
271
+ let inThrottle;
272
+ return function executedFunction(...args) {
273
+ if (!inThrottle) {
274
+ func.apply(this, args);
275
+ inThrottle = true;
276
+ setTimeout(() => inThrottle = false, limit);
277
+ }
278
+ };
279
+ },
280
+
281
+ // 缓存DOM查询结果
282
+ getCachedGridItems() {
283
+ const now = Date.now();
284
+ if (now - this.lastCacheTime > this.cacheValidDuration || this.gridItemsCache.length === 0) {
285
+ this.gridItemsCache = Array.from(document.querySelectorAll('.grid-item'));
286
+ this.lastCacheTime = now;
287
+ }
288
+ return this.gridItemsCache;
289
+ },
290
+
291
+ // 清除DOM缓存
292
+ clearGridItemsCache() {
293
+ this.gridItemsCache = [];
294
+ this.lastCacheTime = 0;
295
+ },
296
+
297
+ // 优化的位置更新方法
298
+ optimizedUpdateDragPosition() {
299
+ if (this.isUpdatingPosition) return;
300
+
301
+ this.isUpdatingPosition = true;
302
+
303
+ const easing = 0.6;
304
+ const targetX = this.targetTouchX - this.touchOffsetX + window.pageXOffset;
305
+ const targetY = this.targetTouchY - this.touchOffsetY + window.pageYOffset;
306
+
307
+ this.dragCloneX += (targetX - this.dragCloneX) * easing;
308
+ this.dragCloneY += (targetY - this.dragCloneY) * easing;
309
+
310
+ if (Math.abs(this.dragCloneX - targetX) < 0.5) {
311
+ this.dragCloneX = targetX;
312
+ }
313
+ if (Math.abs(this.dragCloneY - targetY) < 0.5) {
314
+ this.dragCloneY = targetY;
315
+ }
316
+
317
+ this.updateDragClonePosition();
318
+
319
+ this.isUpdatingPosition = false;
320
+
321
+ if (this.isTouchDragging) {
322
+ this.animationFrameId = requestAnimationFrame(() => this.optimizedUpdateDragPosition());
323
+ }
324
+ },
325
+
326
+ // 批量更新样式,减少重绘
327
+ batchUpdateStyles(elements, styles) {
328
+ if (!elements || elements.length === 0) return;
329
+
330
+ // 使用documentFragment来减少DOM操作
331
+ elements.forEach(element => {
332
+ Object.keys(styles).forEach(property => {
333
+ element.style[property] = styles[property];
334
+ });
335
+ });
336
+ },
337
+
338
+ // 确保项目数据结构完整性
339
+ ensureItemStructure(item) {
340
+ REQUIRED_TYPES.forEach((type) => {
341
+ if (!item[type] || typeof item[type] !== 'object') {
342
+ item[type] = {};
343
+ }
344
+ });
345
+
346
+ return item;
347
+ },
348
+
349
+ // 抽取的动画初始化公共方法
350
+ initItemAnimations(item, itemData = null) {
351
+ const itemId = String(item.itemView.id);
352
+ const dataSource = itemData || item; // 支持传入不同的数据源
353
+
354
+ ANIMATION_TYPES.forEach(({ type, view }) => {
355
+ const animUrl = this.getValue(
356
+ dataSource[view],
357
+ this.data.globalData[view],
358
+ 'animUrl',
359
+ ''
360
+ );
361
+ if (animUrl) {
362
+ this.createLottieAnimation(type, itemId, dataSource[view], dataSource);
363
+ }
364
+ });
365
+ },
366
+
367
+ // 统一的样式应用方法,支持缩放控制
368
+ applyOptimizedAttribute(itemData = {}, type = '', useScale = true) {
369
+ const globalData = this.data.globalData[type] || {};
370
+ const scale = useScale ? scla : 1; // 根据参数决定是否应用缩放
371
+
372
+ const style = {
373
+ color: this.getValue(itemData, globalData, 'textColor', '#000000'),
374
+ fontWeight: this.getValue(itemData, globalData, 'fontWeight', 'normal'),
375
+ fontFamily: this.getValue(itemData, globalData, 'fontFamilyPath', 'inherit'),
376
+ fontSize: `${(this.getValue(itemData, globalData, 'textSize', 16)) * scale}px`,
377
+ // 布局相关样式
378
+ backgroundColor: this.getValue(itemData, globalData, 'backgroundColor', 'transparent'),
379
+ borderRadius: `${(this.getValue(itemData, globalData, 'cornerRadius', 0)) * scale}px`,
380
+ opacity: this.getValue(itemData, globalData, 'alpha', 1),
381
+ };
382
+
383
+ // 处理width
384
+ const width = this.getValue(itemData, globalData, 'width');
385
+ if (typeof width === 'number' && width > 0) {
386
+ style.width = `${width * scale}px`;
387
+ } else if (width === -1) {
388
+ style.width = '100%';
389
+ } else if (width && typeof width === 'string') {
390
+ style.width = width;
391
+ }
392
+
393
+ // 处理height
394
+ const height = this.getValue(itemData, globalData, 'height');
395
+ if (typeof height === 'number' && height > 0) {
396
+ style.height = `${height * scale}px`;
397
+ } else if (height && typeof height === 'string') {
398
+ style.height = height;
399
+ }
400
+
401
+ // 处理visibility和display
402
+ const visibility = this.getValue(itemData, globalData, 'visibility');
403
+ if (typeof visibility === 'number') {
404
+ if (visibility === 0) {
405
+ style.display = 'none';
406
+ } else {
407
+ style.display = this.getValue(itemData, globalData, 'display', '');
408
+ style.visibility = visibility === 1 ? 'visible' : 'hidden';
409
+ }
410
+ } else {
411
+ style.display = this.getValue(itemData, globalData, 'display', '');
412
+ }
413
+
414
+ // 处理边框
415
+ const borderColor = this.data.isEditing
416
+ ? this.getValue(itemData, globalData, 'selectedBorderColor', '')
417
+ : this.getValue(itemData, globalData, 'normalBorderColor', '');
418
+ const borderWidth = this.data.isEditing
419
+ ? this.getValue(itemData, globalData, 'selectedBorderWidth', 0)
420
+ : this.getValue(itemData, globalData, 'normalBorderWidth', 0);
421
+
422
+ style.borderColor = borderColor;
423
+ style.borderWidth = `${borderWidth * scale}px`;
424
+
425
+ // 处理padding
426
+ const padding = this.getValue(itemData, globalData, 'padding');
427
+ if (padding && Array.isArray(padding)) {
428
+ style.paddingTop = `${(padding[1] || 0) * scale}px`;
429
+ style.paddingLeft = `${(padding[0] || 0) * scale}px`;
430
+ style.paddingBottom = `${(padding[3] || 0) * scale}px`;
431
+ style.paddingRight = `${(padding[2] || 0) * scale}px`;
432
+ }
433
+
434
+ // 处理margin
435
+ const margin = this.getValue(itemData, globalData, 'margin');
436
+ if (margin && Array.isArray(margin)) {
437
+ style.marginTop = `${(margin[1] || 0) * scale}px`;
438
+ style.marginLeft = `${(margin[0] || 0) * scale}px`;
439
+ style.marginBottom = `${(margin[3] || 0) * scale}px`;
440
+ style.marginRight = `${(margin[2] || 0) * scale}px`;
441
+ }
442
+
443
+ return style;
444
+ },
445
+
446
+
447
+
448
+
449
+
450
+ // 优化内存使用
451
+ optimizeMemoryUsage() {
452
+ // 清理无用的动画实例
453
+ Object.keys(this.lottieAnimations).forEach(key => {
454
+ const anim = this.lottieAnimations[key];
455
+ if (anim && anim.isLoaded === false) {
456
+ anim.destroy();
457
+ delete this.lottieAnimations[key];
458
+ }
459
+ });
460
+
461
+ // 清理DOM缓存
462
+ if (this.gridItemsCache.length > 50) { // 如果缓存过多,清理
463
+ this.clearGridItemsCache();
464
+ }
465
+ },
466
+
184
467
  // 获取元素在页面中的绝对位置(考虑所有transform影响)
185
468
  getAbsolutePosition(element) {
186
469
  let x = 0;
@@ -217,7 +500,7 @@ export default {
217
500
  this.dragCloneElement.className = 'drag-clone-dynamic';
218
501
 
219
502
  // 设置基本样式(使用不带缩放的样式方法)
220
- const itemViewStyle = this.applyDragCloneAttribute(item.itemView);
503
+ const itemViewStyle = this.applyOptimizedAttribute(item.itemView, 'itemView', false);
221
504
  Object.assign(this.dragCloneElement.style, {
222
505
  position: 'absolute',
223
506
  zIndex: '9999',
@@ -237,11 +520,11 @@ export default {
237
520
  justifyContent: 'space-between',
238
521
  });
239
522
 
240
- // 创建内容HTML(使用不带缩放的样式方法)
241
- const iconImageStyle = this.applyDragCloneAttribute(item.iconImage);
242
- const selectIconStyle = this.applyDragCloneAttribute(item.selectIcon);
243
- const textStyle = this.applyDragCloneAttribute(item.text);
244
- const actionButtonStyle = this.applyDragCloneAttribute(item.actionButton);
523
+ // 创建内容HTML(不使用缩放)
524
+ const iconImageStyle = this.applyOptimizedAttribute(item.iconImage, 'iconImage', false);
525
+ const selectIconStyle = this.applyOptimizedAttribute(item.selectIcon, 'selectIcon', false);
526
+ const textStyle = this.applyOptimizedAttribute(item.text, 'text', false);
527
+ const actionButtonStyle = this.applyOptimizedAttribute(item.actionButton, 'actionButton', false);
245
528
 
246
529
  // 将样式对象转换为CSS字符串
247
530
  const stylesToString = (styleObj) => {
@@ -252,7 +535,7 @@ export default {
252
535
 
253
536
  // 确保内部grid-box也使用固定尺寸(使用不带缩放的样式方法)
254
537
  const gridBoxStyle = {
255
- ...this.applyDragCloneAttribute(item.itemView),
538
+ ...this.applyOptimizedAttribute(item.itemView, 'itemView', false),
256
539
  width: `${this.fixedItemWidth }px`,
257
540
  height: `${this.fixedItemHeight }px`
258
541
  };
@@ -260,17 +543,19 @@ export default {
260
543
  this.dragCloneElement.innerHTML = `
261
544
  <div class="grid-box" style="${stylesToString(gridBoxStyle)} ;display: flex; justify-content: space-between;flex-direction: column">
262
545
  <div class="flexBox" style="display: flex; justify-content: space-between; width: 100%; position: relative; flex-direction: row;">
263
- <img src="${item.iconImage.url}" style="${stylesToString(iconImageStyle)}" />
546
+ <img src="${this.getUrl(item, 'iconImage')}" style="${stylesToString(iconImageStyle)}" />
264
547
  <div style="pointer-events: none;">
265
548
  <img
266
549
  class="isSelected"
267
- src="${item.itemView.isSelected ? item.selectIcon.selectedUrl : item.selectIcon.url}"
550
+ src="${item.itemView.isSelected
551
+ ? this.getUrl(item, 'selectIcon', 'selectedUrl')
552
+ : this.getUrl(item, 'selectIcon')}"
268
553
  style="${stylesToString(selectIconStyle)}"
269
554
  />
270
555
  </div>
271
556
  </div>
272
557
  <div class="itemText" style="${stylesToString(textStyle)}; display: block; width: 80%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
273
- ${item.text.text}
558
+ ${this.getText(item, 'text')}
274
559
  </div>
275
560
  </div>
276
561
  `;
@@ -298,64 +583,11 @@ export default {
298
583
  this.dragCloneElement.style.transform = `translate(${this.dragCloneX}px, ${this.dragCloneY}px)`;
299
584
  },
300
585
 
301
- // 为拖拽克隆元素应用样式(不应用缩放因子)
302
- applyDragCloneAttribute(data = {}) {
303
- let style = {
304
- color: data.textColor || '#000000',
305
- fontWeight: data.fontWeight || 'normal',
306
- fontFamily: data.fontFamilyPath || 'inherit',
307
- fontSize: `${data.textSize || 16}px`, // 不乘以scla
308
- // 布局相关样式
309
- width:
310
- typeof data.width === 'number' && data.width > 0
311
- ? `${data.width}px` // 不乘以scla
312
- : data.width === -1
313
- ? '%'
314
- : data.width || '',
315
- height:
316
- typeof data.height === 'number'
317
- ? `${data.height}px` // 不乘以scla
318
- : data.height || '',
319
- backgroundColor: data.backgroundColor || 'transparent',
320
- borderRadius: `${data.cornerRadius || 0}px`, // 不乘以scla
321
- opacity: data.alpha || 1,
322
- };
323
-
324
- if (typeof data.visibility === 'number') {
325
- if (data.visibility === 0) {
326
- style.display = 'none';
327
- } else {
328
- style.display = data.display || '';
329
- style.visibility = data.visibility === 1 ? 'visible' : 'hidden';
330
- }
331
- } else {
332
- style.display = data.display || '';
333
- }
334
-
335
- style.borderColor = this.data.isEditing
336
- ? data.selectedBorderColor || ''
337
- : data.normalBorderColor || '';
338
- style.borderWidth = this.data.isEditing
339
- ? `${data.selectedBorderWidth || 0}px` // 不乘以scla
340
- : `${data.normalBorderWidth || 0}px`; // 不乘以scla
341
-
342
- if (data.padding) {
343
- style.paddingTop = `${data.padding[1] || 0}px`; // 不乘以scla
344
- style.paddingLeft = `${data.padding[0] || 0}px`; // 不乘以scla
345
- style.paddingBottom = `${data.padding[3] || 0}px`; // 不乘以scla
346
- style.paddingRight = `${data.padding[2] || 0}px`; // 不乘以scla
347
- }
348
- if (data.margin) {
349
- style.marginTop = `${data.margin[1] || 0}px`; // 不乘以scla
350
- style.marginLeft = `${data.margin[0] || 0}px`; // 不乘以scla
351
- style.marginBottom = `${data.margin[3] || 0}px`; // 不乘以scla
352
- style.marginRight = `${data.margin[2] || 0}px`; // 不乘以scla
353
- }
354
-
355
- return style;
586
+ // 普通渲染的样式应用方法(使用缩放)
587
+ applyNormalAttribute(itemData = {}, type = '') {
588
+ return this.applyOptimizedAttribute(itemData, type, true);
356
589
  },
357
590
  updateListItem(data) {
358
- console.log('updateListItem收到数据:', JSON.stringify(data));
359
591
  const id = data.itemView.id;
360
592
  const index = this.data.list.findIndex((item) => item.itemView.id == id);
361
593
 
@@ -363,38 +595,16 @@ export default {
363
595
  return;
364
596
  }
365
597
 
366
- // 合并全局数据到新数据
367
- const mergedData = this.mergeItemData(data);
598
+ // 确保数据结构完整性
599
+ this.ensureItemStructure(data);
368
600
 
369
- this.$set(this.data.list, index, mergedData);
601
+ // 直接更新数据,不再需要合并
602
+ this.$set(this.data.list, index, data);
370
603
  this.$nextTick(() => {
371
604
  const itemId = String(data.itemView.id);
372
- if (mergedData.bgAnimView.animUrl) {
373
- this.createLottieAnimation(
374
- 'bg',
375
- itemId,
376
- mergedData.bgAnimView,
377
- mergedData
378
- );
379
- }
380
-
381
- if (mergedData.loadingAnimView.animUrl) {
382
- this.createLottieAnimation(
383
- 'loading',
384
- itemId,
385
- mergedData.loadingAnimView,
386
- mergedData
387
- );
388
- }
389
-
390
- if (mergedData.frontAnimView.animUrl) {
391
- this.createLottieAnimation(
392
- 'front',
393
- itemId,
394
- mergedData.frontAnimView,
395
- mergedData
396
- );
397
- }
605
+
606
+ // 使用抽取的公共方法初始化动画
607
+ this.initItemAnimations(data, data);
398
608
  });
399
609
  },
400
610
  createLottieAnimation(type, itemId, animView, itemData) {
@@ -488,17 +698,31 @@ export default {
488
698
  },
489
699
  getListData(callback) {
490
700
  let data = Array.from(this.data.list);
491
- data = data.map((item) => ({
492
- ...item,
493
- itemView: {
494
- ...item.itemView,
495
- id: String(item.itemView.id),
496
- },
497
- }));
701
+ data = data.map((item) => {
702
+ // 确保每个项目都有完整的数据结构
703
+ const completeItem = { ...item };
704
+ this.ensureItemStructure(completeItem);
705
+
706
+ // 为了外部使用,合并全局数据到每个类型
707
+ REQUIRED_TYPES.forEach((type) => {
708
+ const globalData = this.data.globalData && this.data.globalData[type] || {};
709
+ completeItem[type] = {
710
+ ...globalData,
711
+ ...completeItem[type]
712
+ };
713
+ });
714
+
715
+ return {
716
+ ...completeItem,
717
+ itemView: {
718
+ ...completeItem.itemView,
719
+ id: String(completeItem.itemView.id),
720
+ },
721
+ };
722
+ });
498
723
  callback({ listData: data });
499
724
  },
500
725
  rightIconClick(e) {
501
- console.log('rightIconClick执行,阻止事件冒泡');
502
726
  e.preventDefault();
503
727
  e.stopPropagation();
504
728
  this.$emit('onClickRightIcon', e);
@@ -554,7 +778,6 @@ export default {
554
778
  },
555
779
 
556
780
  actionClick(e, item, index) {
557
- console.log('actionClick执行,阻止事件冒泡');
558
781
  e.preventDefault();
559
782
  e.stopPropagation(); // 阻止事件冒泡到itemViewClick
560
783
 
@@ -572,8 +795,6 @@ export default {
572
795
  },
573
796
 
574
797
  cleanupOtherAnimations(currentItemId) {
575
- console.log('清理其他项目的动画实例,当前项目ID:', currentItemId);
576
-
577
798
  Object.keys(this.lottieAnimations).forEach((key) => {
578
799
  if (!key.includes(`-${currentItemId}`)) {
579
800
  if (this.lottieAnimations[key]) {
@@ -621,103 +842,10 @@ export default {
621
842
  });
622
843
  },
623
844
 
624
- generateDataList() {
625
- if (!this.data || !this.data.list || !this.data.globalData) {
626
- return;
627
- }
628
- // 直接处理 this.data.list,为每个项目合并全局数据
629
- this.data.list.forEach((item, index) => {
630
- const processedItem = this.mergeItemData(item);
631
- this.$set(this.data.list, index, processedItem);
632
- });
633
-
634
- },
635
-
636
- mergeItemData(item) {
637
- const processedItem = {};
638
- const types = [
639
- 'itemView',
640
- 'text',
641
- 'actionButton',
642
- 'rightIcon',
643
- 'leftTopIcon',
644
- 'iconImage',
645
- 'selectIcon',
646
- 'loadingAnimView',
647
- 'bgAnimView',
648
- 'frontAnimView',
649
- ];
650
-
651
- types.forEach((type) => {
652
- // 使用mergeDate合并全局数据和项目数据
653
- processedItem[type] = this.mergeDate(item[type] || {}, type);
654
- });
655
845
 
656
- return processedItem;
657
- },
658
846
 
659
- getItemStyle(item, type) {
660
- const result = this.mergeDate(item[type] || {}, type);
661
- console.log(type, this.applyNormalAttribute(result), 'ggg');
662
- return this.applyNormalAttribute(result);
663
- },
664
- mergeDate(target, type) {
665
- const globalData = this.data.globalData[type] || {};
666
- return Object.assign({}, globalData, target);
667
- },
668
- applyNormalAttribute(data = {}, type) {
669
- let style = {
670
- color: data.textColor || '#000000',
671
- fontWeight: data.fontWeight || 'normal',
672
- fontFamily: data.fontFamilyPath || 'inherit',
673
- fontSize: `${(data.textSize || 16) * scla}px`, // 默认16px
674
- // 布局相关样式
675
- width:
676
- typeof data.width === 'number' && data.width > 0
677
- ? `${data.width * scla}px`
678
- : data.width === -1
679
- ? '%'
680
- : data.width || '',
681
- height:
682
- typeof data.height === 'number'
683
- ? `${data.height * scla}px`
684
- : data.height || '',
685
- backgroundColor: data.backgroundColor || 'transparent',
686
- borderRadius: `${(data.cornerRadius || 0) * scla}px`,
687
- opacity: data.alpha || 1,
688
- };
689
- if (typeof data.visibility === 'number') {
690
- if (data.visibility === 0) {
691
- style.display = 'none';
692
- } else {
693
- style.display = data.display || '';
694
- style.visibility = data.visibility === 1 ? 'visible' : 'hidden';
695
- }
696
- } else {
697
- style.display = data.display || '';
698
- }
699
847
 
700
- style.borderColor = this.data.isEditing
701
- ? data.selectedBorderColor || ''
702
- : data.normalBorderColor || '';
703
- style.borderWidth = this.data.isEditing
704
- ? `${(data.selectedBorderWidth || 0) * scla}px`
705
- : `${(data.normalBorderWidth || 0) * scla}px`;
706
- if (data.padding) {
707
- style.paddingTop = `${(data.padding[1] || 0) * scla}px`;
708
- style.paddingLeft = `${(data.padding[0] || 0) * scla}px`;
709
- style.paddingBottom = `${(data.padding[3] || 0) * scla}px`;
710
- style.paddingRight = `${(data.padding[2] || 0) * scla}px`;
711
- }
712
- if (data.margin) {
713
- style.marginTop = `${(data.margin[1] || 0) * scla}px`;
714
- style.marginLeft = `${(data.margin[0] || 0) * scla}px`;
715
- style.marginBottom = `${(data.margin[3] || 0) * scla}px`;
716
- style.marginRight = `${(data.margin[2] || 0) * scla}px`;
717
- }
718
848
 
719
- return style;
720
- },
721
849
  // 开始长按
722
850
  startLongPress(e, index) {
723
851
  if (!this.data.isEditable) {
@@ -730,7 +858,6 @@ export default {
730
858
  const touch = e.changedTouches[0];
731
859
  this.touchStartX = touch.pageX;
732
860
  this.touchStartY = touch.pageY;
733
- console.log('开始长按ddddddddd', touch, touch.pageY, touch.pageX);
734
861
  this.isScrolling = false; // 重置滚动状态
735
862
 
736
863
  // 设置新的长按计时器
@@ -788,10 +915,14 @@ export default {
788
915
 
789
916
  // 激活拖拽模式
790
917
  activateDragMode(index) {
791
- console.log('长按触发,进入编辑模式');
792
- // 进入编辑模式
793
-
794
918
  if (this.data.isEditing) {
919
+ // 重置交换状态
920
+ this.lastSwapPair = null;
921
+ this.stablePositionTime = 0;
922
+ this.isAnimating = false;
923
+ this.lastTouchX = this.touchStartX;
924
+ this.lastTouchY = this.touchStartY;
925
+
795
926
  // 设置当前拖拽索引
796
927
  this.draggingIndex = index;
797
928
  this.isTouchDragging = true;
@@ -802,8 +933,9 @@ export default {
802
933
  height: this.fixedItemHeight,
803
934
  };
804
935
 
805
- // 获取当前元素位置用于计算偏移
806
- const element = document.querySelectorAll('.grid-item')[index];
936
+ // 获取当前元素位置用于计算偏移 - 使用缓存的元素
937
+ const gridItems = this.getCachedGridItems();
938
+ const element = gridItems[index];
807
939
  if (element) {
808
940
  // 使用getBoundingClientRect获取视口中的位置
809
941
  const rect = element.getBoundingClientRect();
@@ -830,7 +962,6 @@ export default {
830
962
  this.updateDragClonePosition();
831
963
  });
832
964
 
833
- console.log('拖拽克隆已创建');
834
965
  }
835
966
  }
836
967
  if(!this.data.isEditing){
@@ -872,21 +1003,49 @@ export default {
872
1003
 
873
1004
  // 启动平滑动画(如果尚未启动)
874
1005
  if (!this.animationFrameId) {
875
- this.animationFrameId = requestAnimationFrame(this.updateDragPosition);
1006
+ this.animationFrameId = requestAnimationFrame(this.optimizedUpdateDragPosition);
876
1007
  }
877
1008
 
878
- // 简化的交换逻辑
879
- this.handleSwapLogic(touch.pageX, touch.pageY);
1009
+ // 使用节流优化交换逻辑,增加节流时间减少频繁调用
1010
+ if (!this.updatePositionThrottled) {
1011
+ this.updatePositionThrottled = this.throttle(this.handleSwapLogic.bind(this), 500);
1012
+ }
1013
+
1014
+ // 在动画过程中不执行交换逻辑
1015
+ if (!this.isAnimating) {
1016
+ this.updatePositionThrottled(touch.pageX, touch.pageY);
1017
+ }
880
1018
  },
881
1019
 
882
1020
  // 简化的交换处理逻辑
883
1021
  handleSwapLogic(touchX, touchY) {
884
1022
  // 控制交换频率,避免过于频繁的交换
885
1023
  const now = Date.now();
886
- if (now - this.lastSwapTime < 300) return;
1024
+ if (now - this.lastSwapTime < 150) return; // 增加频率控制到150ms
1025
+
1026
+ // 检查位置稳定性 - 如果触摸点基本没有移动,认为是稳定状态
1027
+ const moveDistance = Math.sqrt(
1028
+ Math.pow(touchX - this.lastTouchX, 2) + Math.pow(touchY - this.lastTouchY, 2)
1029
+ );
1030
+
1031
+ if (moveDistance < 10) { // 移动距离小于10px认为是稳定的
1032
+ if (this.stablePositionTime === 0) {
1033
+ this.stablePositionTime = now;
1034
+ } else if (now - this.stablePositionTime > 300) {
1035
+ // 稳定超过300ms,跳过交换检测
1036
+ return;
1037
+ }
1038
+ } else {
1039
+ // 位置发生明显移动,重置稳定时间
1040
+ this.stablePositionTime = 0;
1041
+ }
1042
+
1043
+ // 更新上次触摸位置
1044
+ this.lastTouchX = touchX;
1045
+ this.lastTouchY = touchY;
887
1046
 
888
- // 获取所有网格元素
889
- const gridItems = Array.from(document.querySelectorAll('.grid-item'));
1047
+ // 使用缓存的网格元素
1048
+ const gridItems = this.getCachedGridItems();
890
1049
  let targetIndex = null;
891
1050
 
892
1051
  // 寻找触摸点所在的目标元素
@@ -899,8 +1058,8 @@ export default {
899
1058
  // 触摸点是否在元素的中心区域内
900
1059
  const centerX = (rect.left + rect.right) / 2;
901
1060
  const centerY = (rect.top + rect.bottom) / 2;
902
- const halfWidth = rect.width * 0.4; // 缩小判断区域到40%,避免边缘误触
903
- const halfHeight = rect.height * 0.4;
1061
+ const halfWidth = rect.width * 0.3; // 缩小判断区域到30%,减少误触
1062
+ const halfHeight = rect.height * 0.3;
904
1063
 
905
1064
  // 检查触摸点是否在元素的中心区域内
906
1065
  if (
@@ -919,46 +1078,46 @@ export default {
919
1078
 
920
1079
  // 执行交换
921
1080
  if (targetIndex !== null && targetIndex !== this.draggingIndex) {
922
- [this.data.list[this.draggingIndex], this.data.list[targetIndex]] = [
923
- this.data.list[targetIndex],
924
- this.data.list[this.draggingIndex],
925
- ];
926
- console.log(`交换: ${this.draggingIndex} ↔ ${targetIndex}`);
927
-
928
- // 更新拖拽索引
1081
+ // 防止在动画过程中频繁交换
1082
+ if (this.isAnimating) return;
1083
+
1084
+ // 创建当前交换对的标识符
1085
+ const currentSwapPair = [this.draggingIndex, targetIndex].sort().join('-');
1086
+
1087
+ // 如果这是同一对元素的重复交换,需要更严格的时间控制
1088
+ if (this.lastSwapPair === currentSwapPair) {
1089
+ if (now - this.lastSwapTime < 1000) return; // 同一对元素需要等待1秒
1090
+ }
1091
+
1092
+ this.isAnimating = true;
1093
+
1094
+ // 使用Vue的$set来避免触发不必要的重新渲染
1095
+ const temp = this.data.list[this.draggingIndex];
1096
+ this.$set(this.data.list, this.draggingIndex, this.data.list[targetIndex]);
1097
+ this.$set(this.data.list, targetIndex, temp);
1098
+
1099
+ // 更新拖拽索引和交换记录
929
1100
  this.draggingIndex = targetIndex;
930
1101
  this.lastSwapTime = now;
1102
+ this.lastSwapPair = currentSwapPair;
1103
+
1104
+ // 重置稳定位置时间,因为刚刚发生了交换
1105
+ this.stablePositionTime = 0;
1106
+
1107
+ // 延迟清理缓存,等待DOM更新完成
1108
+ this.$nextTick(() => {
1109
+ setTimeout(() => {
1110
+ this.clearGridItemsCache();
1111
+ this.isAnimating = false;
1112
+ }, 100); // 增加延迟时间到100ms
1113
+ });
931
1114
  }
932
1115
  },
933
1116
 
934
- // 平滑更新拖拽位置的方法
935
- updateDragPosition() {
936
- const easing = 0.6;
937
- // 计算目标位置(考虑页面滚动位置)
938
- const targetX = this.targetTouchX - this.touchOffsetX + window.pageXOffset;
939
- const targetY = this.targetTouchY - this.touchOffsetY + window.pageYOffset;
940
-
941
- // 平滑插值
942
- this.dragCloneX += (targetX - this.dragCloneX) * easing;
943
- this.dragCloneY += (targetY - this.dragCloneY) * easing;
944
-
945
- // 当接近目标位置时直接设置为目标位置
946
- if (Math.abs(this.dragCloneX - targetX) < 0.5) {
947
- this.dragCloneX = targetX;
948
- }
949
- if (Math.abs(this.dragCloneY - targetY) < 0.5) {
950
- this.dragCloneY = targetY;
951
- }
952
-
953
- // 更新拖拽克隆元素位置
954
- this.updateDragClonePosition();
955
-
956
- this.animationFrameId = requestAnimationFrame(this.updateDragPosition);
957
- },
1117
+
958
1118
 
959
1119
  // 元素本身的触摸结束
960
1120
  endTouch(e) {
961
- console.log('触摸结束');
962
1121
  this.$emit('onDragEventEnd', e);
963
1122
  // 如果只是短按(还在计时),就取消长按
964
1123
  this.cancelLongPress();
@@ -973,20 +1132,32 @@ export default {
973
1132
  this.isTouchDragging = false;
974
1133
  this.isDragClone = false;
975
1134
  this.hoverIndex = null;
1135
+ this.isUpdatingPosition = false;
1136
+
1137
+ // 重置交换状态
1138
+ this.lastSwapPair = null;
1139
+ this.stablePositionTime = 0;
1140
+ this.lastTouchX = 0;
1141
+ this.lastTouchY = 0;
976
1142
 
977
1143
  // 销毁拖拽克隆元素
978
1144
  this.destroyDragClone();
1145
+
1146
+ // 等待可能的交换动画完成后再清理
1147
+ setTimeout(() => {
1148
+ this.clearGridItemsCache();
1149
+ this.isAnimating = false; // 重置动画状态
1150
+ }, 100);
979
1151
 
980
1152
  // 使用延时来重置拖拽索引,避免与点击事件冲突
981
1153
  setTimeout(() => {
982
1154
  this.draggingIndex = null;
983
- }, 100);
1155
+ }, 150);
984
1156
  }
985
1157
  },
986
1158
 
987
1159
  // 触摸结束处理 - 全局事件
988
1160
  onTouchEnd(e) {
989
- console.log('全局触摸结束');
990
1161
  // 取消动画帧
991
1162
  if (this.animationFrameId) {
992
1163
  cancelAnimationFrame(this.animationFrameId);
@@ -997,13 +1168,23 @@ export default {
997
1168
  this.isTouchDragging = false;
998
1169
  this.isDragClone = false;
999
1170
  this.hoverIndex = null;
1171
+ this.isUpdatingPosition = false;
1172
+
1173
+ // 重置交换状态
1174
+ this.lastSwapPair = null;
1175
+ this.stablePositionTime = 0;
1176
+ this.lastTouchX = 0;
1177
+ this.lastTouchY = 0;
1000
1178
 
1001
1179
  // 销毁拖拽克隆元素
1002
1180
  this.destroyDragClone();
1003
-
1004
- // 立即清除拖拽索引,避免状态卡住
1005
- this.draggingIndex = null;
1006
- this.isAnimating = false;
1181
+
1182
+ // 延迟清理,确保所有动画和状态更新完成
1183
+ setTimeout(() => {
1184
+ this.clearGridItemsCache();
1185
+ this.isAnimating = false;
1186
+ this.draggingIndex = null;
1187
+ }, 100);
1007
1188
  },
1008
1189
  // 获取grid-box宽度的方法
1009
1190
  updateGridBoxWidth() {
@@ -1012,7 +1193,6 @@ export default {
1012
1193
  if (gridBoxes && gridBoxes.length > 0) {
1013
1194
  const box = gridBoxes[0];
1014
1195
  this.gridBoxWidth = box.offsetWidth;
1015
- console.log('Grid box宽度:', this.gridBoxWidth);
1016
1196
 
1017
1197
  // 更新text样式,确保不超出
1018
1198
  const textElements = document.querySelectorAll('.itemText');
@@ -1029,7 +1209,6 @@ export default {
1029
1209
  Object.keys(this.lottieAnimations).forEach((key) => {
1030
1210
  const anim = this.lottieAnimations[key];
1031
1211
  if (anim && typeof anim.destroy === 'function') {
1032
- console.log(`销毁旧动画实例: ${key}`);
1033
1212
  try {
1034
1213
  anim.removeEventListener('complete');
1035
1214
  } catch (e) {
@@ -1043,31 +1222,11 @@ export default {
1043
1222
  // 重置动画实例对象
1044
1223
  this.lottieAnimations = {};
1045
1224
 
1046
- console.log('开始初始化Lottie动画,列表项数量:', this.data.list.length);
1047
-
1048
1225
  // 延迟一点时间确保DOM完全更新并且之前的动画已被销毁
1049
1226
  setTimeout(() => {
1050
1227
  // 为每个列表项初始化动画
1051
1228
  this.data.list.forEach((item) => {
1052
- const itemId = String(item.itemView.id);
1053
- // 加载背景动画
1054
- this.createLottieAnimation('bg', itemId, item.bgAnimView, item);
1055
-
1056
- // 加载加载中动画
1057
- this.createLottieAnimation(
1058
- 'loading',
1059
- itemId,
1060
- item.loadingAnimView,
1061
- item
1062
- );
1063
-
1064
- // 加载前景动画
1065
- this.createLottieAnimation(
1066
- 'front',
1067
- itemId,
1068
- item.frontAnimView,
1069
- item
1070
- );
1229
+ this.initItemAnimations(item);
1071
1230
  });
1072
1231
  }, 50); // 小延迟确保DOM已更新和旧动画已销毁
1073
1232
  });
@@ -1094,39 +1253,66 @@ export default {
1094
1253
  },
1095
1254
  },
1096
1255
  watch: {
1097
- 'data.isEditing': {
1098
- handler(newVal) {
1099
- this.generateDataList();
1100
- this.$nextTick(() => {
1101
- this.initLottieAnimations();
1102
- });
1103
- },
1104
- deep: true,
1105
- },
1106
- // 监听列表数据变化,自动更新处理后的数据
1256
+ // 监听列表数据变化,自动更新缓存
1107
1257
  'data.list': {
1108
1258
  handler(newList, oldList) {
1109
- // 防止死循环
1110
- if (newList.length !== oldList.length) {
1111
- console.log('检测到data.list变化');
1112
- this.generateDataList();
1113
- this.$nextTick(() => {
1114
- this.initLottieAnimations();
1115
- });
1259
+ // 跳过拖拽期间的更新,提高性能
1260
+ if (this.isTouchDragging) {
1261
+ return;
1262
+ }
1263
+ // 更精确的变化检测
1264
+ const hasLengthChange = newList.length !== oldList.length;
1265
+ if (hasLengthChange) {
1266
+
1267
+ // 清除DOM缓存
1268
+ this.clearGridItemsCache();
1269
+
1270
+ // 使用防抖处理列表变化
1271
+ if (!this.listChangeDebounced) {
1272
+ this.listChangeDebounced = this.debounce(() => {
1273
+ this.$nextTick(() => {
1274
+ // 优化内存使用
1275
+ // 确保每个项目都有完整的数据结构
1276
+ if (newList && Array.isArray(newList)) {
1277
+ newList.forEach((item, index) => {
1278
+ this.ensureItemStructure(item);
1279
+ });
1280
+ }
1281
+
1282
+ this.optimizeMemoryUsage();
1283
+ });
1284
+ }, 100);
1285
+ }
1286
+ this.listChangeDebounced();
1116
1287
  }
1117
1288
  },
1118
1289
  deep: true,
1119
1290
  },
1120
1291
  },
1121
-
1292
+ updated(){
1293
+ if(this.isTouchDragging){return}
1294
+ },
1122
1295
  mounted() {
1123
- console.log(this.data, 'djdjdjdjdjdjdj');
1296
+
1297
+ // 确保初始数据结构完整性
1298
+ if (this.data && this.data.list && Array.isArray(this.data.list)) {
1299
+ this.data.list.forEach((item, index) => {
1300
+ this.ensureItemStructure(item);
1301
+ });
1302
+ }
1303
+
1304
+ // 缓存容器元素
1305
+ this.containerElement = this.$el;
1306
+
1124
1307
  // 初始获取grid-box宽度
1125
1308
  this.updateGridBoxWidth();
1126
1309
  // 获取并存储固定宽高
1127
1310
  this.updateFixedItemSize();
1128
1311
  // 初始化Lottie动画
1129
1312
  this.initLottieAnimations();
1313
+
1314
+ // 初始化节流函数
1315
+ this.updatePositionThrottled = this.throttle(this.handleSwapLogic.bind(this), 100);
1130
1316
  },
1131
1317
  beforeDestroy() {
1132
1318
  // 确保清理所有事件监听器
@@ -1138,10 +1324,19 @@ export default {
1138
1324
  }
1139
1325
  // 清理拖拽克隆元素
1140
1326
  this.destroyDragClone();
1327
+
1328
+ // 清理缓存
1329
+ this.clearGridItemsCache();
1330
+
1331
+ // 清理节流和防抖函数
1332
+ this.updatePositionThrottled = null;
1333
+ this.listChangeDebounced = null;
1334
+
1335
+ // 清理容器引用
1336
+ this.containerElement = null;
1141
1337
  },
1142
1338
  created() {
1143
- // 初始化处理后的数据列表
1144
- this.generateDataList();
1339
+ // 组件创建时无需预处理数据,使用时按需获取全局数据
1145
1340
  },
1146
1341
  };
1147
1342
  </script>
@@ -1176,21 +1371,28 @@ button {
1176
1371
  grid-template-columns: repeat(var(--grid-cols), 1fr);
1177
1372
  grid-gap: var(--grid-gap);
1178
1373
  width: calc(100% - var(--grid-margin));
1374
+ /* 性能优化 */
1375
+ contain: layout style paint;
1376
+ transform: translateZ(0);
1179
1377
  }
1180
1378
 
1181
1379
  /* 优化排序过渡效果 */
1182
1380
  .grid-fade-move {
1183
- transition: transform 0.2s cubic-bezier(0.2, 0, 0.2, 1);
1381
+ transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
1184
1382
  will-change: transform;
1185
1383
  }
1186
1384
 
1187
1385
  .grid-item {
1188
1386
  position: relative;
1189
1387
  box-sizing: border-box;
1190
- transition: all 0.15s cubic-bezier(0.2, 0, 0.2, 1);
1388
+ transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
1191
1389
  will-change: transform, opacity, box-shadow;
1192
1390
  overflow: hidden;
1193
1391
  z-index: 1;
1392
+ /* 性能优化 */
1393
+ transform: translateZ(0);
1394
+ backface-visibility: hidden;
1395
+ perspective: 1000px;
1194
1396
  }
1195
1397
 
1196
1398
  .grid-box {
@@ -1252,7 +1454,7 @@ button {
1252
1454
  .hover-target {
1253
1455
  z-index: 50;
1254
1456
  transform: scale(1.05);
1255
- transition: all 0.1s ease-out;
1457
+ transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
1256
1458
  position: relative;
1257
1459
  }
1258
1460
 
@@ -1314,4 +1516,6 @@ button {
1314
1516
  transform: rotateZ(2deg);
1315
1517
  }
1316
1518
  }
1519
+
1520
+
1317
1521
  </style>