xydata-tools-uniapp 1.0.0

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.
@@ -0,0 +1,924 @@
1
+ <template>
2
+ <view class="con">
3
+ <template v-if="viewWidth">
4
+ <movable-area class="area" :style="{ height: areaHeight }" @mouseenter="mouseenter" @mouseleave="mouseleave">
5
+ <movable-view
6
+ v-for="(item, index) in imageList"
7
+ :key="item.id"
8
+ class="view"
9
+ direction="all"
10
+ :y="item.y"
11
+ :x="item.x"
12
+ :damping="40"
13
+ :disabled="!draggable"
14
+ @change="onChange($event, item)"
15
+ @touchstart="touchstart(item)"
16
+ @mousedown="touchstart(item)"
17
+ @touchend="touchend($event, item)"
18
+ @mouseup="touchend($event, item)"
19
+ :style="{
20
+ width: viewWidth + 'px',
21
+ height: viewWidth + 'px',
22
+ 'z-index': item.zIndex,
23
+ opacity: item.opacity
24
+ }"
25
+ >
26
+ <view
27
+ class="area-con"
28
+ :style="{
29
+ width: childWidth,
30
+ height: childWidth,
31
+ borderRadius: borderRadius + 'rpx',
32
+ transform: 'scale(' + item.scale + ')'
33
+ }"
34
+ >
35
+ <image
36
+ v-if="moveType === 'image'"
37
+ class="pre-image"
38
+ :src="item.src"
39
+ mode="aspectFill"
40
+ @load="handleMediaLoad(item)"
41
+ @error="handleMediaError(item)"
42
+ @click="handleClick(item, 'image')"
43
+ ></image>
44
+ <video
45
+ v-else-if="moveType === 'video'"
46
+ :id="'readonlyVideo' + index"
47
+ class="myVideo"
48
+ :src="item.src"
49
+ :controls="false"
50
+ :show-center-play-btn="true"
51
+ object-fit="cover"
52
+ @loadedmetadata="handleMediaLoad(item)"
53
+ @error="handleMediaError(item)"
54
+ @click="handleClick(item, 'video')"
55
+ ></video>
56
+ <view class="media-loading-overlay" v-if="item.loading">
57
+ <view class="loading-spinner"></view>
58
+ </view>
59
+ <view
60
+ class="del-con"
61
+ @click="delImages(item, index)"
62
+ @touchstart.stop="delImageMp(item, index)"
63
+ @touchend.stop="nothing()"
64
+ @mousedown.stop="nothing()"
65
+ @mouseup.stop="nothing()"
66
+ >
67
+ <view class="del-wrap">
68
+ <image
69
+ class="del-image"
70
+ src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAhCAYAAABX5MJvAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAhdEVYdENyZWF0aW9uIFRpbWUAMjAyMDowNzoyNSAyMTo1NDoyOU4TkJAAAADcSURBVFhH7ZfRCoMwDEXLvkjwwVf/bH/emmAyN6glTW9WBjsgwm28OeCLpj81Sil7zvlJ90UiONS/yY5VogsO6XrBg3IEQ5a/s8vRSWUAKmLqp2w5jz5BiNQEGMo3GbloDLtFXJ1IkaEuhAiiY6gEIqB4yqACSk9piIBiKQ8VUFpLviKg3C2rESKgWERCBZSWiEfgIfffYvrrsAgoISJ3Apy3zuTxcSxLQkV6ykNEPKVQkZEyiAiiZKgDIaC4upACSlcn5fM/+WuDCAHF1E/Z/N9AhkMZnPNDPI+UDjPIXgAQIGjNAAAAAElFTkSuQmCC"
71
+ >
72
+ </image>
73
+ </view>
74
+ </view>
75
+ </view>
76
+ </movable-view>
77
+ <!-- 添加按钮或自定义插槽 -->
78
+ <view class="add" v-if="imageList.length < number && showAddButton" :style="{ top: add.y, left: add.x, width: viewWidth + 'px', height: viewWidth + 'px' }" @click="$emit('add-click', $event)">
79
+ <view class="add-wrap" :style="{ width: childWidth, height: childWidth, borderRadius: borderRadius + 'rpx' }">
80
+ <slot name="add-button"></slot>
81
+ </view>
82
+ </view>
83
+ </movable-area>
84
+ </template>
85
+ </view>
86
+ </template>
87
+
88
+ <script>
89
+ export default {
90
+ // #ifdef VUE3
91
+ emits: ['input', 'add-click', 'del-image-confirm', 'update:modelValue'],
92
+ // #endif
93
+ // #ifndef VUE3
94
+ emits: ['input', 'add-click', 'del-image-confirm'],
95
+ // #endif
96
+ props: {
97
+ // #ifdef VUE3
98
+ modelValue: {
99
+ type: Array,
100
+ default: () => []
101
+ },
102
+ // #endif
103
+ // #ifndef VUE3
104
+ value: {
105
+ type: Array,
106
+ default: () => []
107
+ },
108
+ // #endif
109
+ // 从 list 元素对象中读取的键名
110
+ keyName: {
111
+ type: String,
112
+ default: null
113
+ },
114
+ // 选择图片数量限制
115
+ number: {
116
+ type: Number,
117
+ default: 6
118
+ },
119
+ // 图片父容器宽度(实际显示的图片宽度为 imageWidth / 1.1 ),单位 rpx
120
+ // imageWidth > 0 则 cols 无效
121
+ imageWidth: {
122
+ type: Number,
123
+ default: 0
124
+ },
125
+ // 图片列数
126
+ cols: {
127
+ type: Number,
128
+ default: 3
129
+ },
130
+ // 图片圆角,单位 rpx
131
+ borderRadius: {
132
+ type: Number,
133
+ default: 8
134
+ },
135
+ // 图片周围空白填充,单位 rpx
136
+ padding: {
137
+ type: Number,
138
+ default: 10
139
+ },
140
+ // 拖动图片时放大倍数 [0, ∞)
141
+ scale: {
142
+ type: Number,
143
+ default: 1.1
144
+ },
145
+ // 拖动图片时透明度 [0, 1]
146
+ opacity: {
147
+ type: Number,
148
+ default: 0.7
149
+ },
150
+ // 是否可拖动
151
+ draggable: {
152
+ type: Boolean,
153
+ default: true
154
+ },
155
+ // 删除前是否询问
156
+ delConfirm: {
157
+ type: Boolean,
158
+ default: false
159
+ },
160
+ // 添加按钮 (仅在 horizontal 模式有效)
161
+ addImage: {
162
+ type: Function,
163
+ default: () => null
164
+ },
165
+ // 删除按钮
166
+ delImage: {
167
+ type: Function,
168
+ default: () => null
169
+ },
170
+ // 媒体类型 image video
171
+ moveType: {
172
+ type: String,
173
+ default: 'image'
174
+ },
175
+ // 对齐方式 left center right
176
+ align: {
177
+ type: String,
178
+ default: 'left'
179
+ },
180
+ // 拖拽手感,默认 40
181
+ damping: {
182
+ type: Number,
183
+ default: 40
184
+ },
185
+ // 是否开启添加按钮(仅在横向模式生效)
186
+ enableAddButton: {
187
+ type: Boolean,
188
+ default: false
189
+ },
190
+
191
+ },
192
+ data() {
193
+ return {
194
+ imageList: [],
195
+ width: 0,
196
+ containerWidth: 0,
197
+ add: {
198
+ x: 0,
199
+ y: 0
200
+ },
201
+ colsValue: 0,
202
+ viewWidth: 0,
203
+ rightAlignOffset: 0,
204
+ tempItem: null,
205
+ timer: null,
206
+ changeStatus: true,
207
+ preStatus: true,
208
+ first: true,
209
+ lastTouchEndTime: null, // 记录最后一次 touchend 的时间,用于避免重复触发
210
+ clickThreshold: 20,
211
+ longPressTime: 300
212
+ };
213
+ },
214
+ computed: {
215
+ // 是否显示添加按钮(内部自动判断)
216
+ showAddButton() {
217
+ return this.enableAddButton && this.imageList.length < this.number;
218
+ },
219
+ areaHeight() {
220
+ let height = '';
221
+ let totalCount = this.imageList.length;
222
+
223
+ // 如果显示添加按钮,需要为按钮预留空间
224
+ if (this.showAddButton) {
225
+ totalCount = this.imageList.length + 1;
226
+ }
227
+
228
+ // 至少显示一行
229
+ const rows = Math.max(1, Math.ceil(totalCount / this.colsValue));
230
+ height = (rows * this.viewWidth).toFixed() + 'px';
231
+ return height;
232
+ },
233
+ childWidth() {
234
+ return this.viewWidth - this.rpx2px(this.padding) * 2 + 'px';
235
+ },
236
+ // 内部统一使用的值缓存
237
+ internalValue() {
238
+ // #ifdef VUE3
239
+ return this.modelValue;
240
+ // #endif
241
+ // #ifndef VUE3
242
+ return this.value;
243
+ // #endif
244
+ }
245
+ },
246
+ watch: {
247
+ internalValue: {
248
+ handler(n) {
249
+ this.handleValueChange(n);
250
+ },
251
+ deep: true,
252
+ immediate: true
253
+ },
254
+ viewWidth(n) {
255
+ if (n > 0 && this.first) {
256
+ // 如果是第一次初始化且获取到了有效宽度
257
+ this.recalculateAllPositions();
258
+ this.updateAddButtonPosition();
259
+ }
260
+ },
261
+ align() {
262
+ this.updateRightAlignOffset();
263
+ if (!this.viewWidth || !this.colsValue) {
264
+ return;
265
+ }
266
+ this.recalculateAllPositions();
267
+ this.updateAddButtonPosition();
268
+ }
269
+ },
270
+ created() {
271
+ this.width = uni.getSystemInfoSync().windowWidth;
272
+ },
273
+ mounted() {
274
+ // 使用 nextTick 并延时确保 DOM 已渲染且可测量
275
+ this.$nextTick(() => {
276
+ setTimeout(() => {
277
+ const query = uni.createSelectorQuery().in(this);
278
+ query.select('.con').boundingClientRect((data) => {
279
+ if (!data || !data.width) {
280
+ console.warn('无法获取到容器宽度,可能导致显示异常');
281
+ }
282
+ this.containerWidth = data.width || 0;
283
+ this.colsValue = this.cols;
284
+ this.viewWidth = (data.width || 0) / this.cols;
285
+ if (this.imageWidth > 0) {
286
+ this.viewWidth = this.rpx2px(this.imageWidth);
287
+ this.colsValue = Math.floor((data.width || 0) / this.viewWidth);
288
+ }
289
+ if (this.colsValue < 1) {
290
+ this.colsValue = 1;
291
+ }
292
+ this.updateRightAlignOffset();
293
+
294
+ // 初始化图片列表
295
+ this.imageList = [];
296
+ // 直接使用计算属性 internalValue,它已经处理了 Vue 2/3 的兼容性
297
+ const currentVal = this.internalValue || [];
298
+ for (let item of currentVal) {
299
+ this.addProperties(this.getSrc(item));
300
+ }
301
+
302
+ this.first = false;
303
+ });
304
+ query.exec();
305
+ }, 100);
306
+ });
307
+ },
308
+ methods: {
309
+ /**
310
+ * 根据索引和总数计算绝对位置(支持左右对齐)
311
+ * @param {Number} index - 当前图片索引
312
+ * @param {Number} total - 图片总数(如果显示添加按钮,需要包括按钮)
313
+ * @returns {Object} { absX, absY }
314
+ */
315
+ calculatePosition(index, total) {
316
+ const absY = Math.floor(index / this.colsValue);
317
+
318
+ if (this.align === 'right') {
319
+ // 靠右对齐:计算当前行的元素数量(包括按钮),然后从右往左排列
320
+ const currentRowItemCount = Math.min(total - absY * this.colsValue, this.colsValue);
321
+ const startX = this.colsValue - currentRowItemCount;
322
+ const positionInRow = index % this.colsValue;
323
+ const absX = startX + positionInRow;
324
+ return { absX, absY };
325
+ } else {
326
+ // 靠左对齐(默认)
327
+ const absX = index % this.colsValue;
328
+ return { absX, absY };
329
+ }
330
+ },
331
+ getRightOffset() {
332
+ return this.align === 'right' ? this.rightAlignOffset : 0;
333
+ },
334
+ getAlignedX(absX) {
335
+ return this.getRightOffset() + absX * this.viewWidth;
336
+ },
337
+ updateRightAlignOffset() {
338
+ if (!this.viewWidth || !this.colsValue || !this.containerWidth) {
339
+ this.rightAlignOffset = 0;
340
+ return;
341
+ }
342
+ if (this.align !== 'right') {
343
+ this.rightAlignOffset = 0;
344
+ return;
345
+ }
346
+ const theoreticalWidth = this.viewWidth * this.colsValue;
347
+ this.rightAlignOffset = Math.max(0, this.containerWidth - theoreticalWidth);
348
+ },
349
+
350
+ /**
351
+ * 获取总数(如果显示添加按钮,则包括按钮)
352
+ */
353
+ getTotalCount() {
354
+ // 如果是右对齐且显示添加按钮,则总数需要包括按钮
355
+ if (this.align === 'right' && this.showAddButton) {
356
+ return this.imageList.length + 1;
357
+ }
358
+ return this.imageList.length;
359
+ },
360
+
361
+ getSrc(item) {
362
+ if (this.keyName !== null) {
363
+ return item[this.keyName];
364
+ }
365
+ return item;
366
+ },
367
+ onChange(e, item) {
368
+ if (!item) return;
369
+ item.oldX = e.detail.x;
370
+ item.oldY = e.detail.y;
371
+ if (e.detail.source === 'touch') {
372
+ if (item.moveEnd) {
373
+ item.offset = Math.sqrt(Math.pow(item.oldX - this.getAlignedX(item.absX), 2) + Math.pow(item.oldY - item.absY * this.viewWidth, 2));
374
+ }
375
+ const offsetX = this.getRightOffset();
376
+ let x = Math.floor((e.detail.x - offsetX + this.viewWidth / 2) / this.viewWidth);
377
+ if (x < 0) {
378
+ x = 0;
379
+ }
380
+ if (x >= this.colsValue) return;
381
+ let y = Math.floor((e.detail.y + this.viewWidth / 2) / this.viewWidth);
382
+ let index = this.colsValue * y + x;
383
+ if (item.index != index && index < this.imageList.length) {
384
+ this.changeStatus = false;
385
+ for (let obj of this.imageList) {
386
+ if (item.index > index && obj.index >= index && obj.index < item.index) {
387
+ this.change(obj, 1);
388
+ } else if (item.index < index && obj.index <= index && obj.index > item.index) {
389
+ this.change(obj, -1);
390
+ } else if (obj.id != item.id) {
391
+ obj.offset = 0;
392
+ obj.x = obj.oldX;
393
+ obj.y = obj.oldY;
394
+ setTimeout(() => {
395
+ this.$nextTick(() => {
396
+ obj.x = this.getAlignedX(obj.absX);
397
+ obj.y = obj.absY * this.viewWidth;
398
+ obj.oldX = obj.x;
399
+ obj.oldY = obj.y;
400
+ });
401
+ }, 0);
402
+ }
403
+ }
404
+ item.index = index;
405
+ const totalWithButton = this.getTotalCount();
406
+ const pos = this.calculatePosition(index, totalWithButton);
407
+ item.absX = pos.absX;
408
+ item.absY = pos.absY;
409
+ if (!item.moveEnd) {
410
+ setTimeout(() => {
411
+ this.$nextTick(() => {
412
+ item.x = this.getAlignedX(item.absX);
413
+ item.y = item.absY * this.viewWidth;
414
+ item.oldX = item.x;
415
+ item.oldY = item.y;
416
+ });
417
+ }, 0);
418
+ }
419
+ this.sortList();
420
+ }
421
+ }
422
+ },
423
+ change(obj, i) {
424
+ obj.index += i;
425
+ obj.offset = 0;
426
+ obj.x = obj.oldX;
427
+ obj.y = obj.oldY;
428
+ const totalWithButton = this.getTotalCount();
429
+ const pos = this.calculatePosition(obj.index, totalWithButton);
430
+ obj.absX = pos.absX;
431
+ obj.absY = pos.absY;
432
+ setTimeout(() => {
433
+ this.$nextTick(() => {
434
+ obj.x = this.getAlignedX(obj.absX);
435
+ obj.y = obj.absY * this.viewWidth;
436
+ obj.oldX = obj.x;
437
+ obj.oldY = obj.y;
438
+ });
439
+ }, 0);
440
+ },
441
+ touchstart(item) {
442
+ // 如果不允许拖拽,则不执行任何操作
443
+ if (!this.draggable) {
444
+ return;
445
+ }
446
+
447
+ this.imageList.forEach((v) => {
448
+ v.zIndex = v.index + 9;
449
+ });
450
+ item.zIndex = 99;
451
+ item.moveEnd = true;
452
+ this.tempItem = item;
453
+
454
+ // 记录起始坐标,用于通过位移判断点击
455
+ item.startX = item.oldX;
456
+ item.startY = item.oldY;
457
+
458
+ this.timer = setTimeout(() => {
459
+ item.scale = this.scale;
460
+ item.opacity = this.opacity;
461
+ if (this.timer) {
462
+ clearTimeout(this.timer);
463
+ this.timer = null;
464
+ }
465
+ }, this.longPressTime);
466
+ },
467
+ handleClick(item, type) {
468
+ // 设为disable后无法触发touch事件
469
+ if (!this.draggable) {
470
+ if (type === 'video') {
471
+ this.playVideo(item);
472
+ } else {
473
+ this.previewImage(item);
474
+ }
475
+ }
476
+ },
477
+ touchend(event, item) {
478
+ const eventType = event.type; // 'touchend' 或 'mouseup'
479
+
480
+ // 如果是触摸事件,标记时间戳,避免后续的 mouseup 重复触发
481
+ if (eventType === 'touchend') {
482
+ this.lastTouchEndTime = Date.now();
483
+ } else if (eventType === 'mouseup') {
484
+ // 如果 100ms 内已经处理过 touchend,则忽略此 mouseup(避免重复)
485
+ if (this.lastTouchEndTime && Date.now() - this.lastTouchEndTime < 100) {
486
+ if (this.timer) {
487
+ clearTimeout(this.timer);
488
+ this.timer = null;
489
+ }
490
+ // 确保 scale 恢复为 1
491
+ item.scale = 1;
492
+ item.opacity = 1;
493
+ return;
494
+ }
495
+ }
496
+
497
+ // 判断是否为点击操作(而非拖动)
498
+ const moveDistance = Math.sqrt(Math.pow(item.oldX - (item.startX || 0), 2) + Math.pow(item.oldY - (item.startY || 0), 2));
499
+ const isClickAction = this.timer && moveDistance < this.rpx2px(this.clickThreshold);
500
+
501
+ if (isClickAction && this.preStatus && this.changeStatus) {
502
+ if (this.moveType === 'video') {
503
+ this.playVideo(item);
504
+ } else {
505
+ setTimeout(()=>{
506
+ this.previewImage(item);
507
+ },0)
508
+ }
509
+ }
510
+
511
+ // 无论是否触发点击,都需清理计时器防止后续意外放大
512
+ if (this.timer) {
513
+ clearTimeout(this.timer);
514
+ this.timer = null;
515
+ }
516
+
517
+ item.scale = 1;
518
+ item.opacity = 1;
519
+ item.x = item.oldX;
520
+ item.y = item.oldY;
521
+ item.offset = 0;
522
+ item.moveEnd = false;
523
+ setTimeout(() => {
524
+ this.$nextTick(() => {
525
+ item.x = this.getAlignedX(item.absX);
526
+ item.y = item.absY * this.viewWidth;
527
+ item.oldX = item.x;
528
+ item.oldY = item.y;
529
+ this.tempItem = null;
530
+ this.changeStatus = true;
531
+ });
532
+ }, 0);
533
+ },
534
+ playVideo(item) {
535
+ clearTimeout(this.timer);
536
+ this.timer = null;
537
+ this.$emit('videoClick', item);
538
+ },
539
+ previewImage(item) {
540
+ clearTimeout(this.timer);
541
+ this.timer = null;
542
+ let srcList = this.internalValue.map((v) => this.getSrc(v));
543
+ uni.previewImage({
544
+ urls: srcList,
545
+ current: item.src,
546
+ success: () => {
547
+ this.preStatus = false;
548
+ setTimeout(() => {
549
+ this.preStatus = true;
550
+ }, 600);
551
+ },
552
+ fail: (e) => {
553
+ console.log(e);
554
+ }
555
+ });
556
+ },
557
+ mouseenter() {
558
+ //#ifdef H5
559
+ this.imageList.forEach((v) => {
560
+ v.disable = false;
561
+ });
562
+ //#endif
563
+ },
564
+ mouseleave() {
565
+ //#ifdef H5
566
+ if (this.tempItem) {
567
+ this.imageList.forEach((v) => {
568
+ v.disable = true;
569
+ v.zIndex = v.index + 9;
570
+ v.offset = 0;
571
+ v.moveEnd = false;
572
+ if (v.id == this.tempItem.id) {
573
+ if (this.timer) {
574
+ clearTimeout(this.timer);
575
+ this.timer = null;
576
+ }
577
+ v.scale = 1;
578
+ v.opacity = 1;
579
+ v.x = v.oldX;
580
+ v.y = v.oldY;
581
+ this.$nextTick(() => {
582
+ v.x = this.getAlignedX(v.absX);
583
+ v.y = v.absY * this.viewWidth;
584
+ v.oldX = v.x;
585
+ v.oldY = v.y;
586
+ this.tempItem = null;
587
+ });
588
+ }
589
+ });
590
+ this.changeStatus = true;
591
+ }
592
+ //#endif
593
+ },
594
+ delImages(item, index) {
595
+ // 检查是否传入了有效的删除 Hook 且不是默认的空函数
596
+ if (this.delImage && typeof this.delImage === 'function' && this.delImage !== this.$options.props.delImage.default) {
597
+ this.delImage((confirmed) => {
598
+ if (confirmed !== false) {
599
+ this.delImageHandle(item, index);
600
+ }
601
+ });
602
+ } else {
603
+ this.delImageHandle(item, index);
604
+ }
605
+ },
606
+ delImageMp(item, index) {
607
+ //#ifdef MP
608
+ this.delImages(item, index);
609
+ //#endif
610
+ },
611
+ delImageHandle(item, index) {
612
+ // 设置标志,防止 watch 触发重新初始化
613
+ this.changeStatus = false;
614
+
615
+ // 删除指定图片
616
+ this.imageList.splice(index, 1);
617
+
618
+ // 重新排序:按照当前的 index 排序(保持拖拽后的逻辑顺序)
619
+ this.imageList.sort((a, b) => a.index - b.index);
620
+
621
+ // 更新索引和位置(根据排序后的新位置)
622
+ const totalWithButton = this.getTotalCount();
623
+ this.imageList.forEach((obj, i) => {
624
+ obj.index = i;
625
+ const pos = this.calculatePosition(i, totalWithButton);
626
+ obj.absX = pos.absX;
627
+ obj.absY = pos.absY;
628
+ const newX = this.getAlignedX(obj.absX);
629
+ const newY = obj.absY * this.viewWidth;
630
+
631
+ // 平滑过渡到新位置
632
+ obj.x = obj.oldX;
633
+ obj.y = obj.oldY;
634
+ this.$nextTick(() => {
635
+ obj.x = newX;
636
+ obj.y = newY;
637
+ obj.oldX = newX;
638
+ obj.oldY = newY;
639
+ });
640
+ });
641
+
642
+ this.updateAddButtonPosition();
643
+ this.sortList();
644
+
645
+ // 恢复标志
646
+ this.$nextTick(() => {
647
+ this.changeStatus = true;
648
+ });
649
+ },
650
+ sortList() {
651
+ const result = [];
652
+ let list = this.imageList.slice();
653
+ list.sort((a, b) => {
654
+ return a.index - b.index;
655
+ });
656
+ for (let s of list) {
657
+ let item = this.internalValue.find((d) => this.getSrc(d) == s.src);
658
+ if (item) {
659
+ result.push(item);
660
+ } else {
661
+ if (this.keyName !== null) {
662
+ result.push({
663
+ [this.keyName]: s.src
664
+ });
665
+ } else {
666
+ result.push(s.src);
667
+ }
668
+ }
669
+ }
670
+
671
+ this.$emit('input', result);
672
+ this.$emit('update:modelValue', result);
673
+ },
674
+ handleValueChange(n) {
675
+ if (!this.first && this.changeStatus) {
676
+ let flag = false;
677
+ for (let i = 0; i < n.length; i++) {
678
+ if (flag) {
679
+ this.addProperties(this.getSrc(n[i]));
680
+ continue;
681
+ }
682
+ if (this.imageList.length === i || this.imageList[i].src !== this.getSrc(n[i])) {
683
+ flag = true;
684
+ this.imageList.splice(i);
685
+ this.addProperties(this.getSrc(n[i]));
686
+ }
687
+ }
688
+ }
689
+ },
690
+ addProperties(item) {
691
+ const newIndex = this.imageList.length;
692
+
693
+ // 先添加图片到列表
694
+ this.imageList.push({
695
+ src: item,
696
+ x: 0,
697
+ y: 0,
698
+ oldX: 0,
699
+ oldY: 0,
700
+ absX: 0,
701
+ absY: 0,
702
+ scale: 1,
703
+ zIndex: 9,
704
+ opacity: 1,
705
+ index: newIndex,
706
+ id: this.guid(16),
707
+ disable: false,
708
+ offset: 0,
709
+ startX: 0,
710
+ startY: 0,
711
+ moveEnd: false,
712
+ loading: true
713
+ });
714
+
715
+ // 计算图片的位置
716
+ const totalWithButton = this.getTotalCount();
717
+ const pos = this.calculatePosition(newIndex, totalWithButton);
718
+ const obj = this.imageList[newIndex];
719
+ obj.absX = pos.absX;
720
+ obj.absY = pos.absY;
721
+ obj.x = this.getAlignedX(pos.absX);
722
+ obj.y = pos.absY * this.viewWidth;
723
+ obj.oldX = obj.x;
724
+ obj.oldY = obj.y;
725
+
726
+ this.updateAddButtonPosition();
727
+ if (this.align === 'right') {
728
+ this.recalculateAllPositions();
729
+ }
730
+ },
731
+
732
+ /**
733
+ * 重新计算所有图片的位置(用于右对齐时添加/删除图片)
734
+ */
735
+ recalculateAllPositions() {
736
+ const totalWithButton = this.getTotalCount();
737
+ this.imageList.forEach((obj, i) => {
738
+ const pos = this.calculatePosition(i, totalWithButton);
739
+ const newX = this.getAlignedX(pos.absX);
740
+ const newY = pos.absY * this.viewWidth;
741
+
742
+ // 如果位置有变化,进行动画过渡
743
+ if (obj.absX !== pos.absX || obj.absY !== pos.absY) {
744
+ obj.x = obj.oldX;
745
+ obj.y = obj.oldY;
746
+ obj.absX = pos.absX;
747
+ obj.absY = pos.absY;
748
+ this.$nextTick(() => {
749
+ obj.x = newX;
750
+ obj.y = newY;
751
+ obj.oldX = newX;
752
+ obj.oldY = newY;
753
+ });
754
+ } else {
755
+ obj.absX = pos.absX;
756
+ obj.absY = pos.absY;
757
+ obj.x = newX;
758
+ obj.y = newY;
759
+ obj.oldX = newX;
760
+ obj.oldY = newY;
761
+ }
762
+ });
763
+ },
764
+ nothing() {},
765
+ rpx2px(v) {
766
+ return (this.width * v) / 750;
767
+ },
768
+ guid(len = 32) {
769
+ const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
770
+ const uuid = [];
771
+ const radix = chars.length;
772
+ for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
773
+ uuid.shift();
774
+ return `u${uuid.join('')}`;
775
+ },
776
+ // 更新添加按钮的位置
777
+ updateAddButtonPosition() {
778
+ if (!this.viewWidth || !this.colsValue) {
779
+ console.warn('updateAddButtonPosition called before initialization');
780
+ return;
781
+ }
782
+ const length = this.imageList.length;
783
+
784
+ if (this.align === 'right' && this.showAddButton) {
785
+ // 右对齐且显示按钮:按钮的位置就是"虚拟的第 length 个元素"的位置
786
+ // 总数包括按钮本身
787
+ const totalWithButton = length + 1;
788
+ const pos = this.calculatePosition(length, totalWithButton);
789
+ this.add.x = this.getAlignedX(pos.absX) + 'px';
790
+ this.add.y = pos.absY * this.viewWidth + 'px';
791
+ } else {
792
+ // 左对齐或不显示按钮:按钮直接跟在最后一个元素后面
793
+ const absX = length % this.colsValue;
794
+ const absY = Math.floor(length / this.colsValue);
795
+ this.add.x = this.getAlignedX(absX) + 'px';
796
+ this.add.y = absY * this.viewWidth + 'px';
797
+ }
798
+ },
799
+ handleMediaLoad(item) {
800
+ if (!item) return;
801
+ item.loading = false;
802
+ },
803
+ handleMediaError(item) {
804
+ if (!item) return;
805
+ item.loading = false;
806
+ }
807
+ }
808
+ };
809
+ </script>
810
+
811
+ <style lang="scss" scoped>
812
+ .con {
813
+ .area {
814
+ width: 100%;
815
+
816
+ .view {
817
+ display: flex;
818
+ justify-content: center;
819
+ align-items: center;
820
+
821
+ .area-con {
822
+ position: relative;
823
+ overflow: hidden;
824
+
825
+ .pre-image {
826
+ width: 100%;
827
+ height: 100%;
828
+ }
829
+
830
+ .myVideo {
831
+ width: 100%;
832
+ height: 100%;
833
+ border-radius: 8rpx;
834
+ position: relative;
835
+ z-index: 0 !important;
836
+
837
+ &::after {
838
+ content: '';
839
+ position: absolute;
840
+ /* background: url('../../static/oxygen/icon_play1.png') no-repeat; */
841
+ background-size: 100% 100%;
842
+ top: 50%;
843
+ left: 50%;
844
+ transform: translate(-50%, -50%);
845
+ width: 60%;
846
+ height: 60%;
847
+ opacity: 0.9;
848
+ z-index: 1;
849
+ }
850
+ }
851
+
852
+ .media-loading-overlay {
853
+ position: absolute;
854
+ top: 0;
855
+ left: 0;
856
+ right: 0;
857
+ bottom: 0;
858
+ display: flex;
859
+ align-items: center;
860
+ justify-content: center;
861
+ background-color: rgba(0, 0, 0, 0.05);
862
+ border-radius: inherit;
863
+ z-index: 5;
864
+ }
865
+
866
+ .loading-spinner {
867
+ width: 40rpx;
868
+ height: 40rpx;
869
+ border: 4rpx solid rgba(0, 0, 0, 0.1);
870
+ border-top-color: #007aff;
871
+ border-radius: 50%;
872
+ animation: spinner-rotate 0.8s linear infinite;
873
+ }
874
+
875
+ .del-con {
876
+ position: absolute;
877
+ top: 0rpx;
878
+ right: 0rpx;
879
+ padding: 0 0 12rpx 12rpx;
880
+
881
+ .del-wrap {
882
+ width: 36rpx;
883
+ height: 36rpx;
884
+ background-color: rgba(0, 0, 0, 0.4);
885
+ border-radius: 0 0 0 10rpx;
886
+ display: flex;
887
+ justify-content: center;
888
+ align-items: center;
889
+
890
+ .del-image {
891
+ width: 20rpx;
892
+ height: 20rpx;
893
+ }
894
+ }
895
+ }
896
+ }
897
+ }
898
+
899
+ .add {
900
+ position: absolute;
901
+ display: flex;
902
+ justify-content: center;
903
+ align-items: center;
904
+
905
+ .add-wrap {
906
+ display: flex;
907
+ justify-content: center;
908
+ align-items: center;
909
+ background-color: #eeeeee;
910
+ }
911
+ }
912
+ }
913
+ }
914
+
915
+ @keyframes spinner-rotate {
916
+ 0% {
917
+ transform: rotate(0deg);
918
+ }
919
+
920
+ 100% {
921
+ transform: rotate(360deg);
922
+ }
923
+ }
924
+ </style>