guanwei 1.1.3 → 1.1.4

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,736 +0,0 @@
1
- import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
2
- import { useNavigate } from 'react-router-dom';
3
- import { Sparkles, Shuffle, Edit3, RotateCcw, Search, Library, X, Play, Pause, SkipForward, SkipBack } from 'lucide-react';
4
- import type { QuestionCategory, Spread, DrawnCard } from '@/types';
5
- import { defaultSpreads } from '@core/data/spreads';
6
- import { tarotCards } from '@core/data/tarotCards';
7
- import { generateComboResult } from '@/utils/comboEngine';
8
-
9
- import { getCustomSpreads } from '@/utils/spreadStorage';
10
- import { saveRecord } from '@/utils/storage';
11
- import { api } from '@/services/api';
12
- import { useDivinationStore } from '@/stores/useDivinationStore';
13
- import TarotCard from '@/components/TarotCard';
14
- import SpreadLibrary from './SpreadLibrary';
15
-
16
- const categories: { value: QuestionCategory; label: string }[] = [
17
- { value: 'love', label: '爱情' },
18
- { value: 'career', label: '事业' },
19
- { value: 'wealth', label: '财运' },
20
- { value: 'health', label: '健康' },
21
- { value: 'study', label: '学业' },
22
- { value: 'family', label: '家庭' },
23
- { value: 'spiritual', label: '灵性' },
24
- { value: 'general', label: '综合' },
25
- ];
26
-
27
- const comboMethods = [
28
- { value: 'xiaoliuren', label: '小六壬' },
29
- { value: 'liuyao', label: '六爻' },
30
- { value: 'meihua', label: '梅花易数' },
31
- { value: 'daliuren', label: '大六壬' },
32
- { value: 'qimen', label: '奇门遁甲' },
33
- ];
34
-
35
- const SUIT_TABS = [
36
- { value: 'all', label: '全部' },
37
- { value: 'major', label: '大阿卡纳' },
38
- { value: 'wands', label: '权杖' },
39
- { value: 'cups', label: '圣杯' },
40
- { value: 'swords', label: '宝剑' },
41
- { value: 'pentacles', label: '星币' },
42
- ];
43
-
44
- function TypewriterText({ text, speed = 50, onComplete }: { text: string; speed?: number; onComplete?: () => void }) {
45
- const [displayText, setDisplayText] = useState('');
46
- const [isComplete, setIsComplete] = useState(false);
47
- const indexRef = useRef(0);
48
-
49
- useEffect(() => {
50
- setDisplayText('');
51
- setIsComplete(false);
52
- indexRef.current = 0;
53
-
54
- const timer = setInterval(() => {
55
- if (indexRef.current < text.length) {
56
- setDisplayText(text.slice(0, indexRef.current + 1));
57
- indexRef.current++;
58
- } else {
59
- clearInterval(timer);
60
- setIsComplete(true);
61
- onComplete?.();
62
- }
63
- }, speed);
64
-
65
- return () => clearInterval(timer);
66
- }, [text, speed, onComplete]);
67
-
68
- return (
69
- <span className={isComplete ? '' : 'border-r border-ancient/50'}>
70
- {displayText}
71
- </span>
72
- );
73
- }
74
-
75
- export default function TarotPage() {
76
- const navigate = useNavigate();
77
- const { setCurrentReading, setComboResult, isComboEnabled, setIsComboEnabled } = useDivinationStore();
78
-
79
- const [question, setQuestion] = useState('');
80
- const [category, setCategory] = useState<QuestionCategory>('general');
81
- const [allSpreads, setAllSpreads] = useState<Spread[]>(defaultSpreads);
82
- const [selectedSpread, setSelectedSpread] = useState<Spread>(defaultSpreads[0]);
83
- const [drawnCards, setDrawnCards] = useState<DrawnCard[]>([]);
84
- const [isDrawing, setIsDrawing] = useState(false);
85
- const [isInterpreting, setIsInterpreting] = useState(false);
86
- const [showResult, setShowResult] = useState(false);
87
- const [interpretation, setInterpretation] = useState<any>(null);
88
- const [selectedComboMethod, setSelectedComboMethod] = useState('xiaoliuren');
89
- const [comboResultLocal, setComboResultLocal] = useState<any>(null);
90
- const [manualMode, setManualMode] = useState(false);
91
- const [manualCards, setManualCards] = useState<{ cardId: number; isReversed: boolean }[]>([]);
92
- const [showLibrary, setShowLibrary] = useState(false);
93
- const [cardSearch, setCardSearch] = useState('');
94
- const [activeSuit, setActiveSuit] = useState('all');
95
-
96
- const [currentSectionIndex, setCurrentSectionIndex] = useState(0);
97
- const [isPlaying, setIsPlaying] = useState(false);
98
- const [displaySpeed, setDisplaySpeed] = useState(50);
99
- const [completedSections, setCompletedSections] = useState<number[]>([]);
100
-
101
- useEffect(() => {
102
- setAllSpreads([...defaultSpreads, ...getCustomSpreads()]);
103
- }, [showLibrary]);
104
-
105
- const filteredCards = useMemo(() => {
106
- let cards = tarotCards;
107
- if (activeSuit === 'major') {
108
- cards = cards.filter(c => c.arcana === 'major');
109
- } else if (activeSuit !== 'all') {
110
- cards = cards.filter(c => c.suit === activeSuit);
111
- }
112
- if (cardSearch) {
113
- const q = cardSearch.toLowerCase();
114
- cards = cards.filter(c => c.name.toLowerCase().includes(q) || c.nameEn.toLowerCase().includes(q) || c.keywords.some(k => k.includes(q)));
115
- }
116
- return cards;
117
- }, [activeSuit, cardSearch]);
118
-
119
- useEffect(() => {
120
- if (isPlaying && interpretation && currentSectionIndex < interpretation.sections.length) {
121
- const section = interpretation.sections[currentSectionIndex];
122
- const duration = (section.content.length * displaySpeed) + 2000;
123
-
124
- const timer = setTimeout(() => {
125
- setCompletedSections(prev => [...prev, currentSectionIndex]);
126
- if (currentSectionIndex < interpretation.sections.length - 1) {
127
- setTimeout(() => {
128
- setCurrentSectionIndex(prev => prev + 1);
129
- }, 1000);
130
- } else {
131
- setIsPlaying(false);
132
- }
133
- }, duration);
134
-
135
- return () => clearTimeout(timer);
136
- }
137
- }, [isPlaying, currentSectionIndex, interpretation, displaySpeed]);
138
-
139
- const handleInterpret = useCallback(async (drawMode: 'random' | 'manual') => {
140
- if (!question.trim()) return;
141
- if (drawMode === 'manual' && manualCards.length !== selectedSpread.positions.length) return;
142
-
143
- setIsDrawing(true);
144
- setIsInterpreting(false);
145
- setShowResult(false);
146
- setComboResultLocal(null);
147
- setCurrentSectionIndex(0);
148
- setCompletedSections([]);
149
- setIsPlaying(false);
150
-
151
- setTimeout(async () => {
152
- setIsDrawing(false);
153
- setIsInterpreting(true);
154
-
155
- let cards: DrawnCard[];
156
- try {
157
- if (drawMode === 'random') {
158
- const drawResult = await api.tarot.draw(selectedSpread.id, selectedSpread.isCustom ? selectedSpread : undefined);
159
- cards = drawResult.cards;
160
- } else {
161
- cards = manualCards.map((mc, index) => ({
162
- cardId: mc.cardId,
163
- isReversed: mc.isReversed,
164
- positionId: selectedSpread.positions[index].id,
165
- }));
166
- }
167
- setDrawnCards(cards);
168
-
169
- const result = await api.tarot.interpret(cards, selectedSpread, question, category);
170
- setInterpretation(result);
171
- setShowResult(true);
172
- setIsInterpreting(false);
173
-
174
- const reading = {
175
- question,
176
- category,
177
- spreadId: selectedSpread.id,
178
- cards,
179
- interpretation: result.fullText,
180
- };
181
- setCurrentReading(reading);
182
-
183
- saveRecord({
184
- id: Date.now().toString(),
185
- type: 'tarot',
186
- question,
187
- category,
188
- spreadId: selectedSpread.id,
189
- cards,
190
- interpretation: result.fullText,
191
- timestamp: Date.now(),
192
- });
193
- } catch (error) {
194
- console.error('API 调用失败:', error);
195
- setIsInterpreting(false);
196
- }
197
- }, 1500);
198
- }, [question, manualCards, selectedSpread, category, setCurrentReading]);
199
-
200
- const handleCombo = useCallback(() => {
201
- const result = generateComboResult(selectedComboMethod);
202
- setComboResultLocal(result);
203
- setComboResult(result);
204
- }, [selectedComboMethod, setComboResult]);
205
-
206
- const reset = () => {
207
- setQuestion('');
208
- setDrawnCards([]);
209
- setShowResult(false);
210
- setInterpretation(null);
211
- setComboResultLocal(null);
212
- setIsComboEnabled(false);
213
- setManualMode(false);
214
- setManualCards([]);
215
- setCurrentSectionIndex(0);
216
- setCompletedSections([]);
217
- setIsPlaying(false);
218
- };
219
-
220
- const toggleManualCard = (cardId: number) => {
221
- if (manualCards.length >= selectedSpread.positions.length) {
222
- setManualCards([...manualCards.slice(0, -1), { cardId, isReversed: false }]);
223
- } else {
224
- setManualCards([...manualCards, { cardId, isReversed: false }]);
225
- }
226
- };
227
-
228
- const toggleManualReversed = (idx: number) => {
229
- setManualCards(manualCards.map((c, i) => i === idx ? { ...c, isReversed: !c.isReversed } : c));
230
- };
231
-
232
- const removeManualCard = (idx: number) => {
233
- setManualCards(manualCards.filter((_, i) => i !== idx));
234
- };
235
-
236
- const handleSelectSpread = (spread: Spread) => {
237
- setSelectedSpread(spread);
238
- setShowLibrary(false);
239
- setManualCards([]);
240
- };
241
-
242
- const goToSection = (index: number) => {
243
- setCurrentSectionIndex(index);
244
- setIsPlaying(false);
245
- };
246
-
247
- const renderInterpretationSection = (section: any, index: number) => {
248
- const isCurrent = index === currentSectionIndex;
249
- const isCompleted = completedSections.includes(index);
250
- const isActive = isCurrent || isCompleted;
251
-
252
- return (
253
- <div
254
- key={section.title}
255
- className={`mb-4 transition-all duration-500 ${
256
- isActive ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4 pointer-events-none'
257
- }`}
258
- >
259
- <div className="flex items-center gap-3 mb-3">
260
- <div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold ${
261
- isCurrent ? 'bg-ancient text-void animate-pulse' :
262
- isCompleted ? 'bg-ancient/20 text-ancient' : 'bg-abyss text-stardust/30'
263
- }`}>
264
- {index + 1}
265
- </div>
266
- <h3 className={`font-display text-sm tracking-wider ${
267
- isCurrent ? 'text-ancient' : 'text-ancient/60'
268
- }`}>{section.title}</h3>
269
- {isCurrent && !isPlaying && (
270
- <button
271
- onClick={() => setIsPlaying(true)}
272
- className="ml-auto flex items-center gap-1 text-xs text-ancient/60 hover:text-ancient transition-colors"
273
- >
274
- <Play className="w-3 h-3" />
275
- 继续
276
- </button>
277
- )}
278
- </div>
279
- <div className="ml-11 text-stardust/70 text-sm leading-relaxed space-y-3 bg-abyss/20 rounded-lg p-4">
280
- {section.content.split('\n\n').map((block: string, i: number) => (
281
- <div key={i} className="animate-fade-in">
282
- {isCurrent && !isCompleted ? (
283
- <TypewriterText
284
- text={block.replace(/\*\*/g, '')}
285
- speed={displaySpeed}
286
- />
287
- ) : (
288
- <p className={block.startsWith('**') ? 'text-ancient/80 font-medium' : ''}>
289
- {block.replace(/\*\*/g, '')}
290
- </p>
291
- )}
292
- </div>
293
- ))}
294
- </div>
295
- </div>
296
- );
297
- };
298
-
299
- if (showLibrary) {
300
- return (
301
- <div className="relative min-h-screen pt-20 pb-16 px-4">
302
- <div className="max-w-6xl mx-auto">
303
- <button
304
- onClick={() => setShowLibrary(false)}
305
- className="inline-flex items-center gap-2 text-stardust/60 hover:text-ancient text-sm mb-6"
306
- >
307
- <X className="w-4 h-4" />
308
- 关闭牌阵库
309
- </button>
310
- <SpreadLibrary onSelect={handleSelectSpread} />
311
- </div>
312
- </div>
313
- );
314
- }
315
-
316
- return (
317
- <div className="relative min-h-screen pt-20 pb-16 px-4">
318
- <div className="max-w-6xl mx-auto">
319
- <div className="text-center mb-12">
320
- <h1 className="font-display text-3xl md:text-4xl text-ancient tracking-wider mb-3">
321
- 塔罗占卜
322
- </h1>
323
- <p className="text-stardust/50 text-sm">向宇宙提出你的问题,让牌面揭示答案</p>
324
- </div>
325
-
326
- <div className="glass-panel p-6 md:p-8 mb-8">
327
- <div className="mb-4">
328
- <label className="block text-ancient/80 text-sm mb-2 tracking-wider">你的问题</label>
329
- <textarea
330
- value={question}
331
- onChange={(e) => setQuestion(e.target.value)}
332
- placeholder="在心中默念你的问题,然后输入..."
333
- className="w-full bg-void/50 border border-ancient/20 rounded-lg p-4 text-stardust placeholder-stardust/30 focus:border-ancient/60 focus:outline-none transition-colors resize-none h-24"
334
- />
335
- </div>
336
-
337
- <div className="mb-4">
338
- <label className="block text-ancient/80 text-sm mb-2 tracking-wider">问题分类</label>
339
- <div className="flex flex-wrap gap-2">
340
- {categories.map((cat) => (
341
- <button
342
- key={cat.value}
343
- onClick={() => setCategory(cat.value)}
344
- className={`px-3 py-1.5 rounded-full text-xs transition-all duration-300 ${
345
- category === cat.value
346
- ? 'bg-ancient/20 border border-ancient/50 text-ancient'
347
- : 'border border-stardust/20 text-stardust/60 hover:border-ancient/30 hover:text-ancient/80'
348
- }`}
349
- >
350
- {cat.label}
351
- </button>
352
- ))}
353
- </div>
354
- </div>
355
-
356
- <div className="flex flex-wrap gap-3">
357
- <button
358
- onClick={() => setManualMode(!manualMode)}
359
- className={`flex items-center gap-2 px-4 py-2 rounded-sm text-sm transition-all ${
360
- manualMode
361
- ? 'bg-mystic/20 border border-mystic/50 text-mystic'
362
- : 'border border-stardust/20 text-stardust/60 hover:border-ancient/30'
363
- }`}
364
- >
365
- <Edit3 className="w-4 h-4" />
366
- {manualMode ? '关闭手动选牌' : '手动选牌'}
367
- </button>
368
- <button
369
- onClick={reset}
370
- className="flex items-center gap-2 px-4 py-2 rounded-sm border border-stardust/20 text-stardust/60 hover:border-ancient/30 hover:text-ancient/80 transition-all text-sm"
371
- >
372
- <RotateCcw className="w-4 h-4" />
373
- 重置
374
- </button>
375
- </div>
376
- </div>
377
-
378
- {!showResult && (
379
- <div className="mb-8">
380
- <div className="flex items-center justify-between mb-4">
381
- <h2 className="font-display text-lg text-ancient tracking-wider">选择牌阵</h2>
382
- <button
383
- onClick={() => setShowLibrary(true)}
384
- className="inline-flex items-center gap-2 px-3 py-1.5 border border-ancient/30 text-ancient/80 hover:border-ancient/60 hover:text-ancient text-xs rounded-sm transition-all"
385
- >
386
- <Library className="w-3 h-3" />
387
- 浏览牌阵库
388
- </button>
389
- </div>
390
- <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
391
- {allSpreads.map((spread) => {
392
- const isSelected = selectedSpread.id === spread.id;
393
- return (
394
- <button
395
- key={spread.id}
396
- onClick={() => {
397
- setSelectedSpread(spread);
398
- setManualCards([]);
399
- }}
400
- className={`relative p-2.5 pt-2 text-left rounded-md border h-24 flex flex-col transition-all duration-200 overflow-hidden ${
401
- isSelected
402
- ? 'border-ancient bg-ancient/10 shadow-[0_0_12px_rgba(201,162,39,0.25)]'
403
- : spread.isCustom
404
- ? 'border-mystic/30 hover:border-mystic/60 hover:bg-mystic/5'
405
- : 'border-stardust/15 hover:border-ancient/40 hover:bg-ancient/5'
406
- }`}
407
- >
408
- <span className={`absolute top-1.5 right-1.5 text-[10px] px-1.5 py-0.5 rounded border ${
409
- isSelected
410
- ? 'bg-ancient/20 border-ancient/40 text-ancient'
411
- : 'bg-abyss/60 border-stardust/10 text-stardust/60'
412
- }`}>
413
- {spread.positions.length}张
414
- </span>
415
- {spread.isCustom && (
416
- <span className="absolute top-1.5 left-1.5 text-[10px] text-mystic/80 leading-none">自定义</span>
417
- )}
418
- <h3 className={`font-display text-xs tracking-wide truncate pr-10 ${spread.isCustom ? 'mt-3.5' : 'mt-0'} ${
419
- isSelected ? 'text-ancient' : 'text-stardust'
420
- }`}>
421
- {spread.name}
422
- </h3>
423
- <p className={`mt-auto text-[11px] leading-tight line-clamp-1 ${
424
- isSelected ? 'text-ancient/60' : 'text-stardust/40'
425
- }`}>
426
- {spread.description}
427
- </p>
428
- </button>
429
- );
430
- })}
431
- </div>
432
- </div>
433
- )}
434
-
435
- {manualMode && !showResult && (
436
- <div className="glass-panel p-6 mb-8">
437
- <div className="flex flex-wrap items-center justify-between gap-4 mb-4">
438
- <h3 className="font-display text-ancient text-sm tracking-wider">
439
- 手动选牌 ({manualCards.length}/{selectedSpread.positions.length})
440
- </h3>
441
- <button
442
- onClick={() => {
443
- if (manualCards.length === selectedSpread.positions.length) {
444
- handleInterpret('manual');
445
- }
446
- }}
447
- disabled={manualCards.length !== selectedSpread.positions.length}
448
- className="px-4 py-1.5 bg-ancient/20 border border-ancient/50 text-ancient hover:bg-ancient/30 transition-all rounded-sm text-sm disabled:opacity-30"
449
- >
450
- {manualCards.length === selectedSpread.positions.length ? '确认并解读' : `选满${selectedSpread.positions.length}张`}
451
- </button>
452
- </div>
453
-
454
- {manualCards.length > 0 && (
455
- <div className="mb-4 p-3 bg-abyss/30 rounded border border-ancient/20">
456
- <div className="text-xs text-ancient/60 mb-2">已选牌位(点击切换正逆位,点击×删除)</div>
457
- <div className="flex flex-wrap gap-2">
458
- {manualCards.map((mc, idx) => {
459
- const card = tarotCards.find(c => c.id === mc.cardId);
460
- const pos = selectedSpread.positions[idx];
461
- return (
462
- <div key={idx} className="flex items-center gap-2 px-2 py-1 bg-abyss border border-ancient/30 rounded">
463
- <span className="text-xs text-ancient/60">{pos?.name || `#${idx+1}`}:</span>
464
- <button
465
- onClick={() => toggleManualReversed(idx)}
466
- className={`text-xs ${mc.isReversed ? 'text-crimson' : 'text-stardust'}`}
467
- >
468
- {card?.name}{mc.isReversed ? '(逆)' : ''}
469
- </button>
470
- <button onClick={() => removeManualCard(idx)} className="text-stardust/40 hover:text-crimson">
471
- <X className="w-3 h-3" />
472
- </button>
473
- </div>
474
- );
475
- })}
476
- </div>
477
- </div>
478
- )}
479
-
480
- <div className="flex flex-col sm:flex-row gap-2 mb-4">
481
- <div className="relative flex-1">
482
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-stardust/40" />
483
- <input
484
- type="text"
485
- value={cardSearch}
486
- onChange={(e) => setCardSearch(e.target.value)}
487
- placeholder="搜索牌名..."
488
- className="w-full bg-void/50 border border-ancient/20 rounded pl-10 pr-3 py-2 text-sm text-stardust focus:border-ancient/60 focus:outline-none"
489
- />
490
- </div>
491
- </div>
492
- <div className="flex flex-wrap gap-2 mb-4">
493
- {SUIT_TABS.map((tab) => (
494
- <button
495
- key={tab.value}
496
- onClick={() => setActiveSuit(tab.value)}
497
- className={`px-3 py-1 rounded-full text-xs transition-all ${
498
- activeSuit === tab.value
499
- ? 'bg-ancient/20 border border-ancient/50 text-ancient'
500
- : 'border border-stardust/20 text-stardust/60 hover:border-ancient/30'
501
- }`}
502
- >
503
- {tab.label}
504
- </button>
505
- ))}
506
- </div>
507
-
508
- <div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 lg:grid-cols-10 gap-2 max-h-96 overflow-y-auto">
509
- {filteredCards.map((card) => (
510
- <button
511
- key={card.id}
512
- onClick={() => toggleManualCard(card.id)}
513
- className={`p-2 rounded border text-xs transition-all hover:border-ancient/50 ${
514
- manualCards.some(mc => mc.cardId === card.id)
515
- ? 'border-ancient bg-ancient/20 text-ancient'
516
- : 'border-stardust/10 text-stardust/50'
517
- }`}
518
- >
519
- <div className="text-center">
520
- <div className="text-lg mb-1">
521
- {card.arcana === 'major' ? '☽' : card.suit === 'wands' ? '⚡' : card.suit === 'cups' ? '🏆' : card.suit === 'swords' ? '⚔️' : '💰'}
522
- </div>
523
- <div className="text-[10px] truncate">{card.name}</div>
524
- </div>
525
- </button>
526
- ))}
527
- </div>
528
- </div>
529
- )}
530
-
531
- {!manualMode && !showResult && (
532
- <div className="text-center mb-12">
533
- <button
534
- onClick={() => handleInterpret('random')}
535
- disabled={!question.trim() || isDrawing}
536
- className="inline-flex items-center gap-3 px-10 py-4 bg-ancient/20 border border-ancient/60 text-ancient hover:bg-ancient/30 hover:border-ancient transition-all duration-500 rounded-sm disabled:opacity-30 disabled:cursor-not-allowed tracking-wider"
537
- >
538
- {isDrawing ? (
539
- <>
540
- <Shuffle className="w-5 h-5 animate-spin" />
541
- 抽牌中...
542
- </>
543
- ) : isInterpreting ? (
544
- <>
545
- <Sparkles className="w-5 h-5 animate-pulse" />
546
- 解读中...
547
- </>
548
- ) : (
549
- <>
550
- <Sparkles className="w-5 h-5" />
551
- 抽牌并解读
552
- </>
553
- )}
554
- </button>
555
- </div>
556
- )}
557
-
558
- {drawnCards.length > 0 && (
559
- <div className="mb-12">
560
- <h2 className="font-display text-lg text-ancient tracking-wider mb-6 text-center">
561
- {selectedSpread.name}
562
- </h2>
563
- <div className="flex flex-wrap justify-center gap-4 md:gap-6">
564
- {drawnCards.map((drawn, index) => {
565
- const card = tarotCards.find((c) => c.id === drawn.cardId);
566
- if (!card) return null;
567
- return (
568
- <div key={index} className="flex flex-col items-center">
569
- <TarotCard
570
- card={card}
571
- isReversed={drawn.isReversed}
572
- isFlipped={showResult}
573
- size="md"
574
- />
575
- <span className="mt-3 text-xs text-stardust/50 text-center max-w-24">
576
- {selectedSpread.positions.find((p) => p.id === drawn.positionId)?.name}
577
- </span>
578
- </div>
579
- );
580
- })}
581
- </div>
582
- </div>
583
- )}
584
-
585
- {isInterpreting && !showResult && (
586
- <div className="glass-panel p-12 mb-8 text-center">
587
- <div className="inline-block animate-pulse">
588
- <Sparkles className="w-12 h-12 text-ancient/60 mx-auto mb-4" />
589
- </div>
590
- <p className="text-ancient/70 text-sm">正在解析问题能量...</p>
591
- <p className="text-stardust/40 text-xs mt-2">基于语义分析与牌面信息的整合解读</p>
592
- </div>
593
- )}
594
-
595
- {showResult && interpretation && (
596
- <div className="glass-panel p-6 md:p-8 mb-8">
597
- <div className="flex items-center justify-between mb-6 flex-wrap gap-2">
598
- <h2 className="font-display text-xl text-ancient tracking-wider">解读结果</h2>
599
- <label className="flex items-center gap-2 text-sm text-stardust/60 cursor-pointer">
600
- <input
601
- type="checkbox"
602
- checked={isComboEnabled}
603
- onChange={(e) => setIsComboEnabled(e.target.checked)}
604
- className="rounded border-ancient/30 bg-void text-ancient focus:ring-ancient"
605
- />
606
- 开启组合占卜
607
- </label>
608
- </div>
609
-
610
- {interpretation.semantic && (
611
- <div className="mb-6 p-4 bg-ancient/5 border border-ancient/20 rounded-lg">
612
- <div className="text-xs text-ancient/60 mb-3">本次解读识别到:</div>
613
- <div className="flex flex-wrap gap-2">
614
- <span className="px-3 py-1 rounded-full bg-ancient/10 border border-ancient/20 text-ancient text-xs">
615
- 场景:{interpretation.semantic.sceneLabel}
616
- </span>
617
- <span className="px-3 py-1 rounded-full bg-mystic/10 border border-mystic/20 text-mystic text-xs">
618
- 情绪:{interpretation.semantic.emotionLabel}
619
- </span>
620
- <span className="px-3 py-1 rounded-full bg-abyss border border-stardust/20 text-stardust/60 text-xs">
621
- 时态:{interpretation.semantic.tenseLabel}
622
- </span>
623
- <span className="px-3 py-1 rounded-full bg-abyss border border-stardust/20 text-stardust/60 text-xs">
624
- 对象:{interpretation.semantic.subjectLabel}
625
- </span>
626
- </div>
627
- </div>
628
- )}
629
-
630
- <div className="bg-abyss/30 rounded-lg p-4 mb-6">
631
- <div className="flex items-center justify-between">
632
- <div className="flex items-center gap-4">
633
- <button
634
- onClick={() => setIsPlaying(!isPlaying)}
635
- className="p-2 rounded-full bg-ancient/20 text-ancient hover:bg-ancient/30 transition-colors"
636
- >
637
- {isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
638
- </button>
639
- <button
640
- onClick={() => setCurrentSectionIndex(prev => Math.max(0, prev - 1))}
641
- className="p-2 rounded-full bg-abyss border border-stardust/20 text-stardust/60 hover:border-ancient/30 hover:text-ancient transition-colors"
642
- >
643
- <SkipBack className="w-4 h-4" />
644
- </button>
645
- <button
646
- onClick={() => setCurrentSectionIndex(prev => Math.min(interpretation.sections.length - 1, prev + 1))}
647
- className="p-2 rounded-full bg-abyss border border-stardust/20 text-stardust/60 hover:border-ancient/30 hover:text-ancient transition-colors"
648
- >
649
- <SkipForward className="w-4 h-4" />
650
- </button>
651
- </div>
652
- <div className="flex items-center gap-3">
653
- <span className="text-xs text-stardust/40">显示速度</span>
654
- <input
655
- type="range"
656
- min="30"
657
- max="100"
658
- value={displaySpeed}
659
- onChange={(e) => setDisplaySpeed(Number(e.target.value))}
660
- className="w-24 accent-ancient"
661
- />
662
- </div>
663
- </div>
664
- <div className="mt-4 flex items-center gap-2">
665
- {interpretation.sections.map((_: unknown, idx: number) => (
666
- <button
667
- key={idx}
668
- onClick={() => goToSection(idx)}
669
- className={`flex-1 h-1.5 rounded-full transition-all ${
670
- idx === currentSectionIndex ? 'bg-ancient' :
671
- idx < currentSectionIndex ? 'bg-ancient/40' : 'bg-abyss'
672
- }`}
673
- />
674
- ))}
675
- </div>
676
- </div>
677
-
678
- <div className="space-y-1">
679
- {interpretation.sections.map((s: any, idx: number) => renderInterpretationSection(s, idx))}
680
- </div>
681
-
682
- {isComboEnabled && (
683
- <div className="mt-8 pt-6 border-t border-ancient/20">
684
- <div className="flex flex-wrap items-center gap-4 mb-4">
685
- <h3 className="font-display text-ancient text-sm tracking-wider">东方术数</h3>
686
- <select
687
- value={selectedComboMethod}
688
- onChange={(e) => setSelectedComboMethod(e.target.value)}
689
- className="bg-void/50 border border-ancient/20 rounded px-3 py-1 text-sm text-stardust focus:border-ancient/60 focus:outline-none"
690
- >
691
- {comboMethods.map((method) => (
692
- <option key={method.value} value={method.value}>{method.label}</option>
693
- ))}
694
- </select>
695
- <button
696
- onClick={handleCombo}
697
- className="px-4 py-1 bg-mystic/20 border border-mystic/50 text-mystic hover:bg-mystic/30 transition-all rounded-sm text-sm"
698
- >
699
- 起卦
700
- </button>
701
- </div>
702
-
703
- {comboResultLocal && (
704
- <div className="bg-abyss/40 rounded-lg p-4 border border-mystic/20">
705
- <div className="flex items-center gap-2 mb-2">
706
- <span className="text-mystic font-display text-sm">{comboResultLocal.methodName}</span>
707
- <span className="text-ancient text-sm">{comboResultLocal.result}</span>
708
- </div>
709
- <p className="text-stardust/60 text-sm mb-2">{comboResultLocal.detail}</p>
710
- <p className="text-mystic/70 text-sm">{comboResultLocal.relationToTarot}</p>
711
- </div>
712
- )}
713
- </div>
714
- )}
715
-
716
- <div className="flex flex-wrap gap-3 mt-8 pt-6 border-t border-ancient/10">
717
- <button
718
- onClick={reset}
719
- className="flex items-center gap-2 px-6 py-2 border border-ancient/40 text-ancient hover:bg-ancient/10 transition-all rounded-sm text-sm"
720
- >
721
- <RotateCcw className="w-4 h-4" />
722
- 再次占卜
723
- </button>
724
- <button
725
- onClick={() => navigate('/history')}
726
- className="flex items-center gap-2 px-6 py-2 border border-stardust/20 text-stardust/60 hover:border-stardust/40 hover:text-stardust transition-all rounded-sm text-sm"
727
- >
728
- 查看历史
729
- </button>
730
- </div>
731
- </div>
732
- )}
733
- </div>
734
- </div>
735
- );
736
- }