xydata-tools 1.0.48 → 1.0.49

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