vhall-mini-sdk 0.4.1 → 0.4.3

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.
Files changed (107) hide show
  1. package/dist/components/gift/gift-display/gift-display.js +118 -0
  2. package/dist/components/gift/gift-display/gift-display.json +3 -0
  3. package/dist/components/gift/gift-display/gift-display.wxml +16 -0
  4. package/dist/components/gift/gift-display/gift-display.wxss +1 -0
  5. package/dist/components/gift/gift-sender/gift-sender.js +63 -0
  6. package/dist/components/gift/gift-sender/gift-sender.json +6 -0
  7. package/dist/components/gift/gift-sender/gift-sender.wxml +19 -0
  8. package/dist/components/gift/gift-sender/gift-sender.wxss +1 -0
  9. package/dist/components/lottery/lottery-accept/index.js +249 -0
  10. package/dist/components/lottery/lottery-accept/index.json +4 -0
  11. package/dist/components/lottery/lottery-accept/index.wxml +49 -0
  12. package/dist/components/lottery/lottery-accept/index.wxss +1 -0
  13. package/dist/components/lottery/lottery-history/index.js +36 -0
  14. package/dist/components/lottery/lottery-history/index.json +3 -0
  15. package/dist/components/lottery/lottery-history/index.wxml +28 -0
  16. package/dist/components/lottery/lottery-history/index.wxss +1 -0
  17. package/dist/components/lottery/lottery-shake/lottery-shake.js +328 -0
  18. package/dist/components/lottery/lottery-shake/lottery-shake.json +1 -0
  19. package/dist/components/lottery/lottery-shake/lottery-shake.wxml +52 -0
  20. package/dist/components/lottery/lottery-shake/lottery-shake.wxss +1 -0
  21. package/dist/components/lottery/lottery-slot/lottery-slot.js +230 -0
  22. package/dist/components/lottery/lottery-slot/lottery-slot.json +1 -0
  23. package/dist/components/lottery/lottery-slot/lottery-slot.wxml +62 -0
  24. package/dist/components/lottery/lottery-slot/lottery-slot.wxss +1 -0
  25. package/dist/components/lottery/lottery-wheel/lottery-wheel.js +153 -0
  26. package/dist/components/lottery/lottery-wheel/lottery-wheel.json +4 -0
  27. package/dist/components/lottery/lottery-wheel/lottery-wheel.wxml +91 -0
  28. package/dist/components/lottery/lottery-wheel/lottery-wheel.wxss +1 -0
  29. package/dist/components/lottery/lottery-winners/index.js +101 -0
  30. package/dist/components/lottery/lottery-winners/index.json +4 -0
  31. package/dist/components/lottery/lottery-winners/index.wxml +39 -0
  32. package/dist/components/lottery/lottery-winners/index.wxss +1 -0
  33. package/dist/components/lottery/lottery.js +388 -0
  34. package/dist/components/lottery/lottery.json +11 -0
  35. package/dist/components/lottery/lottery.wxml +78 -0
  36. package/dist/components/lottery/lottery.wxss +1 -0
  37. package/dist/components/lottery/wheel.json +49 -0
  38. package/dist/components/mall-reward/common/reward-card.wxss +0 -0
  39. package/dist/components/mall-reward/duration-icon/index.js +101 -0
  40. package/dist/components/mall-reward/duration-icon/index.json +3 -0
  41. package/dist/components/mall-reward/duration-icon/index.wxml +7 -0
  42. package/dist/components/mall-reward/duration-icon/index.wxss +1 -0
  43. package/dist/components/mall-reward/f-count-down/index.js +61 -0
  44. package/dist/components/mall-reward/f-count-down/index.json +3 -0
  45. package/dist/components/mall-reward/f-count-down/index.wxml +9 -0
  46. package/dist/components/mall-reward/f-count-down/index.wxss +1 -0
  47. package/dist/components/mall-reward/f-empty/index.js +11 -0
  48. package/dist/components/mall-reward/f-empty/index.json +3 -0
  49. package/dist/components/mall-reward/f-empty/index.wxml +8 -0
  50. package/dist/components/mall-reward/f-empty/index.wxss +1 -0
  51. package/dist/components/mall-reward/f-history/index.js +84 -0
  52. package/dist/components/mall-reward/f-history/index.json +4 -0
  53. package/dist/components/mall-reward/f-history/index.wxml +38 -0
  54. package/dist/components/mall-reward/f-history/index.wxss +1 -0
  55. package/dist/components/mall-reward/f-open/index.js +36 -0
  56. package/dist/components/mall-reward/f-open/index.json +3 -0
  57. package/dist/components/mall-reward/f-open/index.wxml +10 -0
  58. package/dist/components/mall-reward/f-open/index.wxss +1 -0
  59. package/dist/components/mall-reward/f-receive/index.js +36 -0
  60. package/dist/components/mall-reward/f-receive/index.json +3 -0
  61. package/dist/components/mall-reward/f-receive/index.wxml +9 -0
  62. package/dist/components/mall-reward/f-receive/index.wxss +1 -0
  63. package/dist/components/mall-reward/index.js +374 -0
  64. package/dist/components/mall-reward/index.json +8 -0
  65. package/dist/components/mall-reward/index.wxml +4 -0
  66. package/dist/components/mall-reward/index.wxss +0 -0
  67. package/dist/components/mall-reward/main-popup/index.js +46 -0
  68. package/dist/components/mall-reward/main-popup/index.json +12 -0
  69. package/dist/components/mall-reward/main-popup/index.wxml +16 -0
  70. package/dist/components/mall-reward/main-popup/index.wxss +1 -0
  71. package/dist/components/praise/praise.js +150 -0
  72. package/dist/components/praise/praise.json +5 -0
  73. package/dist/components/praise/praise.wxml +17 -0
  74. package/dist/components/praise/praise.wxss +1 -0
  75. package/dist/components/product/product-card/product-card.js +109 -0
  76. package/dist/components/product/product-card/product-card.json +3 -0
  77. package/dist/components/product/product-card/product-card.wxml +47 -0
  78. package/dist/components/product/product-card/product-card.wxss +1 -0
  79. package/dist/components/product/product-detail/product-detail.js +127 -0
  80. package/dist/components/product/product-detail/product-detail.json +3 -0
  81. package/dist/components/product/product-detail/product-detail.wxml +57 -0
  82. package/dist/components/product/product-detail/product-detail.wxss +1 -0
  83. package/dist/components/product/product-item/product-item.js +56 -0
  84. package/dist/components/product/product-item/product-item.json +3 -0
  85. package/dist/components/product/product-item/product-item.wxml +35 -0
  86. package/dist/components/product/product-item/product-item.wxss +1 -0
  87. package/dist/components/product/product-list/product-list.js +67 -0
  88. package/dist/components/product/product-list/product-list.json +6 -0
  89. package/dist/components/product/product-list/product-list.wxml +16 -0
  90. package/dist/components/product/product-list/product-list.wxss +1 -0
  91. package/dist/components/product/product-order/product-order.js +394 -0
  92. package/dist/components/product/product-order/product-order.json +3 -0
  93. package/dist/components/product/product-order/product-order.wxml +75 -0
  94. package/dist/components/product/product-order/product-order.wxss +1 -0
  95. package/dist/pubComponents/popup/popup.wxss +1 -0
  96. package/dist/sdk/config/index.js +1 -0
  97. package/dist/sdk/plugins/InteractServer.js +1 -0
  98. package/dist/sdk/plugins/LotteryServer.js +1 -0
  99. package/dist/sdk/plugins/MallRewardServer.js +1 -0
  100. package/dist/sdk/plugins/ProductServer.js +1 -0
  101. package/lib/components/player/components/controls/controls.wxml +6 -3
  102. package/lib/components/player/components/controls/controls.wxss +1 -1
  103. package/lib/components/player/player.wxml +26 -24
  104. package/lib/components/player/player.wxss +1 -1
  105. package/lib/sdk/main/index.js +2 -2
  106. package/lib/shared/mpUtils.js +1 -1
  107. package/package.json +1 -1
