wedux-ui 0.4.0 → 0.6.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,568 @@
1
+ const { createDrag } = require('../../utils/drag');
2
+
3
+ Component({
4
+ relations: {
5
+ '../carousel-item/carousel-item': {
6
+ type: 'descendant',
7
+ linked: function () {
8
+ this._onChildrenChange();
9
+ },
10
+ unlinked: function () {
11
+ this._onChildrenChange();
12
+ },
13
+ },
14
+ },
15
+
16
+ properties: {
17
+ current: {
18
+ type: Number,
19
+ value: -1,
20
+ },
21
+ defaultCurrent: {
22
+ type: Number,
23
+ value: 0,
24
+ },
25
+ effect: {
26
+ type: String,
27
+ value: 'slide',
28
+ },
29
+ direction: {
30
+ type: String,
31
+ value: 'horizontal',
32
+ },
33
+ loop: {
34
+ type: Boolean,
35
+ value: true,
36
+ },
37
+ autoplay: {
38
+ type: Boolean,
39
+ value: false,
40
+ },
41
+ interval: {
42
+ type: Number,
43
+ value: 5000,
44
+ },
45
+ duration: {
46
+ type: Number,
47
+ value: 300,
48
+ },
49
+ slidesPerView: {
50
+ type: Number,
51
+ value: 1,
52
+ },
53
+ spaceBetween: {
54
+ type: Number,
55
+ value: 0,
56
+ },
57
+ showDots: {
58
+ type: Boolean,
59
+ value: true,
60
+ },
61
+ dotType: {
62
+ type: String,
63
+ value: 'dot',
64
+ },
65
+ dotPlacement: {
66
+ type: String,
67
+ value: 'bottom',
68
+ },
69
+ },
70
+
71
+ data: {
72
+ _trackStyle: '',
73
+ _dots: [],
74
+ _total: 0,
75
+ },
76
+
77
+ observers: {
78
+ current: function (val) {
79
+ if (val >= 0) {
80
+ this._goTo(val, true);
81
+ }
82
+ },
83
+ },
84
+
85
+ lifetimes: {
86
+ created() {
87
+ this._internalIndex = this.data.defaultCurrent;
88
+ this._drag = createDrag();
89
+ },
90
+
91
+ ready() {
92
+ this._containerWidth = 0;
93
+ this._containerHeight = 0;
94
+ this._slideSize = 0;
95
+ this._autoplayTimer = null;
96
+ this._loopResetTimer = null;
97
+ this._boundaryItemIndex = -1;
98
+
99
+ wx.nextTick(() => {
100
+ this._measure();
101
+ });
102
+ },
103
+
104
+ detached() {
105
+ this._stopAutoplay();
106
+ if (this._loopResetTimer) {
107
+ clearTimeout(this._loopResetTimer);
108
+ this._loopResetTimer = null;
109
+ }
110
+ },
111
+ },
112
+
113
+ methods: {
114
+ /* ── Relations ── */
115
+ _getItems() {
116
+ return this.getRelationNodes('../carousel-item/carousel-item') || [];
117
+ },
118
+
119
+ _onChildrenChange() {
120
+ const items = this._getItems();
121
+ const total = items.length;
122
+ this._internalIndex = Math.min(this._internalIndex || 0, Math.max(0, total - 1));
123
+ this._rebuildDots();
124
+ if (this._slideSize > 0) {
125
+ this._updateChildSizes();
126
+ this._goTo(this._internalIndex, false);
127
+ }
128
+ },
129
+
130
+ /* ── Measure ── */
131
+ _measure() {
132
+ const query = this.createSelectorQuery();
133
+ query.select('.w-carousel').boundingClientRect();
134
+ query.exec((res) => {
135
+ if (!res[0]) return;
136
+ const { width, height } = res[0];
137
+ this._containerWidth = width;
138
+ this._containerHeight = height;
139
+ const { slidesPerView, spaceBetween, direction } = this.data;
140
+ const containerSize = direction === 'horizontal' ? width : height;
141
+ this._slideSize = (containerSize - (slidesPerView - 1) * spaceBetween) / slidesPerView;
142
+
143
+ this._updateChildSizes();
144
+ this._goTo(this._internalIndex, false);
145
+ this._startAutoplay();
146
+ });
147
+ },
148
+
149
+ /* ── Child size dispatch (slide effect only) ── */
150
+ _updateChildSizes() {
151
+ const items = this._getItems();
152
+ const { effect, direction, spaceBetween } = this.data;
153
+ if (effect !== 'slide') return;
154
+
155
+ items.forEach((item) => {
156
+ const sizeStyle =
157
+ direction === 'horizontal'
158
+ ? `width: ${this._slideSize}px; height: 100%; margin-right: ${spaceBetween}px;`
159
+ : `width: 100%; height: ${this._slideSize}px; margin-bottom: ${spaceBetween}px;`;
160
+ item._setState('active', sizeStyle);
161
+ });
162
+ },
163
+
164
+ /* ── Dots ── */
165
+ _rebuildDots() {
166
+ const items = this._getItems();
167
+ const total = items.length;
168
+ const { slidesPerView } = this.data;
169
+ const dotCount =
170
+ slidesPerView <= 1 ? total : Math.max(0, total - Math.floor(slidesPerView) + 1);
171
+ const displayIndex = this._internalIndex;
172
+ const dots = Array.from({ length: dotCount }, (_, i) => ({
173
+ active: i === displayIndex,
174
+ }));
175
+ this.setData({ _total: total, _dots: dots });
176
+ },
177
+
178
+ /* ── Go to index ── */
179
+ _goTo(index, animate) {
180
+ if (animate === undefined) animate = true;
181
+ const items = this._getItems();
182
+ const total = items.length;
183
+ if (!total) return;
184
+
185
+ // 清除待执行的 loop 复位,立即复位
186
+ if (this._loopResetTimer) {
187
+ clearTimeout(this._loopResetTimer);
188
+ this._loopResetTimer = null;
189
+ this._updateChildSizes();
190
+ }
191
+
192
+ let newIndex = index;
193
+
194
+ if (this.data.loop) {
195
+ newIndex = ((index % total) + total) % total;
196
+ } else {
197
+ newIndex = Math.max(0, Math.min(index, total - 1));
198
+ }
199
+
200
+ const prev = this._internalIndex;
201
+ this._internalIndex = newIndex;
202
+
203
+ // slide + loop 边界过渡:无缝动画而非瞬移
204
+ if (
205
+ this.data.loop &&
206
+ this.data.effect === 'slide' &&
207
+ animate &&
208
+ total > 1 &&
209
+ this.data.slidesPerView <= 1
210
+ ) {
211
+ const isForwardLoop = prev === total - 1 && newIndex === 0;
212
+ const isBackwardLoop = prev === 0 && newIndex === total - 1;
213
+
214
+ if (isForwardLoop || isBackwardLoop) {
215
+ this._slideLoopTransition(prev, newIndex, isForwardLoop);
216
+ this._rebuildDots();
217
+ if (prev !== newIndex) {
218
+ this.triggerEvent('change', { current: newIndex, previous: prev });
219
+ }
220
+ return;
221
+ }
222
+ }
223
+
224
+ // 非边界过渡,清理拖拽时预置的 boundary item
225
+ if (this._boundaryItemIndex >= 0) {
226
+ this._boundaryItemIndex = -1;
227
+ if (animate) {
228
+ this._loopResetTimer = setTimeout(() => {
229
+ this._loopResetTimer = null;
230
+ this._updateChildSizes();
231
+ }, this.data.duration + 20);
232
+ } else {
233
+ this._updateChildSizes();
234
+ }
235
+ }
236
+
237
+ this._applyEffect(animate);
238
+ this._rebuildDots();
239
+
240
+ if (prev !== newIndex) {
241
+ this.triggerEvent('change', { current: newIndex, previous: prev });
242
+ }
243
+ },
244
+
245
+ /* ── Apply visual effect ── */
246
+ _applyEffect(animate) {
247
+ const { effect, direction, spaceBetween, duration } = this.data;
248
+ const dur = animate ? duration : 0;
249
+
250
+ if (effect === 'slide') {
251
+ const offset = this._internalIndex * (this._slideSize + spaceBetween);
252
+ const transform =
253
+ direction === 'horizontal' ? `translateX(${-offset}px)` : `translateY(${-offset}px)`;
254
+ this.setData({
255
+ _trackStyle: `transform: ${transform}; transition: transform ${dur}ms ease;`,
256
+ });
257
+ return;
258
+ }
259
+
260
+ this._applyNonSlideEffect(dur);
261
+ },
262
+
263
+ _applyNonSlideEffect(dur) {
264
+ const items = this._getItems();
265
+ const total = items.length;
266
+ const { effect, loop } = this.data;
267
+ const cur = this._internalIndex;
268
+
269
+ items.forEach((item, i) => {
270
+ let state, style;
271
+ const base = `width: 100%; height: 100%; position: absolute; top: 0; left: 0;`;
272
+
273
+ if (effect === 'fade') {
274
+ const isActive = i === cur;
275
+ state = isActive ? 'active' : 'hidden';
276
+ style = `${base} opacity: ${isActive ? 1 : 0}; transition: opacity ${dur}ms; z-index: ${isActive ? 1 : 0};`;
277
+ } else {
278
+ // card
279
+ const cardBase = `${base} border-radius: var(--w-carousel-card-radius); overflow: hidden;`;
280
+ const diff = i - cur;
281
+ const isPrev = diff === -1 || (loop && diff === total - 1);
282
+ const isNext = diff === 1 || (loop && diff === -(total - 1));
283
+
284
+ if (diff === 0) {
285
+ state = 'active';
286
+ style = `${cardBase} transform: scale(1) translateX(0); opacity: 1; transition: transform ${dur}ms ease, opacity ${dur}ms ease; z-index: 1;`;
287
+ } else if (isPrev) {
288
+ state = 'prev';
289
+ style = `${cardBase} transform: scale(0.85) translateX(-70%); opacity: 0.7; transition: transform ${dur}ms ease, opacity ${dur}ms ease; z-index: 0;`;
290
+ } else if (isNext) {
291
+ state = 'next';
292
+ style = `${cardBase} transform: scale(0.85) translateX(70%); opacity: 0.7; transition: transform ${dur}ms ease, opacity ${dur}ms ease; z-index: 0;`;
293
+ } else {
294
+ state = 'hidden';
295
+ style = `${cardBase} opacity: 0; pointer-events: none; z-index: 0;`;
296
+ }
297
+ }
298
+
299
+ item._setState(state, style);
300
+ });
301
+ },
302
+
303
+ /* ── Slide loop 无缝过渡 ── */
304
+ _getItemSizeStyle() {
305
+ const { direction, spaceBetween } = this.data;
306
+ return direction === 'horizontal'
307
+ ? `width: ${this._slideSize}px; height: 100%; margin-right: ${spaceBetween}px;`
308
+ : `width: 100%; height: ${this._slideSize}px; margin-bottom: ${spaceBetween}px;`;
309
+ },
310
+
311
+ _slideLoopTransition(fromIndex, toIndex, isForward) {
312
+ const items = this._getItems();
313
+ const total = items.length;
314
+ const { direction, spaceBetween, duration } = this.data;
315
+ const step = this._slideSize + spaceBetween;
316
+ const isH = direction === 'horizontal';
317
+ const axis = isH ? 'X' : 'Y';
318
+ const sizeStyle = this._getItemSizeStyle();
319
+
320
+ this._boundaryItemIndex = -1;
321
+
322
+ if (isForward) {
323
+ // 最后→第一:把 item[0] 移到最后一张后面
324
+ items[toIndex]._setState(
325
+ 'active',
326
+ `${sizeStyle} transform: translate${axis}(${total * step}px);`,
327
+ );
328
+ // track 向前滑动一格到 item[0] 的临时位置
329
+ const offset = total * step;
330
+ const transform = isH ? `translateX(${-offset}px)` : `translateY(${-offset}px)`;
331
+ this.setData({
332
+ _trackStyle: `transform: ${transform}; transition: transform ${duration}ms ease;`,
333
+ });
334
+ } else {
335
+ // 第一→最后:把 item[last] 移到第一张前面
336
+ items[toIndex]._setState(
337
+ 'active',
338
+ `${sizeStyle} transform: translate${axis}(${-total * step}px);`,
339
+ );
340
+ // track 向后滑动一格
341
+ const offset = -step;
342
+ const transform = isH ? `translateX(${-offset}px)` : `translateY(${-offset}px)`;
343
+ this.setData({
344
+ _trackStyle: `transform: ${transform}; transition: transform ${duration}ms ease;`,
345
+ });
346
+ }
347
+
348
+ // 动画结束后复位所有 item 和 track
349
+ this._loopResetTimer = setTimeout(() => {
350
+ this._loopResetTimer = null;
351
+ this._updateChildSizes();
352
+ this._applyEffect(false);
353
+ }, duration + 20);
354
+ },
355
+
356
+ /**
357
+ * 拖拽前预置边界 item,使跟手拖动时能看到下一张
358
+ */
359
+ _prepareBoundarySlide() {
360
+ const items = this._getItems();
361
+ const total = items.length;
362
+ if (total <= 1) return;
363
+
364
+ const cur = this._internalIndex;
365
+ const { direction, spaceBetween } = this.data;
366
+ const step = this._slideSize + spaceBetween;
367
+ const isH = direction === 'horizontal';
368
+ const axis = isH ? 'X' : 'Y';
369
+ const sizeStyle = this._getItemSizeStyle();
370
+
371
+ this._boundaryItemIndex = -1;
372
+
373
+ if (cur === 0) {
374
+ // 可能向前滑(回到最后)→ 把最后一张移到第一张前面
375
+ items[total - 1]._setState(
376
+ 'active',
377
+ `${sizeStyle} transform: translate${axis}(${-total * step}px);`,
378
+ );
379
+ this._boundaryItemIndex = total - 1;
380
+ } else if (cur === total - 1) {
381
+ // 可能向后滑(到第一张)→ 把第一张移到最后一张后面
382
+ items[0]._setState(
383
+ 'active',
384
+ `${sizeStyle} transform: translate${axis}(${total * step}px);`,
385
+ );
386
+ this._boundaryItemIndex = 0;
387
+ }
388
+ },
389
+
390
+ /* ── Touch handlers ── */
391
+ onTouchStart(e) {
392
+ this._stopAutoplay();
393
+ // 清除待执行的 loop 复位
394
+ if (this._loopResetTimer) {
395
+ clearTimeout(this._loopResetTimer);
396
+ this._loopResetTimer = null;
397
+ this._updateChildSizes();
398
+ this._applyEffect(false);
399
+ }
400
+
401
+ this._drag.start(e.touches[0]);
402
+ if (this.data.effect === 'slide') {
403
+ this._startTranslate = this._internalIndex * (this._slideSize + this.data.spaceBetween);
404
+ // loop 模式下预置边界 item,使跟手拖动无缝
405
+ if (this.data.loop && this.data.slidesPerView <= 1) {
406
+ this._prepareBoundarySlide();
407
+ }
408
+ }
409
+ },
410
+
411
+ onTouchMove(e) {
412
+ if (!this._drag.isActive()) return;
413
+ const { effect, direction } = this.data;
414
+ const touch = e.touches[0];
415
+
416
+ if (effect === 'slide') {
417
+ const delta = this._drag.delta(touch, direction);
418
+ const offset = this._startTranslate - delta;
419
+ const transform =
420
+ direction === 'horizontal' ? `translateX(${-offset}px)` : `translateY(${-offset}px)`;
421
+ this.setData({ _trackStyle: `transform: ${transform}; transition: none;` });
422
+ } else {
423
+ const containerSize =
424
+ direction === 'horizontal' ? this._containerWidth : this._containerHeight;
425
+ const progress = this._drag.progress(touch, direction, containerSize);
426
+ if (effect === 'fade') {
427
+ this._applyFadeDrag(progress);
428
+ } else {
429
+ this._applyCardDrag(progress);
430
+ }
431
+ }
432
+ },
433
+
434
+ onTouchEnd(e) {
435
+ const { direction } = this.data;
436
+ const containerSize =
437
+ direction === 'horizontal' ? this._containerWidth : this._containerHeight;
438
+ const result = this._drag.end(e.changedTouches[0], direction, containerSize);
439
+ if (!result) return;
440
+
441
+ if (result.swipe === 1) {
442
+ this._goTo(this._internalIndex + 1);
443
+ } else if (result.swipe === -1) {
444
+ this._goTo(this._internalIndex - 1);
445
+ } else {
446
+ this._goTo(this._internalIndex);
447
+ }
448
+ this._startAutoplay();
449
+ },
450
+
451
+ /* ── Fade 跟手拖动 ── */
452
+ _applyFadeDrag(progress) {
453
+ const items = this._getItems();
454
+ const total = items.length;
455
+ const cur = this._internalIndex;
456
+ const { loop } = this.data;
457
+ const absP = Math.abs(progress);
458
+
459
+ let targetIndex;
460
+ if (progress > 0) {
461
+ targetIndex = loop ? (cur + 1) % total : Math.min(cur + 1, total - 1);
462
+ } else {
463
+ targetIndex = loop ? (cur - 1 + total) % total : Math.max(cur - 1, 0);
464
+ }
465
+
466
+ const base = 'width: 100%; height: 100%; position: absolute; top: 0; left: 0;';
467
+ items.forEach((item, i) => {
468
+ let opacity, zIndex;
469
+ if (i === cur) {
470
+ opacity = 1 - absP;
471
+ zIndex = 1;
472
+ } else if (i === targetIndex && targetIndex !== cur) {
473
+ opacity = absP;
474
+ zIndex = 1;
475
+ } else {
476
+ opacity = 0;
477
+ zIndex = 0;
478
+ }
479
+ item._setState(
480
+ i === cur ? 'active' : 'hidden',
481
+ `${base} opacity: ${opacity}; z-index: ${zIndex}; transition: none;`,
482
+ );
483
+ });
484
+ },
485
+
486
+ /* ── Card 跟手拖动 ── */
487
+ _applyCardDrag(progress) {
488
+ const items = this._getItems();
489
+ const total = items.length;
490
+ const cur = this._internalIndex;
491
+ const { loop } = this.data;
492
+ const absP = Math.abs(progress);
493
+
494
+ const prevIndex = loop ? (cur - 1 + total) % total : cur - 1;
495
+ const nextIndex = loop ? (cur + 1) % total : cur + 1;
496
+
497
+ const base =
498
+ 'width: 100%; height: 100%; position: absolute; top: 0; left: 0; border-radius: var(--w-carousel-card-radius); overflow: hidden;';
499
+ items.forEach((item, i) => {
500
+ let style;
501
+ if (i === cur) {
502
+ const tx = -progress * 70;
503
+ const scale = 1 - absP * 0.15;
504
+ const opacity = 1 - absP * 0.3;
505
+ style = `${base} transform: scale(${scale}) translateX(${tx}%); opacity: ${opacity}; z-index: 1; transition: none;`;
506
+ } else if (i === prevIndex && prevIndex >= 0) {
507
+ const tx = progress < 0 ? -70 + absP * 70 : -70;
508
+ const scale = progress < 0 ? 0.85 + absP * 0.15 : 0.85;
509
+ const opacity = progress < 0 ? 0.7 + absP * 0.3 : 0.7;
510
+ style = `${base} transform: scale(${scale}) translateX(${tx}%); opacity: ${opacity}; z-index: 0; transition: none;`;
511
+ } else if (i === nextIndex && nextIndex < total) {
512
+ const tx = progress > 0 ? 70 - absP * 70 : 70;
513
+ const scale = progress > 0 ? 0.85 + absP * 0.15 : 0.85;
514
+ const opacity = progress > 0 ? 0.7 + absP * 0.3 : 0.7;
515
+ style = `${base} transform: scale(${scale}) translateX(${tx}%); opacity: ${opacity}; z-index: 0; transition: none;`;
516
+ } else {
517
+ style = `${base} opacity: 0; pointer-events: none; z-index: 0;`;
518
+ }
519
+ item._setState(i === cur ? 'active' : 'other', style);
520
+ });
521
+ },
522
+
523
+ /* ── Card tap ── */
524
+ _onItemTap(item) {
525
+ if (this.data.effect !== 'card') return;
526
+ const items = this._getItems();
527
+ const idx = items.indexOf(item);
528
+ if (idx !== -1 && idx !== this._internalIndex) {
529
+ this._goTo(idx);
530
+ }
531
+ },
532
+
533
+ /* ── Autoplay ── */
534
+ _startAutoplay() {
535
+ if (!this.data.autoplay) return;
536
+ this._stopAutoplay();
537
+ const items = this._getItems();
538
+ if (items.length <= 1) return;
539
+ this._autoplayTimer = setInterval(() => {
540
+ this._goTo(this._internalIndex + 1);
541
+ }, this.data.interval);
542
+ },
543
+
544
+ _stopAutoplay() {
545
+ if (this._autoplayTimer) {
546
+ clearInterval(this._autoplayTimer);
547
+ this._autoplayTimer = null;
548
+ }
549
+ },
550
+
551
+ /* ── Public methods ── */
552
+ to(index) {
553
+ this._goTo(index, true);
554
+ },
555
+
556
+ prev() {
557
+ this._goTo(this._internalIndex - 1, true);
558
+ },
559
+
560
+ next() {
561
+ this._goTo(this._internalIndex + 1, true);
562
+ },
563
+
564
+ getCurrentIndex() {
565
+ return this._internalIndex;
566
+ },
567
+ },
568
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "component": true,
3
+ "styleIsolation": "apply-shared"
4
+ }
@@ -0,0 +1,98 @@
1
+ .w-carousel {
2
+ position: relative;
3
+ overflow: hidden;
4
+ width: 100%;
5
+ height: 100%;
6
+ }
7
+
8
+ .w-carousel__track {
9
+ display: flex;
10
+ flex-direction: row;
11
+ height: 100%;
12
+ will-change: transform;
13
+ }
14
+
15
+ .w-carousel--vertical .w-carousel__track {
16
+ flex-direction: column;
17
+ width: 100%;
18
+ }
19
+
20
+ .w-carousel__slides {
21
+ position: relative;
22
+ width: 100%;
23
+ height: 100%;
24
+ }
25
+
26
+ /* Dots container */
27
+ .w-carousel__dots {
28
+ position: absolute;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ gap: 8rpx;
33
+ z-index: 10;
34
+ pointer-events: none;
35
+ }
36
+
37
+ .w-carousel__dots--bottom {
38
+ bottom: 24rpx;
39
+ left: 0;
40
+ right: 0;
41
+ flex-direction: row;
42
+ }
43
+
44
+ .w-carousel__dots--top {
45
+ top: 24rpx;
46
+ left: 0;
47
+ right: 0;
48
+ flex-direction: row;
49
+ }
50
+
51
+ .w-carousel__dots--left {
52
+ left: 24rpx;
53
+ top: 0;
54
+ bottom: 0;
55
+ flex-direction: column;
56
+ }
57
+
58
+ .w-carousel__dots--right {
59
+ right: 24rpx;
60
+ top: 0;
61
+ bottom: 0;
62
+ flex-direction: column;
63
+ }
64
+
65
+ /* Dot 圆点 */
66
+ .w-carousel__dot {
67
+ width: var(--w-carousel-dot-size);
68
+ height: var(--w-carousel-dot-size);
69
+ border-radius: 9999rpx;
70
+ background: var(--w-carousel-dot-color);
71
+ transition:
72
+ transform 300ms,
73
+ background 300ms;
74
+ flex-shrink: 0;
75
+ }
76
+
77
+ .w-carousel__dot--active {
78
+ background: var(--w-carousel-dot-color-active);
79
+ transform: scale(1.25);
80
+ }
81
+
82
+ /* Line 线条 */
83
+ .w-carousel__dot--line {
84
+ width: var(--w-carousel-dot-line-width);
85
+ height: 8rpx;
86
+ border-radius: 4rpx;
87
+ background: var(--w-carousel-dot-color);
88
+ transition:
89
+ width 300ms,
90
+ background 300ms;
91
+ transform: none;
92
+ }
93
+
94
+ .w-carousel__dot--line.w-carousel__dot--active {
95
+ width: var(--w-carousel-dot-line-width-active);
96
+ background: var(--w-carousel-dot-color-active);
97
+ transform: none;
98
+ }
@@ -0,0 +1,33 @@
1
+ <view
2
+ class="w-carousel w-carousel--{{effect}} w-carousel--{{direction}} w-carousel--dots-{{dotPlacement}}"
3
+ >
4
+ <view
5
+ wx:if="{{effect === 'slide'}}"
6
+ class="w-carousel__track"
7
+ style="{{_trackStyle}}"
8
+ bindtouchstart="onTouchStart"
9
+ bindtouchmove="onTouchMove"
10
+ bindtouchend="onTouchEnd"
11
+ >
12
+ <slot />
13
+ </view>
14
+ <view
15
+ wx:else
16
+ class="w-carousel__slides"
17
+ bindtouchstart="onTouchStart"
18
+ bindtouchmove="onTouchMove"
19
+ bindtouchend="onTouchEnd"
20
+ >
21
+ <slot />
22
+ </view>
23
+ <view
24
+ wx:if="{{showDots && _total > 1}}"
25
+ class="w-carousel__dots w-carousel__dots--{{dotPlacement}}"
26
+ >
27
+ <view
28
+ wx:for="{{_dots}}"
29
+ wx:key="index"
30
+ class="w-carousel__dot {{item.active ? 'w-carousel__dot--active' : ''}} {{dotType === 'line' ? 'w-carousel__dot--line' : ''}}"
31
+ />
32
+ </view>
33
+ </view>
@@ -0,0 +1,25 @@
1
+ Component({
2
+ relations: {
3
+ '../carousel/carousel': {
4
+ type: 'ancestor',
5
+ },
6
+ },
7
+
8
+ data: {
9
+ _style: '',
10
+ _state: 'hidden',
11
+ },
12
+
13
+ methods: {
14
+ _setState(state, style) {
15
+ this.setData({ _state: state, _style: style });
16
+ },
17
+
18
+ onTap() {
19
+ const parents = this.getRelationNodes('../carousel/carousel');
20
+ if (parents && parents.length) {
21
+ parents[0]._onItemTap(this);
22
+ }
23
+ },
24
+ },
25
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "component": true,
3
+ "styleIsolation": "apply-shared"
4
+ }
@@ -0,0 +1,6 @@
1
+ .w-carousel-item {
2
+ flex-shrink: 0;
3
+ position: relative;
4
+ overflow: hidden;
5
+ box-sizing: border-box;
6
+ }
@@ -0,0 +1,3 @@
1
+ <view class="w-carousel-item w-carousel-item--{{_state}}" style="{{_style}}" bindtap="onTap"
2
+ ><slot
3
+ /></view>
@@ -0,0 +1,95 @@
1
+ Component({
2
+ options: {
3
+ multipleSlots: true,
4
+ },
5
+
6
+ properties: {
7
+ src: {
8
+ type: String,
9
+ value: '',
10
+ },
11
+ mode: {
12
+ type: String,
13
+ value: 'aspectFill', // scaleToFill | aspectFit | aspectFill | widthFix | heightFix | ...
14
+ },
15
+ width: {
16
+ type: String,
17
+ value: '',
18
+ },
19
+ height: {
20
+ type: String,
21
+ value: '',
22
+ },
23
+ radius: {
24
+ type: String,
25
+ value: '',
26
+ },
27
+ round: {
28
+ type: Boolean,
29
+ value: false,
30
+ },
31
+ showLoading: {
32
+ type: Boolean,
33
+ value: true,
34
+ },
35
+ showError: {
36
+ type: Boolean,
37
+ value: true,
38
+ },
39
+ fade: {
40
+ type: Boolean,
41
+ value: true,
42
+ },
43
+ lazyLoad: {
44
+ type: Boolean,
45
+ value: false,
46
+ },
47
+ fallbackSrc: {
48
+ type: String,
49
+ value: '',
50
+ },
51
+ showMenuByLongpress: {
52
+ type: Boolean,
53
+ value: false,
54
+ },
55
+ },
56
+
57
+ data: {
58
+ _status: 'loading', // loading | loaded | error
59
+ _currentSrc: '',
60
+ _containerStyle: '',
61
+ },
62
+
63
+ observers: {
64
+ src(src) {
65
+ this.setData({
66
+ _currentSrc: src,
67
+ _status: src ? 'loading' : 'error',
68
+ });
69
+ },
70
+ 'width, height, radius'(width, height, radius) {
71
+ const parts = [];
72
+ if (width) parts.push(`width: ${width}`);
73
+ if (height) parts.push(`height: ${height}`);
74
+ if (radius) parts.push(`border-radius: ${radius}`);
75
+ this.setData({ _containerStyle: parts.join('; ') });
76
+ },
77
+ },
78
+
79
+ methods: {
80
+ handleLoad(e) {
81
+ this.setData({ _status: 'loaded' });
82
+ this.triggerEvent('load', e.detail);
83
+ },
84
+
85
+ handleError(e) {
86
+ const { fallbackSrc, _currentSrc } = this.data;
87
+ if (fallbackSrc && _currentSrc !== fallbackSrc) {
88
+ this.setData({ _currentSrc: fallbackSrc });
89
+ } else {
90
+ this.setData({ _status: 'error' });
91
+ }
92
+ this.triggerEvent('error', e.detail);
93
+ },
94
+ },
95
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "component": true,
3
+ "styleIsolation": "apply-shared"
4
+ }
@@ -0,0 +1,104 @@
1
+ @import '../../styles/iconfont.scss';
2
+
3
+ .w-image {
4
+ position: relative;
5
+ display: inline-block;
6
+ overflow: hidden;
7
+ border-radius: var(--w-image-radius, 0);
8
+ background-color: var(--w-image-bg, var(--color-bg-base));
9
+ }
10
+
11
+ .w-image--round {
12
+ border-radius: var(--radius-full);
13
+ }
14
+
15
+ /* Image */
16
+ .w-image__img {
17
+ display: block;
18
+ width: 100%;
19
+ height: 100%;
20
+ }
21
+
22
+ .w-image--fade-in .w-image__img {
23
+ animation: w-image-fade-in var(--w-image-fade-duration, 300ms) ease-out;
24
+ }
25
+
26
+ @keyframes w-image-fade-in {
27
+ from {
28
+ opacity: 0;
29
+ }
30
+ to {
31
+ opacity: 1;
32
+ }
33
+ }
34
+
35
+ /* Placeholder (loading & error) */
36
+ .w-image__placeholder {
37
+ position: absolute;
38
+ top: 0;
39
+ left: 0;
40
+ width: 100%;
41
+ height: 100%;
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ }
46
+
47
+ /* Slot controls: hide default when slot has content */
48
+ .w-image__loading-slot:empty {
49
+ display: none;
50
+ }
51
+
52
+ .w-image__loading-slot:empty + .w-image__loading-default {
53
+ display: flex;
54
+ }
55
+
56
+ .w-image__loading-slot:not(:empty) + .w-image__loading-default {
57
+ display: none;
58
+ }
59
+
60
+ .w-image__error-slot:empty {
61
+ display: none;
62
+ }
63
+
64
+ .w-image__error-slot:empty + .w-image__error-default {
65
+ display: flex;
66
+ }
67
+
68
+ .w-image__error-slot:not(:empty) + .w-image__error-default {
69
+ display: none;
70
+ }
71
+
72
+ /* Loading default: spinning animation */
73
+ .w-image__loading-default {
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ }
78
+
79
+ .w-image__loading-icon {
80
+ width: var(--w-image-loading-icon-size, 48rpx);
81
+ height: var(--w-image-loading-icon-size, 48rpx);
82
+ border: 4rpx solid var(--color-separator);
83
+ border-top-color: var(--color-text-secondary);
84
+ border-radius: 50%;
85
+ animation: w-image-spin 0.8s linear infinite;
86
+ }
87
+
88
+ @keyframes w-image-spin {
89
+ to {
90
+ transform: rotate(360deg);
91
+ }
92
+ }
93
+
94
+ /* Error default */
95
+ .w-image__error-default {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ }
100
+
101
+ .w-image__error-icon {
102
+ font-size: var(--w-image-error-icon-size, 48rpx);
103
+ color: var(--color-text-placeholder);
104
+ }
@@ -0,0 +1,35 @@
1
+ <view
2
+ class="w-image {{round ? 'w-image--round' : ''}} {{_status === 'loaded' && fade ? 'w-image--fade-in' : ''}}"
3
+ style="{{_containerStyle}}"
4
+ >
5
+ <image
6
+ wx:if="{{_currentSrc && _status !== 'error'}}"
7
+ class="w-image__img"
8
+ src="{{_currentSrc}}"
9
+ mode="{{mode}}"
10
+ lazy-load="{{lazyLoad}}"
11
+ show-menu-by-longpress="{{showMenuByLongpress}}"
12
+ bindload="handleLoad"
13
+ binderror="handleError"
14
+ ></image>
15
+
16
+ <!-- Loading -->
17
+ <view wx:if="{{_status === 'loading' && showLoading}}" class="w-image__placeholder">
18
+ <view class="w-image__loading-slot">
19
+ <slot name="loading"></slot>
20
+ </view>
21
+ <view class="w-image__loading-default">
22
+ <view class="w-image__loading-icon"></view>
23
+ </view>
24
+ </view>
25
+
26
+ <!-- Error -->
27
+ <view wx:if="{{_status === 'error' && showError}}" class="w-image__placeholder">
28
+ <view class="w-image__error-slot">
29
+ <slot name="error"></slot>
30
+ </view>
31
+ <view class="w-image__error-default">
32
+ <view class="w-image__error-icon iconfont icon-broken-image"></view>
33
+ </view>
34
+ </view>
35
+ </view>
@@ -1,8 +1,8 @@
1
1
  @font-face {
2
2
  font-family: "iconfont"; /* Project id 5134551 */
3
3
  src:
4
- url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'),
5
- url('//at.alicdn.com/t/c/font_5134551_zr54w3qnk2m.ttf?t=1775402273099') format('truetype');
4
+ url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'),
5
+ url('//at.alicdn.com/t/c/font_5134551_7oyrdzd7ntn.ttf?t=1775471224272') format('truetype');
6
6
  }
7
7
 
8
8
  .iconfont {
@@ -13,6 +13,10 @@
13
13
  -moz-osx-font-smoothing: grayscale;
14
14
  }
15
15
 
16
+ .icon-broken-image:before {
17
+ content: "\e600";
18
+ }
19
+
16
20
  .icon-empty:before {
17
21
  content: "\e6a6";
18
22
  }
@@ -304,4 +304,19 @@ page {
304
304
  /* Price */
305
305
  --w-price-color: var(--color-brand);
306
306
  --w-price-original-color: var(--color-text-secondary);
307
+
308
+ /* Image */
309
+ --w-image-radius: 0;
310
+ --w-image-bg: var(--color-bg-base);
311
+ --w-image-fade-duration: 300ms;
312
+ --w-image-loading-icon-size: 48rpx;
313
+ --w-image-error-icon-size: 48rpx;
314
+
315
+ /* Carousel */
316
+ --w-carousel-dot-color: rgba(255, 255, 255, 0.5);
317
+ --w-carousel-dot-color-active: #ffffff;
318
+ --w-carousel-dot-size: 16rpx;
319
+ --w-carousel-dot-line-width: 16rpx;
320
+ --w-carousel-dot-line-width-active: 48rpx;
321
+ --w-carousel-card-radius: var(--w-radius-lg, 16rpx);
307
322
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 拖动跟踪工具
3
+ * 封装 touch 事件的追踪、delta 计算、滑动判定
4
+ */
5
+ function createDrag() {
6
+ let startX = 0;
7
+ let startY = 0;
8
+ let startTime = 0;
9
+ let active = false;
10
+
11
+ return {
12
+ /** 记录起点 */
13
+ start(touch) {
14
+ active = true;
15
+ startX = touch.clientX;
16
+ startY = touch.clientY;
17
+ startTime = Date.now();
18
+ },
19
+
20
+ /** 计算拖动位移(px) */
21
+ delta(touch, direction) {
22
+ const dx = touch.clientX - startX;
23
+ const dy = touch.clientY - startY;
24
+ return direction === 'horizontal' ? dx : dy;
25
+ },
26
+
27
+ /**
28
+ * 计算拖动进度(-1 ~ 1),正值 = 向 next 拖动
29
+ * @param {Object} touch
30
+ * @param {string} direction
31
+ * @param {number} containerSize
32
+ */
33
+ progress(touch, direction, containerSize) {
34
+ if (!containerSize) return 0;
35
+ const d = this.delta(touch, direction);
36
+ return Math.max(-1, Math.min(1, -d / containerSize));
37
+ },
38
+
39
+ /**
40
+ * 结束拖动,返回滑动判定
41
+ * @returns {{ delta: number, velocity: number, swipe: number } | null}
42
+ * swipe: 1 = next, -1 = prev, 0 = 回弹
43
+ */
44
+ end(touch, direction, containerSize) {
45
+ if (!active) return null;
46
+ active = false;
47
+ const delta = this.delta(touch, direction);
48
+ const elapsed = Date.now() - startTime;
49
+ const velocity = elapsed > 0 ? delta / elapsed : 0;
50
+
51
+ let swipe = 0;
52
+ if (delta < -containerSize * 0.3 || velocity < -0.4) {
53
+ swipe = 1;
54
+ } else if (delta > containerSize * 0.3 || velocity > 0.4) {
55
+ swipe = -1;
56
+ }
57
+
58
+ return { delta, velocity, swipe };
59
+ },
60
+
61
+ isActive() {
62
+ return active;
63
+ },
64
+
65
+ cancel() {
66
+ active = false;
67
+ },
68
+ };
69
+ }
70
+
71
+ module.exports = { createDrag };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wedux-ui",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "微信小程序组件库,支持主题切换",
5
5
  "miniprogram": "miniprogram_dist",
6
6
  "files": [