@openyida/yidacli 0.1.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,833 @@
1
+ // ============================================================
2
+ // 状态管理
3
+ // ============================================================
4
+
5
+ const _customState = {
6
+ // 游戏阶段: 'welcome' | 'playing' | 'wishing' | 'celebrating'
7
+ gameStage: 'welcome',
8
+ // 寿星姓名
9
+ birthdayPersonName: '',
10
+ // 送祝福人姓名
11
+ senderName: '',
12
+ // 祝福语
13
+ blessingMessage: '',
14
+ // 蜡烛数量 (5-10随机)
15
+ totalCandles: 0,
16
+ // 已熄灭的蜡烛索引数组
17
+ extinguishedCandles: [],
18
+ // 倒计时剩余秒数
19
+ countdownSeconds: 15,
20
+ // 游戏是否超时
21
+ isTimeout: false,
22
+ // 彩带动画触发器
23
+ confettiTrigger: 0,
24
+ // 麦克风权限状态
25
+ micPermission: 'prompt', // 'prompt' | 'granted' | 'denied'
26
+ // 音频分析器
27
+ audioAnalyser: null,
28
+ // 音频上下文
29
+ audioContext: null,
30
+ // 麦克风流
31
+ micStream: null,
32
+ // 检测吹气的定时器
33
+ blowDetectionInterval: null,
34
+ };
35
+
36
+ /**
37
+ * 获取状态
38
+ */
39
+ export function getCustomState(key) {
40
+ if (key) {
41
+ return _customState[key];
42
+ }
43
+ return { ..._customState };
44
+ }
45
+
46
+ /**
47
+ * 设置状态(合并更新,自动触发重新渲染)
48
+ */
49
+ export function setCustomState(newState) {
50
+ Object.keys(newState).forEach(function(key) {
51
+ _customState[key] = newState[key];
52
+ });
53
+ this.forceUpdate();
54
+ }
55
+
56
+ /**
57
+ * 强制重新渲染
58
+ */
59
+ export function forceUpdate() {
60
+ this.setState({ timestamp: new Date().getTime() });
61
+ }
62
+
63
+ // ============================================================
64
+ // 生命周期
65
+ // ============================================================
66
+
67
+ export function didMount() {
68
+ // 初始化随机蜡烛数量
69
+ var randomCandles = Math.floor(Math.random() * 6) + 5; // 5-10
70
+ _customState.totalCandles = randomCandles;
71
+ this.forceUpdate();
72
+ }
73
+
74
+ export function didUnmount() {
75
+ // 清理定时器
76
+ this.stopCountdown();
77
+ this.stopBlowDetection();
78
+ // 停止麦克风
79
+ if (_customState.micStream) {
80
+ _customState.micStream.getTracks().forEach(function(track) {
81
+ track.stop();
82
+ });
83
+ _customState.micStream = null;
84
+ }
85
+ if (_customState.audioContext) {
86
+ _customState.audioContext.close();
87
+ _customState.audioContext = null;
88
+ }
89
+ }
90
+
91
+ // ============================================================
92
+ // 游戏逻辑方法
93
+ // ============================================================
94
+
95
+ /**
96
+ * 开始游戏
97
+ */
98
+ export function startGame() {
99
+ var nameInput = document.getElementById('birthday-person-input');
100
+ var senderInput = document.getElementById('sender-name-input');
101
+
102
+ var name = nameInput ? nameInput.value.trim() : '';
103
+ var sender = senderInput ? senderInput.value.trim() : '';
104
+
105
+ if (!name) {
106
+ this.utils.toast({ title: '请输入寿星姓名', type: 'error' });
107
+ return;
108
+ }
109
+
110
+ _customState.birthdayPersonName = name;
111
+ _customState.senderName = sender || window.loginUser.userName || '神秘好友';
112
+ _customState.gameStage = 'playing';
113
+ _customState.extinguishedCandles = [];
114
+ _customState.countdownSeconds = 15;
115
+ _customState.isTimeout = false;
116
+
117
+ this.setCustomState({
118
+ gameStage: 'playing',
119
+ birthdayPersonName: name,
120
+ senderName: _customState.senderName,
121
+ extinguishedCandles: [],
122
+ countdownSeconds: 15,
123
+ isTimeout: false
124
+ });
125
+
126
+ // 启动倒计时
127
+ this.startCountdown();
128
+ // 请求麦克风权限并开始检测吹气
129
+ this.requestMicrophone();
130
+ }
131
+
132
+ /**
133
+ * 启动倒计时
134
+ */
135
+ export function startCountdown() {
136
+ this.stopCountdown();
137
+ _customState.countdownTimer = setInterval(function() {
138
+ var current = _customState.countdownSeconds - 1;
139
+ _customState.countdownSeconds = current;
140
+
141
+ if (current <= 0) {
142
+ this.handleTimeout();
143
+ } else {
144
+ this.forceUpdate();
145
+ }
146
+ }.bind(this), 1000);
147
+ }
148
+
149
+ /**
150
+ * 停止倒计时
151
+ */
152
+ export function stopCountdown() {
153
+ if (_customState.countdownTimer) {
154
+ clearInterval(_customState.countdownTimer);
155
+ _customState.countdownTimer = null;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * 处理超时
161
+ */
162
+ export function handleTimeout() {
163
+ this.stopCountdown();
164
+ _customState.isTimeout = true;
165
+ this.setCustomState({ isTimeout: true });
166
+ }
167
+
168
+ /**
169
+ * 重试游戏
170
+ */
171
+ export function retryGame() {
172
+ _customState.extinguishedCandles = [];
173
+ _customState.countdownSeconds = 15;
174
+ _customState.isTimeout = false;
175
+ _customState.totalCandles = Math.floor(Math.random() * 6) + 5;
176
+
177
+ this.setCustomState({
178
+ extinguishedCandles: [],
179
+ countdownSeconds: 15,
180
+ isTimeout: false,
181
+ totalCandles: _customState.totalCandles
182
+ });
183
+
184
+ this.startCountdown();
185
+ }
186
+
187
+ /**
188
+ * 熄灭蜡烛(通过点击或吹气)
189
+ */
190
+ export function extinguishCandle(candleIndex) {
191
+ var alreadyExtinguished = _customState.extinguishedCandles.indexOf(candleIndex) !== -1;
192
+
193
+ if (alreadyExtinguished || _customState.isTimeout) {
194
+ return;
195
+ }
196
+
197
+ var newExtinguished = _customState.extinguishedCandles.concat([candleIndex]);
198
+ _customState.extinguishedCandles = newExtinguished;
199
+
200
+ // 检查是否全部熄灭
201
+ if (newExtinguished.length >= _customState.totalCandles) {
202
+ this.stopCountdown();
203
+ this.stopBlowDetection();
204
+ setTimeout(function() {
205
+ _customState.gameStage = 'wishing';
206
+ this.setCustomState({ gameStage: 'wishing' });
207
+ }.bind(this), 500);
208
+ } else {
209
+ this.forceUpdate();
210
+ }
211
+ }
212
+
213
+ /**
214
+ * 请求麦克风权限
215
+ */
216
+ export function requestMicrophone() {
217
+ var self = this;
218
+ navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) {
219
+ _customState.micStream = stream;
220
+ _customState.micPermission = 'granted';
221
+
222
+ // 创建音频上下文和分析器
223
+ var audioContext = new (window.AudioContext || window.webkitAudioContext)();
224
+ var analyser = audioContext.createAnalyser();
225
+ var microphone = audioContext.createMediaStreamSource(stream);
226
+
227
+ analyser.fftSize = 256;
228
+ microphone.connect(analyser);
229
+
230
+ _customState.audioContext = audioContext;
231
+ _customState.audioAnalyser = analyser;
232
+
233
+ // 开始检测吹气
234
+ self.startBlowDetection();
235
+ }).catch(function(err) {
236
+ console.log('麦克风权限被拒绝:', err);
237
+ _customState.micPermission = 'denied';
238
+ // 即使麦克风失败,用户仍可以点击蜡烛
239
+ });
240
+ }
241
+
242
+ /**
243
+ * 开始检测吹气
244
+ */
245
+ export function startBlowDetection() {
246
+ this.stopBlowDetection();
247
+ var dataArray = new Uint8Array(_customState.audioAnalyser.frequencyBinCount);
248
+ var lastBlowTime = 0;
249
+ var blowCooldown = 800; // 吹气冷却时间(ms)
250
+
251
+ _customState.blowDetectionInterval = setInterval(function() {
252
+ if (!_customState.audioAnalyser || _customState.gameStage !== 'playing') {
253
+ return;
254
+ }
255
+
256
+ _customState.audioAnalyser.getByteFrequencyData(dataArray);
257
+
258
+ // 计算平均音量
259
+ var sum = 0;
260
+ for (var i = 0; i < dataArray.length; i++) {
261
+ sum += dataArray[i];
262
+ }
263
+ var average = sum / dataArray.length;
264
+
265
+ // 检测吹气(音量阈值设为30)
266
+ var now = Date.now();
267
+ if (average > 30 && now - lastBlowTime > blowCooldown) {
268
+ lastBlowTime = now;
269
+
270
+ // 找到第一个未熄灭的蜡烛并熄灭它
271
+ for (var j = 0; j < _customState.totalCandles; j++) {
272
+ if (_customState.extinguishedCandles.indexOf(j) === -1) {
273
+ this.extinguishCandle(j);
274
+ break;
275
+ }
276
+ }
277
+ }
278
+ }.bind(this), 100);
279
+ }
280
+
281
+ /**
282
+ * 停止检测吹气
283
+ */
284
+ export function stopBlowDetection() {
285
+ if (_customState.blowDetectionInterval) {
286
+ clearInterval(_customState.blowDetectionInterval);
287
+ _customState.blowDetectionInterval = null;
288
+ }
289
+ }
290
+
291
+ /**
292
+ * 提交祝福
293
+ */
294
+ export function submitBlessing() {
295
+ var blessingInput = document.getElementById('blessing-input');
296
+ var message = blessingInput ? blessingInput.value.trim() : '';
297
+
298
+ if (!message) {
299
+ this.utils.toast({ title: '请输入祝福语', type: 'error' });
300
+ return;
301
+ }
302
+
303
+ _customState.blessingMessage = message;
304
+ _customState.gameStage = 'celebrating';
305
+ _customState.confettiTrigger = Date.now();
306
+
307
+ this.setCustomState({
308
+ blessingMessage: message,
309
+ gameStage: 'celebrating',
310
+ confettiTrigger: Date.now()
311
+ });
312
+ }
313
+
314
+ /**
315
+ * 重新开始
316
+ */
317
+ export function restartGame() {
318
+ _customState.gameStage = 'welcome';
319
+ _customState.birthdayPersonName = '';
320
+ _customState.senderName = '';
321
+ _customState.blessingMessage = '';
322
+ _customState.extinguishedCandles = [];
323
+ _customState.countdownSeconds = 15;
324
+ _customState.isTimeout = false;
325
+ _customState.totalCandles = Math.floor(Math.random() * 6) + 5;
326
+
327
+ // 清空输入框
328
+ var nameInput = document.getElementById('birthday-person-input');
329
+ var senderInput = document.getElementById('sender-name-input');
330
+ var blessingInput = document.getElementById('blessing-input');
331
+
332
+ if (nameInput) nameInput.value = '';
333
+ if (senderInput) senderInput.value = '';
334
+ if (blessingInput) blessingInput.value = '';
335
+
336
+ this.setCustomState({
337
+ gameStage: 'welcome',
338
+ birthdayPersonName: '',
339
+ senderName: '',
340
+ blessingMessage: '',
341
+ extinguishedCandles: [],
342
+ countdownSeconds: 15,
343
+ isTimeout: false,
344
+ totalCandles: _customState.totalCandles
345
+ });
346
+ }
347
+
348
+ // ============================================================
349
+ // 样式(提取到模块顶层,供各渲染函数共享)
350
+ // ============================================================
351
+
352
+ var GAME_STYLES = {
353
+ container: {
354
+ minHeight: '100vh',
355
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
356
+ display: 'flex',
357
+ flexDirection: 'column',
358
+ alignItems: 'center',
359
+ justifyContent: 'center',
360
+ padding: '20px',
361
+ fontFamily: '"PingFang SC", "Microsoft YaHei", sans-serif',
362
+ position: 'relative',
363
+ overflow: 'hidden'
364
+ },
365
+ card: {
366
+ background: 'rgba(255, 255, 255, 0.95)',
367
+ borderRadius: '24px',
368
+ padding: '40px',
369
+ maxWidth: '480px',
370
+ width: '100%',
371
+ boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
372
+ textAlign: 'center',
373
+ position: 'relative',
374
+ zIndex: 10
375
+ },
376
+ title: {
377
+ fontSize: '32px',
378
+ fontWeight: 'bold',
379
+ color: '#764ba2',
380
+ marginBottom: '10px',
381
+ display: 'flex',
382
+ alignItems: 'center',
383
+ justifyContent: 'center',
384
+ gap: '10px'
385
+ },
386
+ subtitle: {
387
+ fontSize: '16px',
388
+ color: '#666',
389
+ marginBottom: '30px'
390
+ },
391
+ input: {
392
+ width: '100%',
393
+ padding: '15px 20px',
394
+ fontSize: '16px',
395
+ border: '2px solid #e0e0e0',
396
+ borderRadius: '12px',
397
+ marginBottom: '20px',
398
+ outline: 'none',
399
+ transition: 'border-color 0.3s',
400
+ boxSizing: 'border-box'
401
+ },
402
+ button: {
403
+ width: '100%',
404
+ padding: '16px 32px',
405
+ fontSize: '18px',
406
+ fontWeight: 'bold',
407
+ color: '#fff',
408
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
409
+ border: 'none',
410
+ borderRadius: '12px',
411
+ cursor: 'pointer',
412
+ transition: 'transform 0.2s, box-shadow 0.2s',
413
+ marginTop: '10px'
414
+ },
415
+ cakeContainer: {
416
+ position: 'relative',
417
+ margin: '30px 0'
418
+ },
419
+ cake: {
420
+ fontSize: '120px',
421
+ lineHeight: '1'
422
+ },
423
+ candlesRow: {
424
+ position: 'absolute',
425
+ top: '-20px',
426
+ left: '50%',
427
+ transform: 'translateX(-50%)',
428
+ display: 'flex',
429
+ gap: '8px',
430
+ justifyContent: 'center'
431
+ },
432
+ candle: {
433
+ fontSize: '28px',
434
+ cursor: 'pointer',
435
+ transition: 'transform 0.2s, opacity 0.3s',
436
+ userSelect: 'none'
437
+ },
438
+ candleLit: {
439
+ animation: 'flicker 0.5s infinite alternate'
440
+ },
441
+ candleExtinguished: {
442
+ opacity: 0.5,
443
+ transform: 'scale(0.9)'
444
+ },
445
+ countdown: {
446
+ fontSize: '48px',
447
+ fontWeight: 'bold',
448
+ color: '#e74c3c',
449
+ margin: '20px 0'
450
+ },
451
+ progressBar: {
452
+ width: '100%',
453
+ height: '8px',
454
+ background: '#e0e0e0',
455
+ borderRadius: '4px',
456
+ overflow: 'hidden',
457
+ marginBottom: '20px'
458
+ },
459
+ progressFill: {
460
+ height: '100%',
461
+ background: 'linear-gradient(90deg, #667eea, #764ba2)',
462
+ transition: 'width 0.3s ease'
463
+ },
464
+ textarea: {
465
+ width: '100%',
466
+ padding: '15px 20px',
467
+ fontSize: '16px',
468
+ border: '2px solid #e0e0e0',
469
+ borderRadius: '12px',
470
+ minHeight: '100px',
471
+ resize: 'vertical',
472
+ outline: 'none',
473
+ boxSizing: 'border-box',
474
+ fontFamily: 'inherit'
475
+ },
476
+ blessingCard: {
477
+ background: 'linear-gradient(135deg, #ffeaa7 0%, #fab1a0 50%, #fd79a8 100%)',
478
+ borderRadius: '20px',
479
+ padding: '40px',
480
+ color: '#fff',
481
+ textShadow: '0 2px 4px rgba(0,0,0,0.2)'
482
+ },
483
+ blessingTitle: {
484
+ fontSize: '36px',
485
+ fontWeight: 'bold',
486
+ marginBottom: '20px'
487
+ },
488
+ blessingText: {
489
+ fontSize: '24px',
490
+ lineHeight: '1.6',
491
+ marginBottom: '30px',
492
+ fontStyle: 'italic'
493
+ },
494
+ blessingFrom: {
495
+ fontSize: '16px',
496
+ opacity: 0.9
497
+ },
498
+ confetti: {
499
+ position: 'fixed',
500
+ top: 0,
501
+ left: 0,
502
+ width: '100%',
503
+ height: '100%',
504
+ pointerEvents: 'none',
505
+ zIndex: 100
506
+ },
507
+ micHint: {
508
+ fontSize: '14px',
509
+ color: '#888',
510
+ marginTop: '15px',
511
+ display: 'flex',
512
+ alignItems: 'center',
513
+ justifyContent: 'center',
514
+ gap: '6px'
515
+ },
516
+ timeoutOverlay: {
517
+ position: 'absolute',
518
+ top: 0,
519
+ left: 0,
520
+ right: 0,
521
+ bottom: 0,
522
+ background: 'rgba(0,0,0,0.7)',
523
+ borderRadius: '24px',
524
+ display: 'flex',
525
+ flexDirection: 'column',
526
+ alignItems: 'center',
527
+ justifyContent: 'center',
528
+ color: '#fff',
529
+ zIndex: 20
530
+ },
531
+ star: {
532
+ position: 'absolute',
533
+ color: '#ffd700',
534
+ fontSize: '20px',
535
+ animation: 'twinkle 1s infinite'
536
+ }
537
+ };
538
+
539
+ // ============================================================
540
+ // 子渲染函数(export function,this 正确绑定)
541
+ // ============================================================
542
+
543
+ /**
544
+ * 渲染蜡烛列表
545
+ */
546
+ export function renderCandles() {
547
+ var state = this.getCustomState();
548
+ var candles = [];
549
+ for (var i = 0; i < state.totalCandles; i++) {
550
+ // 用立即执行函数捕获循环变量 i,避免闭包陷阱
551
+ candles.push((function(candleIndex) {
552
+ var isExtinguished = state.extinguishedCandles.indexOf(candleIndex) !== -1;
553
+ return (
554
+ <span
555
+ key={candleIndex}
556
+ style={Object.assign({}, GAME_STYLES.candle, isExtinguished ? GAME_STYLES.candleExtinguished : GAME_STYLES.candleLit)}
557
+ onClick={(e) => { this.extinguishCandle(candleIndex); }}
558
+ >
559
+ {isExtinguished ? '💨' : '🕯️'}
560
+ </span>
561
+ );
562
+ }).call(this, i));
563
+ }
564
+ return candles;
565
+ }
566
+
567
+ /**
568
+ * 渲染星星装饰(庆祝阶段)
569
+ */
570
+ export function renderStars() {
571
+ var state = this.getCustomState();
572
+ if (state.gameStage !== 'celebrating') return null;
573
+ var stars = [];
574
+ for (var i = 0; i < 20; i++) {
575
+ var starLeft = Math.random() * 100;
576
+ var starTop = Math.random() * 100;
577
+ var starDelay = Math.random() * 2;
578
+ stars.push(
579
+ <span
580
+ key={i}
581
+ style={Object.assign({}, GAME_STYLES.star, {
582
+ left: starLeft + '%',
583
+ top: starTop + '%',
584
+ animationDelay: starDelay + 's'
585
+ })}
586
+ >
587
+
588
+ </span>
589
+ );
590
+ }
591
+ return stars;
592
+ }
593
+
594
+ /**
595
+ * 渲染欢迎界面
596
+ */
597
+ export function renderWelcome() {
598
+ return (
599
+ <div style={GAME_STYLES.card}>
600
+ <div style={GAME_STYLES.title}>
601
+ 🎂 生日祝福
602
+ </div>
603
+ <div style={GAME_STYLES.subtitle}>
604
+ 为TA点燃蜡烛,送上最真挚的祝福
605
+ </div>
606
+
607
+ <input
608
+ id="birthday-person-input"
609
+ type="text"
610
+ placeholder="请输入寿星姓名"
611
+ style={GAME_STYLES.input}
612
+ defaultValue=""
613
+ />
614
+
615
+ <input
616
+ id="sender-name-input"
617
+ type="text"
618
+ placeholder="请输入您的姓名(选填)"
619
+ style={GAME_STYLES.input}
620
+ defaultValue=""
621
+ />
622
+
623
+ <button
624
+ style={GAME_STYLES.button}
625
+ onMouseEnter={function(e) {
626
+ e.target.style.transform = 'translateY(-2px)';
627
+ e.target.style.boxShadow = '0 8px 20px rgba(102, 126, 234, 0.4)';
628
+ }}
629
+ onMouseLeave={function(e) {
630
+ e.target.style.transform = 'translateY(0)';
631
+ e.target.style.boxShadow = 'none';
632
+ }}
633
+ onClick={(e) => { this.startGame(); }}
634
+ >
635
+ 🎉 开始游戏
636
+ </button>
637
+
638
+ <div style={GAME_STYLES.micHint}>
639
+ 🎤 支持吹灭蜡烛哦~
640
+ </div>
641
+ </div>
642
+ );
643
+ }
644
+
645
+ /**
646
+ * 渲染游戏界面
647
+ */
648
+ export function renderPlaying() {
649
+ var state = this.getCustomState();
650
+ var progressPercent = (state.extinguishedCandles.length / state.totalCandles) * 100;
651
+
652
+ return (
653
+ <div style={GAME_STYLES.card}>
654
+ <div style={GAME_STYLES.title}>
655
+ 🎂 点蜡烛
656
+ </div>
657
+
658
+ <div style={GAME_STYLES.progressBar}>
659
+ <div style={Object.assign({}, GAME_STYLES.progressFill, { width: progressPercent + '%' })}></div>
660
+ </div>
661
+
662
+ <div style={GAME_STYLES.countdown}>
663
+ {state.countdownSeconds}s
664
+ </div>
665
+
666
+ <div style={GAME_STYLES.cakeContainer}>
667
+ <div style={GAME_STYLES.candlesRow}>
668
+ {this.renderCandles()}
669
+ </div>
670
+ <div style={GAME_STYLES.cake}>🎂</div>
671
+ </div>
672
+
673
+ <div style={GAME_STYLES.subtitle}>
674
+ {state.micPermission === 'denied'
675
+ ? '👆 点击蜡烛将其熄灭'
676
+ : '吹气或点击蜡烛'}
677
+ </div>
678
+
679
+ <div style={GAME_STYLES.subtitle}>
680
+ 已熄灭 {state.extinguishedCandles.length} / {state.totalCandles} 根蜡烛
681
+ </div>
682
+
683
+ {state.isTimeout && (
684
+ <div style={GAME_STYLES.timeoutOverlay}>
685
+ <div style={{ fontSize: '48px', marginBottom: '20px' }}>⏰</div>
686
+ <div style={{ fontSize: '24px', marginBottom: '20px' }}>时间到!</div>
687
+ <button
688
+ style={Object.assign({}, GAME_STYLES.button, { width: 'auto', padding: '12px 30px' })}
689
+ onClick={(e) => { this.retryGame(); }}
690
+ >
691
+ 🔄 再试一次
692
+ </button>
693
+ </div>
694
+ )}
695
+ </div>
696
+ );
697
+ }
698
+
699
+ /**
700
+ * 渲染许愿界面
701
+ */
702
+ export function renderWishing() {
703
+ var state = this.getCustomState();
704
+
705
+ return (
706
+ <div style={GAME_STYLES.card}>
707
+ <div style={GAME_STYLES.title}>
708
+ ⭐ 许愿时刻
709
+ </div>
710
+
711
+ <div style={{ fontSize: '64px', marginBottom: '20px' }}>✨</div>
712
+
713
+ <div style={GAME_STYLES.subtitle}>
714
+ 所有蜡烛已熄灭!请为 {state.birthdayPersonName} 送上祝福
715
+ </div>
716
+
717
+ <textarea
718
+ id="blessing-input"
719
+ placeholder="写下你的生日祝福..."
720
+ style={GAME_STYLES.textarea}
721
+ defaultValue=""
722
+ ></textarea>
723
+
724
+ <button
725
+ style={GAME_STYLES.button}
726
+ onClick={(e) => { this.submitBlessing(); }}
727
+ >
728
+ 💝 送出祝福
729
+ </button>
730
+ </div>
731
+ );
732
+ }
733
+
734
+ /**
735
+ * 渲染庆祝界面
736
+ */
737
+ export function renderCelebrating() {
738
+ var state = this.getCustomState();
739
+
740
+ return (
741
+ <div style={Object.assign({}, GAME_STYLES.card, GAME_STYLES.blessingCard)}>
742
+ {this.renderStars()}
743
+
744
+ <div style={GAME_STYLES.blessingTitle}>
745
+ 🎉 生日快乐
746
+ </div>
747
+
748
+ <div style={{ fontSize: '72px', marginBottom: '20px' }}>
749
+ 🎂
750
+ </div>
751
+
752
+ <div style={{ fontSize: '28px', marginBottom: '10px' }}>
753
+ 亲爱的 {state.birthdayPersonName}
754
+ </div>
755
+
756
+ <div style={GAME_STYLES.blessingText}>
757
+ "{state.blessingMessage}"
758
+ </div>
759
+
760
+ <div style={GAME_STYLES.blessingFrom}>
761
+ —— {state.senderName} 敬上
762
+ </div>
763
+
764
+ <button
765
+ style={Object.assign({}, GAME_STYLES.button, {
766
+ marginTop: '30px',
767
+ background: 'rgba(255,255,255,0.3)',
768
+ backdropFilter: 'blur(10px)'
769
+ })}
770
+ onClick={(e) => { this.restartGame(); }}
771
+ >
772
+ 🎮 再玩一次
773
+ </button>
774
+ </div>
775
+ );
776
+ }
777
+
778
+ // ============================================================
779
+ // 渲染
780
+ // ============================================================
781
+
782
+ export function renderJsx() {
783
+ var state = this.getCustomState();
784
+ var timestamp = this.state.timestamp;
785
+
786
+ var currentStage;
787
+ switch (state.gameStage) {
788
+ case 'playing':
789
+ currentStage = this.renderPlaying();
790
+ break;
791
+ case 'wishing':
792
+ currentStage = this.renderWishing();
793
+ break;
794
+ case 'celebrating':
795
+ currentStage = this.renderCelebrating();
796
+ break;
797
+ default:
798
+ currentStage = this.renderWelcome();
799
+ }
800
+
801
+ return (
802
+ <div style={GAME_STYLES.container}>
803
+ {/* 用于触发重新渲染 */}
804
+ <div style={{ display: 'none' }}>{timestamp}</div>
805
+
806
+ {/* CSS 动画 */}
807
+ <style dangerouslySetInnerHTML={{__html: `
808
+ @keyframes flicker {
809
+ 0% { transform: scale(1) rotate(-2deg); }
810
+ 100% { transform: scale(1.1) rotate(2deg); }
811
+ }
812
+ @keyframes twinkle {
813
+ 0%, 100% { opacity: 1; transform: scale(1); }
814
+ 50% { opacity: 0.5; transform: scale(0.8); }
815
+ }
816
+ @keyframes fall {
817
+ 0% { transform: translateY(-100vh) rotate(0deg); opacity: 1; }
818
+ 100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
819
+ }
820
+ `}} />
821
+
822
+ {/* 彩带效果 */}
823
+ {state.gameStage === 'celebrating' && (
824
+ <canvas
825
+ id="confetti-canvas"
826
+ style={GAME_STYLES.confetti}
827
+ />
828
+ )}
829
+
830
+ {currentStage}
831
+ </div>
832
+ );
833
+ }