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