@@ -0,0 +1,328 @@
1
+ Component({
2
+ properties: {
3
+ remark: { type: String, value: '' },
4
+ // 抽奖 ID:用于区分不同抽奖的缓存 key
5
+ lotteryId: { type: String, value: '' },
6
+ autoplay: { type: Boolean, value: false },
7
+ loop: { type: Boolean, value: false },
8
+ // 外部控制:true 开始摇晃,false 平滑停止(与转盘/老虎机一致)
9
+ running: {
10
+ type: Boolean,
11
+ value: false,
12
+ observer(val) {
13
+ if (val) {
14
+ this.spin();
15
+ } else {
16
+ this.stop();
17
+ }
18
+ },
19
+ },
20
+ },
21
+
22
+ data: {
23
+ spinning: false,
24
+ stopping: false,
25
+ stopped: false,
26
+ // 球列表(每球独立位置、速度、大小)
27
+ balls: [],
28
+ // 中奖结果展示
29
+ resultVisible: false,
30
+ resultEmoji: '',
31
+ resultLabel: '',
32
+ },
33
+
34
+ lifetimes: {
35
+ attached() {
36
+ this._initBalls();
37
+ // 与转盘/老虎机逻辑一致:先判断 running,再判断缓存恢复;不再因 autoplay 自动启动
38
+ if (this.data.running) {
39
+ this.spin();
40
+ } else if (this._hasSpinCache()) {
41
+ // 刷新前抽奖仍在进行(有缓存),进入页面后自动恢复摇晃动画
42
+ this.spin();
43
+ }
44
+ },
45
+ detached() {
46
+ this._clearTimers();
47
+ },
48
+ },
49
+
50
+ methods: {
51
+ // 初始化 12 颗球(不同颜色、表情、初始位置)
52
+ _initBalls() {
53
+ const colors = ['#ff9a3c', '#ff6b35', '#ffd93d', '#ff4757', '#ff7f50', '#ffb347'];
54
+ const emojis = ['😊', '😄', '🎉', '🤩', '😋', '🥳', '😆', '🤗', '✨', '🎁', '💫', '⭐'];
55
+ const labels = ['谢谢参与', '参与奖励', '一等奖', '二等奖', '三等奖', '参与奖励'];
56
+ const balls = [];
57
+ const n = 12;
58
+ const xMin = 22;
59
+ const xMax = 78;
60
+ const bowlR = 44; // 碗内半径(%),与 _animFrame 碰撞半径一致
61
+ const ballR = 7; // 球碰撞半径(%),与 _animFrame 一致
62
+ for (let i = 0; i < n; i++) {
63
+ // 均匀铺在碗底:x 从 xMin 到 xMax,y 落在碗底内壁上(静止、零速度)
64
+ const x = xMin + ((xMax - xMin) * i) / (n - 1);
65
+ const dx = x - 50;
66
+ const y = 50 + Math.sqrt(Math.max(0, bowlR * bowlR - dx * dx)) - ballR;
67
+ balls.push({
68
+ id: i,
69
+ color: colors[i % colors.length],
70
+ emoji: emojis[i % emojis.length],
71
+ label: labels[i % labels.length],
72
+ // 初始化时全部静止沉在碗底,未开始动画不运动
73
+ x,
74
+ y,
75
+ size: 66, // 统一大小(rpx),稍大
76
+ vx: 0,
77
+ vy: 0,
78
+ rotation: 0,
79
+ scale: 1, // 统一缩放
80
+ opacity: 1,
81
+ zIndex: i,
82
+ });
83
+ }
84
+ this.setData({ balls });
85
+ },
86
+
87
+ // 开始摇晃动画
88
+ spin() {
89
+ // 进行中或减速停止阶段,忽略重复点击,避免动画重复执行 / 重复上报抽奖
90
+ if (this.data.spinning || this.data.stopping) return;
91
+ this._clearTimers();
92
+ // 阶段状态机:tumble=翻滚,settle=落底;循环往复模拟摇奖机
93
+ this._phase = 'tumble';
94
+ this._phaseStart = Date.now();
95
+ this._tumbleMs = 1300; // 一次翻滚时长(约两圈)
96
+ this._settleMs = 1000; // 落底静置时长
97
+ this.setData({
98
+ spinning: true,
99
+ stopping: false,
100
+ stopped: false,
101
+ resultVisible: false,
102
+ });
103
+ // 记录缓存:抽奖进行中,刷新后可据此恢复动画
104
+ this._setSpinCache();
105
+
106
+ // 起步给一个抖动爆发力,把球从瓶底掀起来翻滚
107
+ this._applyShake();
108
+ // 物理弹跳模拟
109
+ this._animFrame();
110
+ this.triggerEvent('draw');
111
+ },
112
+
113
+ // 每帧更新球位置(物理碰撞 + 持续滚动 / 停止落定)
114
+ _animFrame() {
115
+ if (!this.data.spinning || this.data.stopped) return;
116
+ const stopping = this.data.stopping;
117
+
118
+ // 阶段循环:翻滚(tumble) → 落底(settle) → 抖动爆发力 → 翻滚 ...
119
+ if (!stopping) {
120
+ const now = Date.now();
121
+ const phaseDur = this._phase === 'tumble' ? this._tumbleMs : this._settleMs;
122
+ if (now - this._phaseStart >= phaseDur) {
123
+ if (this._phase === 'tumble') {
124
+ // 翻滚结束 → 进入落底阶段(小球沉到瓶底)
125
+ this._phase = 'settle';
126
+ this._phaseStart = now;
127
+ } else {
128
+ // 落底结束 → 施加抖动爆发力,重新翻滚
129
+ this._phase = 'tumble';
130
+ this._phaseStart = now;
131
+ this._applyShake();
132
+ }
133
+ }
134
+ }
135
+
136
+ const phase = stopping ? 'settle' : this._phase;
137
+ const damp = phase === 'settle' ? 0.92 : 0.99; // 落底时衰减更快,便于沉底
138
+ const bounce = 0.6;
139
+ const minSpeed = phase === 'tumble' ? 3.0 : 0; // 仅翻滚期维持速度(贴壁匀速转)
140
+ const maxSpeed = 6;
141
+ const ballR = 7; // 球碰撞半径(%)
142
+
143
+ const updated = this.data.balls.map((ball) => {
144
+ let { x, y, vx, vy, rotation } = ball;
145
+
146
+ if (phase === 'tumble') {
147
+ // 以瓶心为基准的径向 / 切向(圆形玻璃球,圆心 50,50)
148
+ const dx = x - 50;
149
+ const dy = y - 50;
150
+ const tlen = Math.sqrt(dx * dx + dy * dy) || 1;
151
+ const tx = -dy; // 切向(绕圈方向)
152
+ const ty = dx;
153
+ // 切向力:让球顺着瓶壁环绕翻滚(加大,转动更明显)
154
+ vx += (tx / tlen) * 0.42;
155
+ vy += (ty / tlen) * 0.42;
156
+ // 径向离心力:把球向外推,贴住玻璃瓶壁(加大,明确贴壁)
157
+ vx += (dx / tlen) * 0.3;
158
+ vy += (dy / tlen) * 0.3;
159
+ // 轻随机扰动:仅制造细微错落,不破坏贴壁轨道
160
+ vx += (Math.random() - 0.5) * 0.12;
161
+ vy += (Math.random() - 0.5) * 0.12;
162
+ // 速度维持区间 [minSpeed, maxSpeed]:保证翻滚期始终贴壁在动
163
+ let sp = Math.sqrt(vx * vx + vy * vy);
164
+ if (sp < minSpeed) {
165
+ const k = minSpeed / (sp || 0.001);
166
+ vx *= k;
167
+ vy *= k;
168
+ } else if (sp > maxSpeed) {
169
+ const k = maxSpeed / sp;
170
+ vx *= k;
171
+ vy *= k;
172
+ }
173
+ } else {
174
+ // 落底阶段:重力 + 阻尼,小球自然沉到瓶底
175
+ vy += 0.22;
176
+ }
177
+ vx *= damp;
178
+ vy *= damp;
179
+
180
+ // 更新位置
181
+ x += vx;
182
+ y += vy;
183
+ rotation += vx * 4;
184
+
185
+ // 圆形碗壁碰撞(圆方程:(x-50)^2 + (y-50)^2 = R^2)
186
+ const nx = (x - 50) / 44; // 半径 ~44%
187
+ const ny = (y - 50) / 44;
188
+ const dist = nx * nx + ny * ny;
189
+
190
+ if (dist > 1) {
191
+ // 碰到碗壁,反射
192
+ const normalX = nx / Math.sqrt(dist);
193
+ const normalY = ny / Math.sqrt(dist);
194
+ const dot = vx * normalX + vy * normalY;
195
+ vx = (vx - 2 * dot * normalX) * bounce;
196
+ vy = (vy - 2 * dot * normalY) * bounce;
197
+ // 推回碗内
198
+ x = 50 + normalX * 43.5;
199
+ y = 50 + normalY * 43.5;
200
+ }
201
+
202
+ return { ...ball, x, y, vx, vy, rotation };
203
+ });
204
+
205
+ // 球与球弹性碰撞:彼此撞击、翻滚、填充整个瓶身
206
+ for (let i = 0; i < updated.length; i++) {
207
+ for (let j = i + 1; j < updated.length; j++) {
208
+ const a = updated[i];
209
+ const b = updated[j];
210
+ let dx = b.x - a.x;
211
+ let dy = b.y - a.y;
212
+ let d = Math.sqrt(dx * dx + dy * dy);
213
+ const minD = ballR * 2;
214
+ if (d < minD && d > 0.0001) {
215
+ const nX = dx / d;
216
+ const nY = dy / d;
217
+ const overlap = (minD - d) / 2;
218
+ // 分离重叠
219
+ a.x -= nX * overlap;
220
+ a.y -= nY * overlap;
221
+ b.x += nX * overlap;
222
+ b.y += nY * overlap;
223
+ // 沿法向交换动量(弹性碰撞)
224
+ const rvx = b.vx - a.vx;
225
+ const rvy = b.vy - a.vy;
226
+ const velN = rvx * nX + rvy * nY;
227
+ if (velN < 0) {
228
+ const rest = 0.85;
229
+ const imp = (-(1 + rest) * velN) / 2;
230
+ a.vx -= imp * nX;
231
+ a.vy -= imp * nY;
232
+ b.vx += imp * nX;
233
+ b.vy += imp * nY;
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ this.setData({ balls: updated });
240
+
241
+ if (stopping || phase === 'settle') {
242
+ // stop() 触发 → 等待小球全部沉底静止再开奖
243
+ const totalV = updated.reduce((s, b) => s + Math.abs(b.vx) + Math.abs(b.vy), 0);
244
+ if (stopping && totalV < 0.8) {
245
+ this.setData({ stopped: true, spinning: false, stopping: false });
246
+ this._onStopComplete();
247
+ return;
248
+ }
249
+ }
250
+
251
+ // 小程序无 requestAnimationFrame,用 setTimeout 递归维持动画循环(~20ms/帧)
252
+ this._frameReq = setTimeout(() => this._animFrame(), 20);
253
+ },
254
+
255
+ // 抖动瞬间:给所有球一个向上的爆发力,从瓶底重新掀起来翻滚
256
+ _applyShake() {
257
+ const balls = this.data.balls.map((b) => ({
258
+ ...b,
259
+ vx: (Math.random() - 0.5) * 9,
260
+ vy: -(3 + Math.random() * 5),
261
+ }));
262
+ this.setData({ balls });
263
+ },
264
+
265
+ // 外部调用停止(LOTTERY_END 触发)
266
+ stop(targetIndex) {
267
+ if (!this.data.spinning || this.data.stopping || this.data.stopped) return false;
268
+ // 抽奖已结束(服务端已返回结果),清除进行中缓存
269
+ this._clearSpinCache();
270
+ this.setData({ stopping: true });
271
+ // targetIndex 用于确定中奖球(0~11 映射到奖品),默认随机选一颗
272
+ this._targetBallIndex = targetIndex !== undefined ? targetIndex : Math.floor(Math.random() * 12);
273
+
274
+ // 让目标球高亮突出
275
+ setTimeout(() => {
276
+ const balls = this.data.balls.map((b, i) => {
277
+ if (i === this._targetBallIndex) {
278
+ return { ...b, scale: 1.4, opacity: 1, zIndex: 20 };
279
+ }
280
+ return { ...b, scale: 0.6, opacity: 0.4 };
281
+ });
282
+ this.setData({ balls });
283
+ }, 800);
284
+
285
+ return true;
286
+ },
287
+
288
+ _onStopComplete() {
289
+ // 抽奖真正结束(开奖结果展示),确保进行中缓存被删除(双保险,stop 中已删一次)
290
+ this._clearSpinCache();
291
+ // 展示中奖结果
292
+ const idx = this._targetBallIndex ?? 0;
293
+ const ball = this.data.balls[idx] || this.data.balls[0];
294
+ this.setData({
295
+ resultVisible: true,
296
+ resultEmoji: ball.emoji,
297
+ resultLabel: ball.label,
298
+ });
299
+ this.triggerEvent('stopped', { detail: { index: idx, label: ball.label } });
300
+ },
301
+
302
+ _clearTimers() {
303
+ if (this._frameReq) {
304
+ clearTimeout(this._frameReq);
305
+ this._frameReq = null;
306
+ }
307
+ },
308
+
309
+ // 缓存 key:lottery_<lotteryId>_cache,标记"该抽奖仍在摇晃/进行中"
310
+ _cacheKey() {
311
+ return `lottery_${this.data.lotteryId}_cache`;
312
+ },
313
+ _hasSpinCache() {
314
+ return !!wx.getStorageSync(this._cacheKey());
315
+ },
316
+ _setSpinCache() {
317
+ wx.setStorageSync(this._cacheKey(), true);
318
+ },
319
+ _clearSpinCache() {
320
+ wx.removeStorageSync(this._cacheKey());
321
+ },
322
+
323
+ onDrawBtnTap() {
324
+ if (this.data.spinning) return;
325
+ this.spin();
326
+ },
327
+ },
328
+ });
@@ -0,0 +1 @@
1
+ {"component": true, "usingComponents": {}}
@@ -0,0 +1,52 @@
1
+ <view class="shake-machine">
2
+ <!-- 透明碗 + 弹跳球 -->
3
+ <view class="shake-bowl-area">
4
+ <!-- 碗体(透明玻璃效果) -->
5
+ <view class="shake-bowl">
6
+ <!-- 球 -->
7
+ <view wx:for="{{balls}}" wx:key="id" class="shake-ball" style="left: {{item.x}}%; top: {{item.y}}%; width: {{item.size}}rpx; height: {{item.size}}rpx; background: {{item.color}}; transform: translate(-50%, -50%) rotate({{item.rotation}}deg) scale({{item.scale}}); opacity: {{item.opacity}}; z-index: {{item.zIndex}}; {{stopping && item.zIndex==20 ? 'box-shadow: 0 0 30rpx rgba(255,200,0,0.9), 0 0 60rpx rgba(255,100,0,0.5);' : ''}}">
8
+ <text class="ball-emoji">{{item.emoji}}</text>
9
+ </view>
10
+ <!-- 碗口高光 -->
11
+ <view class="bowl-rim"></view>
12
+ <!-- 碗身反光 -->
13
+ <view class="bowl-highlight"></view>
14
+ </view>
15
+ <!-- 碗底座 -->
16
+ <view class="shake-base">
17
+ <view class="base-body"></view>
18
+ <view class="base-ring"></view>
19
+ <!-- 底座顶部标签:常显,文字跑马灯 -->
20
+ <view class="shake-banner">
21
+ <view class="shake-banner-bg">
22
+ <text class="shake-banner-text">{{remark || '抽奖进行中...'}}</text>
23
+ </view>
24
+ </view>
25
+ </view>
26
+ </view>
27
+ <!-- 底部控制区 -->
28
+ <view class="shake-control-area">
29
+ <!-- 奖品预览 -->
30
+ <view class="shake-prize-preview">
31
+ <view wx:if="{{resultVisible}}" class="prize-showcase">
32
+ <text class="prize-big-emoji">{{resultEmoji}}</text>
33
+ <text class="prize-name-text">{{resultLabel}}</text>
34
+ </view>
35
+ <view wx:else class="prize-placeholder">
36
+ <view class="placeholder-icon-wrap">
37
+ <text class="placeholder-icon">🎁</text>
38
+ </view>
39
+ </view>
40
+ </view>
41
+ <!-- 抽奖按钮 -->
42
+ <view class="shake-btn-wrap">
43
+ <view class="shake-draw-btn {{spinning?'shaking':''}} {{stopped?'btn-done':''}}" bindtap="onDrawBtnTap">
44
+ <text class="draw-btn-text">{{stopped ? '开奖完成' : (spinning ? '摇奖中...' : '点我抽奖')}}</text>
45
+ </view>
46
+ </view>
47
+ </view>
48
+ <!-- 装饰光点 -->
49
+ <view class="sparkle sparkle-1"></view>
50
+ <view class="sparkle sparkle-2"></view>
51
+ <view class="sparkle sparkle-3"></view>
52
+ </view>
@@ -0,0 +1 @@
1
+ .shake-machine{align-items:center;display:flex;flex-direction:column;padding:0 20rpx 20rpx}.shake-banner{background:#f5c842;box-shadow:0 4rpx 16rpx rgba(230,168,50,.45),inset 0 2rpx 4rpx hsla(0,0%,100%,.3);left:50%;padding:12rpx 20rpx;position:absolute;top:10rpx;transform:translateX(-50%);width:220rpx;z-index:6}.shake-banner,.shake-banner-bg{border-radius:20rpx;overflow:hidden}.shake-banner-bg{align-items:center;background:#7a6bfd;display:flex;height:32rpx;white-space:nowrap}.shake-banner-text{animation:shake-marquee 3s linear infinite;color:#fff;display:inline-block;font-size:22rpx;font-weight:700;letter-spacing:2rpx;line-height:1;padding-left:100%;text-shadow:0 1rpx 3rpx rgba(0,0,0,.25);white-space:nowrap}@keyframes shake-marquee{0%{transform:translateX(0)}to{transform:translateX(-100%)}}.shake-bowl-area{align-items:center;display:flex;flex-direction:column;height:560rpx;margin-top:10rpx;position:relative;width:520rpx}.shake-bowl{background:radial-gradient(circle at 32% 28%,hsla(0,0%,100%,.22) 0,rgba(200,230,255,.1) 38%,rgba(100,150,220,.07) 70%,rgba(60,100,180,.12) 100%);border:4rpx solid #fff;border-radius:50%;box-shadow:inset 0 -30rpx 60rpx rgba(0,0,0,.1),inset 0 10rpx 30rpx hsla(0,0%,100%,.15),0 8rpx 32rpx rgba(0,0,0,.15);height:440rpx;overflow:hidden;position:relative;width:440rpx;z-index:3}.bowl-rim{background:linear-gradient(90deg,transparent,hsla(0,0%,100%,.45) 30%,hsla(0,0%,100%,.6) 50%,hsla(0,0%,100%,.45) 70%,transparent);border-radius:50%;-webkit-filter:blur(1rpx);filter:blur(1rpx);height:24rpx;left:8%;position:absolute;top:-2rpx;width:84%}.bowl-highlight{background:linear-gradient(180deg,hsla(0,0%,100%,.3),hsla(0,0%,100%,.08) 50%,transparent);border-radius:120rpx;height:240rpx;left:6%;position:absolute;top:18%;transform:rotate(-8deg);width:80rpx}.shake-ball{align-items:center;border-radius:50%;box-shadow:inset -6rpx -6rpx 16rpx rgba(0,0,0,.25),inset 4rpx 4rpx 12rpx hsla(0,0%,100%,.4),0 4rpx 12rpx rgba(0,0,0,.2);display:flex;justify-content:center;position:absolute;transition:opacity .5s ease}.ball-emoji{-webkit-filter:drop-shadow(0 1rpx 1rpx rgba(0,0,0,.2));filter:drop-shadow(0 1rpx 1rpx rgba(0,0,0,.2));font-size:36rpx;line-height:1}.shake-base{border-radius:28rpx 28rpx 48rpx 48rpx;height:120rpx;margin-top:-50rpx;position:relative;width:460rpx;z-index:4}.base-body{background:linear-gradient(180deg,#8b6afc,#6a4fed 40%,#5538d4 70%,#4229b8);border-radius:0 0 48rpx 48rpx;box-shadow:0 10rpx 30rpx rgba(66,41,184,.45),0 4rpx 12rpx rgba(66,41,184,.3),inset 0 4rpx 14rpx hsla(0,0%,100%,.15);height:100%;width:100%}.base-ring{background:linear-gradient(90deg,#a78bff,#d4bfff,#a78bff);border-radius:11rpx;bottom:0;height:22rpx;left:50%;opacity:.65;position:absolute;transform:translateX(-50%);width:340rpx}.shake-control-area{align-items:center;background:linear-gradient(145deg,#7a5bfd,#5e45dd 40%,#4a35c2 80%,#3d29a6);border-radius:36rpx;box-shadow:0 10rpx 36rpx rgba(61,41,166,.45),inset 0 2rpx 10rpx hsla(0,0%,100%,.12);display:flex;justify-content:space-between;margin-top:-48rpx;max-width:520rpx;padding:20rpx 30rpx;width:100%}.shake-prize-preview{align-items:center;display:flex;flex:1;justify-content:center;min-height:100rpx}.prize-showcase{align-items:center;animation:prizePopIn .5s cubic-bezier(.175,.885,.32,1.275) both;display:flex;flex-direction:column}.prize-big-emoji{font-size:52rpx;line-height:1.2}.prize-name-text{color:#ffd93d;font-size:24rpx;font-weight:700;margin-top:4rpx;text-shadow:0 1rpx 4rpx rgba(0,0,0,.3)}.placeholder-icon-wrap,.prize-placeholder{align-items:center;display:flex;justify-content:center}.placeholder-icon-wrap{background:linear-gradient(145deg,rgba(0,0,0,.25),rgba(0,0,0,.4));border-radius:50%;box-shadow:inset 0 2rpx 6rpx hsla(0,0%,100%,.08),0 2rpx 8rpx rgba(0,0,0,.2);height:96rpx;width:96rpx}.placeholder-icon{font-size:52rpx;line-height:1}@keyframes prizePopIn{0%{opacity:0;transform:scale(0)}to{opacity:1;transform:scale(1)}}.shake-btn-wrap{flex-shrink:0;margin-left:20rpx}.shake-draw-btn{background:linear-gradient(135deg,#ffd93d,#ffb830 40%,#ff9a2e);border:none;border-radius:48rpx;box-shadow:0 6rpx 20rpx rgba(255,154,46,.55),inset 0 2rpx 5rpx hsla(0,0%,100%,.45),inset 0 -2rpx 4rpx rgba(200,120,0,.15);padding:20rpx 36rpx}.draw-btn-text{color:#8b4513;font-size:26rpx;font-weight:700;letter-spacing:1rpx;text-shadow:0 1rpx 1rpx hsla(0,0%,100%,.4);white-space:nowrap}.shake-draw-btn:not(.btn-done):not(.shaking){animation:shakePulse 1.5s ease-in-out infinite}@keyframes shakePulse{0%,to{transform:scale(1)}50%{transform:scale(1.06)}}.shake-draw-btn.shaking{animation:btnShake .3s ease-in-out infinite}@keyframes btnShake{0%,to{transform:rotate(0deg)}25%{transform:rotate(-4deg) scale(1.02)}75%{transform:rotate(4deg) scale(1.02)}}.shake-draw-btn.btn-done{background:linear-gradient(135deg,#4ade80,#22c55e);box-shadow:0 4rpx 16rpx rgba(34,197,94,.4)}.shake-draw-btn.btn-done .draw-btn-text{color:#fff}.sparkle{animation:sparkleTwinkle 1.5s ease-in-out infinite alternate;background:radial-gradient(circle,#fff 0,#ffd93d 60%,transparent 100%);border-radius:50%;height:12rpx;position:absolute;width:12rpx}.sparkle-1{animation-delay:0s;left:30rpx;top:60rpx}.sparkle-2{animation-delay:.5s;height:16rpx;right:40rpx;top:100rpx;width:16rpx}.sparkle-3{animation-delay:1s;height:10rpx;left:10rpx;top:280rpx;width:10rpx}@keyframes sparkleTwinkle{0%{opacity:.3;transform:scale(.8)}to{opacity:1;transform:scale(1.3)}}
@@ -0,0 +1,230 @@
1
+ Component({
2
+ properties: {
3
+ type: {
4
+ type: String,
5
+ value: '2',
6
+ },
7
+ // 抽奖 ID:用于区分不同抽奖的缓存 key
8
+ lotteryId: {
9
+ type: String,
10
+ value: '',
11
+ },
12
+ remark: {
13
+ type: String,
14
+ value: '抽奖进行中...',
15
+ },
16
+ autoplay: {
17
+ type: Boolean,
18
+ value: false,
19
+ },
20
+ loop: {
21
+ type: Boolean,
22
+ value: false,
23
+ },
24
+ // 外部控制:true 开始转,false 停止
25
+ running: {
26
+ type: Boolean,
27
+ value: false,
28
+ observer(val) {
29
+ if (val) {
30
+ this.spin();
31
+ } else {
32
+ this.stop();
33
+ }
34
+ },
35
+ },
36
+ },
37
+
38
+ data: {
39
+ spinning: false,
40
+ stopping: false,
41
+ // 单个滚轴的奖品列表(基础 6 项,每列仅 1 个"谢谢参与",其余为礼品图标+恭喜中奖)
42
+ basePrizes: [
43
+ { emoji: '😊', label: '谢谢参与' },
44
+ { emoji: '🎁', label: '恭喜中奖' },
45
+ { emoji: '🎁', label: '恭喜中奖' },
46
+ { emoji: '🎁', label: '恭喜中奖' },
47
+ { emoji: '🎁', label: '恭喜中奖' },
48
+ { emoji: '🎁', label: '恭喜中奖' },
49
+ ],
50
+ // 滚轴实际渲染列表:基础列表重复多份,保证连续滚动不露底
51
+ reelList: [],
52
+ // 每个滚轴当前位移(rpx),由 JS 连续驱动
53
+ // 初始让 item0 落在中间某一份上居中(55 - 一份周期804 = -749),使上下各露出相邻方块
54
+ offset1: -749,
55
+ offset2: -749,
56
+ offset3: -749,
57
+ // 停止状态:每个轴是否已停
58
+ stopped1: true,
59
+ stopped2: true,
60
+ stopped3: true,
61
+ },
62
+
63
+ lifetimes: {
64
+ attached() {
65
+ // 基础 6 项重复 12 份 = 72 项(9936rpx),覆盖停止落定时的最大位移(约 6624rpx)+窗口高(240),不露底
66
+ const repeated = [];
67
+ for (let i = 0; i < 12; i++) {
68
+ this.data.basePrizes.forEach((p) => repeated.push(p));
69
+ }
70
+ this.setData({ reelList: repeated });
71
+ if (this.data.running) {
72
+ this.spin();
73
+ } else if (this._hasSpinCache()) {
74
+ // 刷新前抽奖仍在进行(有缓存),进入页面后自动恢复滚动动画
75
+ this.spin();
76
+ }
77
+ },
78
+ detached() {
79
+ this._clearTimers();
80
+ },
81
+ },
82
+
83
+ methods: {
84
+ _clearTimers() {
85
+ if (this._spinTimer) {
86
+ clearInterval(this._spinTimer);
87
+ this._spinTimer = null;
88
+ }
89
+ [1, 2, 3].forEach((i) => {
90
+ if (this['_stopTimer' + i]) {
91
+ clearTimeout(this['_stopTimer' + i]);
92
+ this['_stopTimer' + i] = null;
93
+ }
94
+ });
95
+ },
96
+
97
+ // 缓存 key:lottery_<lotteryId>_cache,标记"该抽奖仍在滚动/进行中"
98
+ _cacheKey() {
99
+ return `lottery_${this.data.lotteryId}_cache`;
100
+ },
101
+ _hasSpinCache() {
102
+ return !!wx.getStorageSync(this._cacheKey());
103
+ },
104
+ _setSpinCache() {
105
+ wx.setStorageSync(this._cacheKey(), true);
106
+ },
107
+ _clearSpinCache() {
108
+ wx.removeStorageSync(this._cacheKey());
109
+ },
110
+
111
+ // 单格高度与循环周期(与 less 中 .slot-item 高度一致)
112
+ _itemH() {
113
+ // 每项垂直占用(含 .slot-reel 的 gap 4rpx):130 + 4 = 134,用于循环/步进计算
114
+ return 134;
115
+ },
116
+ _itemBoxH() {
117
+ // 单项真实高度(与 less 中 .slot-item height 一致,不含 gap)
118
+ return 130;
119
+ },
120
+ _cycle() {
121
+ return this.data.basePrizes.length * this._itemH(); // 6 * 168 = 1008
122
+ },
123
+
124
+ // 开始连续滚动:位移持续递减,显示值对周期取模(内容已重复,取模无缝)
125
+ spin() {
126
+ // 进行中或减速停止阶段,忽略重复点击,避免动画重复执行 / 重复上报抽奖
127
+ if (this.data.spinning || this.data.stopping) return;
128
+ this._clearTimers();
129
+ this.setData({
130
+ spinning: true,
131
+ stopping: false,
132
+ stopped1: false,
133
+ stopped2: false,
134
+ stopped3: false,
135
+ offset1: -749,
136
+ offset2: -749,
137
+ offset3: -749,
138
+ });
139
+ // 记录缓存:抽奖进行中,刷新后可据此恢复动画
140
+ this._setSpinCache();
141
+ const cycle = this._cycle();
142
+ // 三个滚轴各自独立的位置与速度(速度刻意不同,更有老虎机层次感)
143
+ // _pos 初始与 offset=-749 同余,使滚动首帧与 idle 视觉连续、避免点击瞬间跳变
144
+ this._pos = [-749, -749, -749];
145
+ this._step = [18, 13, 23]; // 每帧位移(rpx):左中右速度各异(调慢,原 [30,22,38])
146
+ this._spinTimer = setInterval(() => {
147
+ const next = {};
148
+ for (let i = 0; i < 3; i++) {
149
+ this._pos[i] -= this._step[i];
150
+ // 对周期取模并归一到 (-cycle, 0],配合重复内容实现无缝循环
151
+ const shown = ((this._pos[i] % cycle) + cycle) % cycle;
152
+ next['offset' + (i + 1)] = shown === 0 ? 0 : -shown;
153
+ }
154
+ this.setData(next);
155
+ }, 30);
156
+
157
+ this.triggerEvent('draw');
158
+ },
159
+
160
+ // 把某轴平滑停止到目标项 T(0~5),加减速过渡;返回最终位移
161
+ _landReel(num, currentOffset, targetT, extraCycles) {
162
+ const itemBoxH = this._itemBoxH(); // 单项真实高度 130
163
+ const step = this._itemH(); // 含间距步进 138
164
+ const cycle = this._cycle();
165
+ const windowCenter = 120; // .slot-reel-wrap 高度 240 / 2
166
+ // 使目标项 T 的中心对齐窗口中心(120):滚动量 = T*step - (窗口中心 - 半高)
167
+ const sT = targetT * step - (windowCenter - itemBoxH / 2);
168
+ const curAbs = -currentOffset; // 当前已滚动量
169
+ const targetMod = ((sT % cycle) + cycle) % cycle; // 目标模周期
170
+ // 在当前滚动量基础上再推进 extraCycles 整圈后对齐到目标项
171
+ const delta = (((targetMod - (curAbs % cycle)) % cycle) + cycle) % cycle;
172
+ // + cycle:额外推进一整份,使目标项落在中间某一份(而非头部份),
173
+ // 保证停止后上下都露出相邻方块,与初始/idle 状态一致
174
+ const finalAbs = curAbs + extraCycles * cycle + delta + cycle;
175
+ const finalOffset = -finalAbs;
176
+ this.setData({ ['offset' + num]: finalOffset, ['stopped' + num]: true });
177
+ return finalOffset;
178
+ },
179
+
180
+ // 停止滚动;won=true 表示中奖(三轴都停在"恭喜中奖"),否则三轴都停在"谢谢参与"
181
+ stop(won) {
182
+ if (!this.data.spinning || this.data.stopping) return false;
183
+ this._clearTimers();
184
+ // 抽奖已结束(服务端已返回结果),清除进行中缓存
185
+ this._clearSpinCache();
186
+ this.setData({ stopping: true });
187
+
188
+ // 根据中奖结果决定三轴落定目标:
189
+ // - 未中奖(won 非 true):三个"谢谢参与" → 基础列表 index 0
190
+ // - 中奖(won === true):三个"恭喜中奖" → 基础列表 index 1~5(取同一个,保证整齐一致)
191
+ let r1, r2, r3;
192
+ if (won === true) {
193
+ const winIdx = 1 + Math.floor(Math.random() * 5); // 1~5 均为"恭喜中奖"
194
+ r1 = r2 = r3 = winIdx;
195
+ } else {
196
+ r1 = r2 = r3 = 0; // index 0 为"谢谢参与"
197
+ }
198
+
199
+ // 依次停止(左→右),制造老虎机逐列停下的效果
200
+ // 注:过渡时长由 less 中 .slot-reel.stopped 控制
201
+ this._stopTimer1 = setTimeout(() => {
202
+ this._landReel(1, this.data.offset1, r1, 5);
203
+ }, 0);
204
+ this._stopTimer2 = setTimeout(() => {
205
+ this._landReel(2, this.data.offset2, r2, 5);
206
+ }, 500);
207
+ this._stopTimer3 = setTimeout(() => {
208
+ this._landReel(3, this.data.offset3, r3, 5);
209
+ setTimeout(() => {
210
+ this.setData({ spinning: false, stopping: false });
211
+ this.triggerEvent('stopped', { resultIndex: [r1, r2, r3], won: won === true });
212
+ }, 1400); // 等最后一条轴过渡完
213
+ }, 1000);
214
+
215
+ return true;
216
+ },
217
+
218
+ play() {
219
+ this.spin();
220
+ },
221
+
222
+ pause() {
223
+ this.stop();
224
+ },
225
+
226
+ onDrawTap() {
227
+ this.spin();
228
+ },
229
+ },
230
+ });
@@ -0,0 +1 @@
1
+ {"component": true, "usingComponents": {}}