這份程式碼是一個完整的單頁塔羅占卜應用,以下逐段(以功能區塊為單位)解釋每一行的作用與設計意圖。
<head>(第 1–13 行)<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>✦ 神秘塔羅占卜 ✦</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&family=Noto+Serif+TC:wght@300;400;600&display=swap" rel="stylesheet">
<!DOCTYPE html>:宣告為 HTML5 文件。<html lang="zh-TW">:設定網頁語言為繁體中文(台灣)。<meta charset="UTF-8">:設定字元編碼為 UTF-8,支援中文與特殊符號。<meta name="viewport" ...>:讓網頁在手機上正確縮放(響應式設計基礎)。<title>:瀏覽器分頁標題。<link ... Google Fonts>:載入兩款字體:
Cinzel Decorative:用於標題、按鈕等裝飾性文字Noto Serif TC:用於內文,支援繁體中文要解釋的程式碼全部
<!DOCTYPE html> <html lang="zh-TW"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>✦ 神秘塔羅占卜 ✦</title> <link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&family=Noto+Serif+TC:wght@300;400;600&display=swap" rel="stylesheet"> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { background: #050d1a; color: #e8dfc8; font-family: 'Noto Serif TC', serif; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; padding: 40px 20px 80px; background-image: radial-gradient(ellipse at 20% 20%, rgba(90,40,140,0.18) 0%, transparent 60%), radial-gradient(ellipse at 80% 80%, rgba(20,60,120,0.22) 0%, transparent 60%), url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.018'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); } .stars { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; overflow: hidden; } .star { position: absolute; background: #fff; border-radius: 50%; animation: twinkle 3s infinite alternate; } @keyframes twinkle { 0% { opacity: 0.1; } 100% { opacity: 0.7; } } .container { position: relative; z-index: 1; width: 100%; max-width: 760px; } .title-block { text-align: center; margin-bottom: 48px; } .title-symbol { font-size: 2.4rem; letter-spacing: 0.5em; color: #c9a84c; display: block; margin-bottom: 12px; } .title-main { font-family: 'Cinzel Decorative', serif; font-size: clamp(1.6rem, 5vw, 2.6rem); color: #f0e6c4; letter-spacing: 0.08em; text-shadow: 0 0 40px rgba(201,168,76,0.5); line-height: 1.3; } .title-sub { font-size: 1rem; color: #9bafc8; margin-top: 12px; letter-spacing: 0.15em; } .divider { display: flex; align-items: center; gap: 12px; margin: 24px auto; max-width: 320px; } .divider-line { flex: 1; height: 1px; background: linear-gradient(90deg, transparent, #c9a84c, transparent); } .divider-gem { color: #c9a84c; font-size: 1rem; } .panel { background: linear-gradient(135deg, rgba(10,22,45,0.95) 0%, rgba(8,18,38,0.98) 100%); border: 1px solid rgba(201,168,76,0.25); border-radius: 20px; padding: 44px 40px; box-shadow: 0 0 60px rgba(0,0,0,0.6), inset 0 0 40px rgba(90,40,140,0.06); margin-bottom: 32px; } .step-indicator { display: flex; justify-content: center; gap: 10px; margin-bottom: 40px; } .step-dot { width: 10px; height: 10px; border-radius: 50%; background: rgba(201,168,76,0.2); border: 1px solid rgba(201,168,76,0.35); } .step-dot.active { background: #c9a84c; box-shadow: 0 0 10px rgba(201,168,76,0.6); } .step-dot.done { background: rgba(201,168,76,0.5); } .section-label { font-family: 'Cinzel Decorative', serif; font-size: 0.75rem; letter-spacing: 0.3em; color: #c9a84c; text-transform: uppercase; margin-bottom: 16px; display: flex; align-items: center; gap: 10px; } .section-label::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, rgba(201,168,76,0.4), transparent); } .question-label { font-size: 1.25rem; color: #d4c9b0; margin-bottom: 20px; line-height: 1.7; } textarea { width: 100%; background: rgba(255,255,255,0.04); border: 1px solid rgba(201,168,76,0.3); border-radius: 12px; color: #e8dfc8; font-family: 'Noto Serif TC', serif; font-size: 1.1rem; line-height: 1.8; padding: 18px 22px; resize: vertical; min-height: 110px; outline: none; } textarea::placeholder { color: rgba(200,190,165,0.35); } .spread-options { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 24px; } .spread-opt { border: 1px solid rgba(201,168,76,0.28); border-radius: 12px; padding: 18px 14px; text-align: center; cursor: pointer; background: rgba(255,255,255,0.025); } .spread-opt.selected { border-color: #c9a84c; background: rgba(201,168,76,0.08); box-shadow: 0 0 20px rgba(201,168,76,0.15); } .spread-opt-icon { font-size: 1.8rem; display: block; margin-bottom: 8px; } .spread-opt-name { font-size: 0.95rem; color: #d4c9b0; font-weight: 600; display: block; margin-bottom: 4px; } .spread-opt-desc { font-size: 0.78rem; color: #7d8fa8; line-height: 1.5; } .btn { display: inline-block; font-family: 'Cinzel Decorative', serif; font-size: 0.9rem; letter-spacing: 0.2em; padding: 16px 40px; border-radius: 50px; cursor: pointer; border: none; margin-top: 32px; } .btn-primary { background: linear-gradient(135deg, #b8882e, #e2b84a, #b8882e); color: #0a1628; box-shadow: 0 4px 24px rgba(201,168,76,0.35); font-weight: 700; } .btn-ghost { background: transparent; border: 1px solid rgba(201,168,76,0.4); color: #c9a84c; } .btn-center { display: flex; justify-content: center; } .cards-draw-area { display: flex; gap: 20px; justify-content: center; flex-wrap: wrap; margin: 32px 0; } .card-slot { display: flex; flex-direction: column; align-items: center; gap: 12px; } .card-slot-label { font-size: 0.82rem; color: #9bafc8; letter-spacing: 0.12em; text-align: center; } .tarot-card { width: 110px; height: 180px; border-radius: 12px; border: 2px solid rgba(201,168,76,0.3); background: linear-gradient(160deg, #0d1f3c 0%, #060f20 100%); display: flex; align-items: center; justify-content: center; cursor: pointer; position: relative; overflow: hidden; box-shadow: 0 8px 32px rgba(0,0,0,0.5); } .tarot-card.face-down { background: linear-gradient(160deg, #0e1f40, #07122a); } .tarot-card.face-down::before { content: ''; position: absolute; inset: 8px; border: 1px solid rgba(201,168,76,0.2); border-radius: 6px; background-image: repeating-linear-gradient( 45deg, transparent, transparent 4px, rgba(201,168,76,0.04) 4px, rgba(201,168,76,0.04) 5px ); } .card-back-symbol { font-size: 2.4rem; opacity: 0.6; position: relative; z-index: 1; } .tarot-card.revealed { border-color: #c9a84c; background: linear-gradient(160deg, #12213f, #0a1628); box-shadow: 0 0 30px rgba(201,168,76,0.2), 0 8px 32px rgba(0,0,0,0.5); cursor: default; } .card-face { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 12px 8px; gap: 8px; text-align: center; } .card-arcana-symbol { font-size: 2.6rem; line-height: 1; } .card-name-en { font-family: 'Cinzel Decorative', serif; font-size: 0.52rem; letter-spacing: 0.08em; color: #c9a84c; line-height: 1.3; } .card-name-zh { font-size: 0.78rem; color: #e8dfc8; font-weight: 600; } .card-reversed { font-size: 0.62rem; color: #9b5a5a; letter-spacing: 0.1em; } .card-upright { font-size: 0.62rem; color: #5a9b6a; letter-spacing: 0.1em; } .reading-section { margin-top: 40px; } .reading-card-block { border-left: 3px solid rgba(201,168,76,0.4); padding-left: 24px; margin-bottom: 36px; } .reading-card-header { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; } .reading-card-icon { font-size: 1.8rem; } .reading-card-title { font-family: 'Cinzel Decorative', serif; font-size: 1rem; color: #c9a84c; line-height: 1.4; } .reading-card-pos { font-size: 0.78rem; color: #7d8fa8; margin-top: 2px; letter-spacing: 0.1em; } .reading-text { font-size: 1.05rem; color: #cfc4aa; line-height: 1.9; } .reading-keywords { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; } .kw-tag { font-size: 0.78rem; padding: 4px 12px; border-radius: 20px; border: 1px solid rgba(201,168,76,0.3); color: #c9a84c; letter-spacing: 0.08em; } .synthesis-block { background: rgba(201,168,76,0.06); border: 1px solid rgba(201,168,76,0.2); border-radius: 14px; padding: 28px 30px; margin-top: 36px; } .synthesis-title { font-family: 'Cinzel Decorative', serif; font-size: 0.95rem; color: #c9a84c; letter-spacing: 0.15em; margin-bottom: 16px; } .synthesis-text { font-size: 1.08rem; color: #d4c9b0; line-height: 2; } .advice-block { margin-top: 24px; padding: 20px 24px; background: rgba(90,40,140,0.08); border-radius: 12px; border: 1px solid rgba(140,90,200,0.2); } .advice-label { font-size: 0.78rem; color: #a080c8; letter-spacing: 0.2em; margin-bottom: 10px; } .advice-text { font-size: 1rem; color: #c8b8e0; line-height: 1.85; } .loading-oracle { text-align: center; padding: 48px 0; } .oracle-spinner { font-size: 3rem; display: block; margin: 0 auto 20px; animation: spin 3s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .oracle-text { font-size: 1rem; color: #9bafc8; letter-spacing: 0.2em; } .error-msg { background: rgba(180,60,60,0.1); border: 1px solid rgba(180,60,60,0.3); border-radius: 10px; padding: 16px 20px; font-size: 0.95rem; color: #e09090; margin-top: 20px; } .restart-block { text-align: center; margin-top: 48px; padding-top: 32px; border-top: 1px solid rgba(201,168,76,0.15); } .restart-msg { font-size: 0.9rem; color: #7d8fa8; margin-bottom: 20px; letter-spacing: 0.08em; } .hidden { display: none !important; } @media (max-width: 680px) { .panel { padding: 28px 20px; } .spread-options { grid-template-columns: 1fr; } .cards-draw-area { gap: 14px; } .tarot-card { width: 88px; height: 148px; } .card-arcana-symbol { font-size: 2rem; } .reading-card-block { padding-left: 14px; } .synthesis-block { padding: 20px 18px; } } @media (max-width: 400px) { .tarot-card { width: 76px; height: 130px; } .card-name-en { font-size: 0.44rem; } .btn { padding: 14px 28px; font-size: 0.8rem; } } </style> </head> <body> <div class="stars" id="starsContainer"></div> <div class="container"> <div class="title-block"> <span class="title-symbol">✦ ☽ ✦</span> <h1 class="title-main">神秘塔羅占卜</h1> <p class="title-sub">與宇宙的智慧對話 · 探索命運的密語</p> <div class="divider"> <div class="divider-line"></div> <span class="divider-gem">◆</span> <div class="divider-line"></div> </div> </div> <div class="panel" id="step1Panel"> <div class="step-indicator"> <div class="step-dot active" id="dot1"></div> <div class="step-dot" id="dot2"></div> <div class="step-dot" id="dot3"></div> </div> <div class="section-label">第一步 · 心中之問</div> <p class="question-label">在靜默中,將你的疑惑化為文字。<br>問題越真誠,宇宙回應越清晰。</p> <textarea id="questionInput" placeholder="例如:我在這段感情中應該如何走下一步? 或:這次轉職機會對我是否適合?"></textarea> <div class="section-label" style="margin-top:32px;">選擇牌陣</div> <div class="spread-options" id="spreadOptions"> <div class="spread-opt selected" data-spread="single" onclick="selectSpread(this)"> <span class="spread-opt-icon">🌕</span> <span class="spread-opt-name">單張牌陣</span> <span class="spread-opt-desc">一張牌,一個核心答案</span> </div> <div class="spread-opt" data-spread="three" onclick="selectSpread(this)"> <span class="spread-opt-icon">🌙</span> <span class="spread-opt-name">三張牌陣</span> <span class="spread-opt-desc">過去・現在・未來</span> </div> <div class="spread-opt" data-spread="celtic" onclick="selectSpread(this)"> <span class="spread-opt-icon">⭐</span> <span class="spread-opt-name">凱爾特十字</span> <span class="spread-opt-desc">五張深度全面解析</span> </div> </div> <div class="btn-center"> <button class="btn btn-primary" onclick="goToStep2()">✦ 召喚牌卡 ✦</button> </div> <div class="error-msg hidden" id="step1Error">請先在上方輸入你的問題,再讓塔羅為你指引。</div> </div> <div class="panel hidden" id="step2Panel"> <div class="step-indicator"> <div class="step-dot done"></div> <div class="step-dot active"></div> <div class="step-dot"></div> </div> <div class="section-label">第二步 · 抽取牌卡</div> <p class="question-label" style="margin-bottom:8px;">你的問題:</p> <p id="questionDisplay" style="font-size:1.1rem;color:#c9a84c;font-style:italic;line-height:1.7;margin-bottom:28px;"></p> <p style="font-size:1rem;color:#9bafc8;line-height:1.8;margin-bottom:24px;text-align:center;"> 深呼吸,清空思緒。<br>點選每一張牌背,讓宇宙為你揭示答案。 </p> <div class="cards-draw-area" id="cardsArea"></div> <div class="btn-center"> <button class="btn btn-primary hidden" id="interpretBtn" onclick="goToStep3()">✦ 解讀星象 ✦</button> </div> </div> <div class="panel hidden" id="step3Panel"> <div class="step-indicator"> <div class="step-dot done"></div> <div class="step-dot done"></div> <div class="step-dot active"></div> </div> <div class="section-label">第三步 · 命運解析</div> <div class="loading-oracle hidden" id="loadingArea"> <span class="oracle-spinner">🔮</span> <p class="oracle-text">神諭正在解讀星象…</p> </div> <div id="readingContent"></div> <div class="error-msg hidden" id="readingError"></div> <div class="restart-block hidden" id="restartBlock"> <p class="restart-msg">✦ 此次占卜已完成 · 願星光指引你的道路 ✦</p> <button class="btn btn-ghost" onclick="restart()">再次占卜</button> </div> </div> </div> <script> // ────────────────────────────────────────────── // 塔羅牌資料庫(22 張大阿爾克那) // ────────────────────────────────────────────── const TAROT_DECK = [ { id:0, en:"The Fool", zh:"愚者", symbol:"🌀", keywords:["新開始","冒險","純真","自由","無懼"], upright_core:"勇敢踏出第一步,以赤子之心迎接未知。", reversed_core:"衝動魯莽,缺乏計畫,逃避責任。" }, { id:1, en:"The Magician", zh:"魔術師", symbol:"⚡", keywords:["意志力","創造","技能","行動","掌控"], upright_core:"你擁有實現目標的一切工具,全力施展吧。", reversed_core:"操弄、欺騙,或能力尚未充分發揮。" }, { id:2, en:"The High Priestess",zh:"女祭司", symbol:"🌙", keywords:["直覺","神秘","內在智慧","潛意識"], upright_core:"傾聽內心深處的聲音,真理藏於靜默之中。", reversed_core:"壓抑直覺,隱藏秘密,或與潛意識脫節。" }, { id:3, en:"The Empress", zh:"女皇", symbol:"🌸", keywords:["豐盛","母性","創造力","美麗","自然"], upright_core:"大地的慷慨降臨,豐饒與愛意圍繞著你。", reversed_core:"依賴、過度保護,或創造力受阻。" }, { id:4, en:"The Emperor", zh:"皇帝", symbol:"🏛️", keywords:["權威","結構","穩定","父性","秩序"], upright_core:"建立穩固的基礎,以智慧與紀律引領全局。", reversed_core:"獨裁、剛愎,或缺乏自我約束。" }, { id:5, en:"The Hierophant", zh:"教皇", symbol:"⛪", keywords:["傳統","信仰","教導","精神導引","遵循"], upright_core:"在傳統與信仰中尋找力量,向智者請益。", reversed_core:"叛逆傳統,或被教條束縛。" }, { id:6, en:"The Lovers", zh:"戀人", symbol:"💞", keywords:["愛情","選擇","和諧","價值觀","結合"], upright_core:"重大選擇即將到來,遵循心之所向。", reversed_core:"關係不和諧,價值觀衝突,或逃避選擇。" }, { id:7, en:"The Chariot", zh:"戰車", symbol:"🏆", keywords:["勝利","意志","自律","掌控","前進"], upright_core:"以堅定意志克服阻礙,勝利屬於不懈之人。", reversed_core:"失去方向,或以強硬手段強行推進。" }, { id:8, en:"Strength", zh:"力量", symbol:"🦁", keywords:["勇氣","耐心","慈悲","內在力量","控制"], upright_core:"真正的力量來自溫柔與韌性,而非蠻力。", reversed_core:"懷疑自我,或以恐懼驅動行動。" }, { id:9, en:"The Hermit", zh:"隱士", symbol:"🕯️", keywords:["孤獨","內省","尋求","智慧","引導"], upright_core:"退入內心,在孤獨中尋得屬於你的真理。", reversed_core:"過度孤立,或拒絕接受他人智慧。" }, { id:10, en:"Wheel of Fortune", zh:"命運之輪",symbol:"☯️", keywords:["命運","轉機","循環","機會","變化"], upright_core:"命運的齒輪轉動,好運與轉機即將到來。", reversed_core:"厄運、阻力,或抵抗無可避免的變化。" }, { id:11, en:"Justice", zh:"正義", symbol:"⚖️", keywords:["公平","真相","因果","決策","平衡"], upright_core:"因果律明確,公正的結果即將揭曉。", reversed_core:"不公、偏見,或逃避應承擔的責任。" }, { id:12, en:"The Hanged Man", zh:"倒吊人", symbol:"🌿", keywords:["暫停","犧牲","新視角","放下","等待"], upright_core:"暫停腳步,從不同角度觀看,將帶來頓悟。", reversed_core:"無謂犧牲,或因猶豫不決而停滯。" }, { id:13, en:"Death", zh:"死神", symbol:"🌑", keywords:["結束","轉化","放下","蛻變","重生"], upright_core:"一段旅程落幕,為嶄新的開始讓出空間。", reversed_core:"抗拒改變,或無法放下過去。" }, { id:14, en:"Temperance", zh:"節制", symbol:"🌊", keywords:["平衡","耐心","調和","療癒","中庸"], upright_core:"在靜流中調和一切,耐心將帶來療癒。", reversed_core:"過度、失衡,或極端行為導致動盪。" }, { id:15, en:"The Devil", zh:"惡魔", symbol:"⛓️", keywords:["束縛","誘惑","物質","陰暗面","依賴"], upright_core:"認清讓你受困的鎖鏈,你比想象中更自由。", reversed_core:"覺醒,開始掙脫束縛與不健康的依附。" }, { id:16, en:"The Tower", zh:"塔", symbol:"⚡", keywords:["突變","動盪","破壞","啟示","解構"], upright_core:"舊有結構崩塌,但廢墟中埋藏著新的真相。", reversed_core:"預防危機,或拒絕承認已然崩潰之物。" }, { id:17, en:"The Star", zh:"星星", symbol:"⭐", keywords:["希望","療癒","更新","靈感","信念"], upright_core:"黑暗之後,星光降臨。希望與療癒正在展開。", reversed_core:"失去信念,或夢想與現實嚴重脫節。" }, { id:18, en:"The Moon", zh:"月亮", symbol:"🌛", keywords:["幻象","潛意識","恐懼","直覺","模糊"], upright_core:"現實並非表面所示,細心辨別幻象與真相。", reversed_core:"恐懼消散,迷霧漸退,真相漸漸明朗。" }, { id:19, en:"The Sun", zh:"太陽", symbol:"☀️", keywords:["喜悅","成功","活力","光明","豐盛"], upright_core:"陽光燦爛,成功與喜悅如期而至。", reversed_core:"暫時的烏雲遮蔽,或過度樂觀而忽略細節。" }, { id:20, en:"Judgement", zh:"審判", symbol:"📯", keywords:["覺醒","重生","召喚","反思","轉化"], upright_core:"號角響起,是時候回應內心深處的召喚。", reversed_core:"自我懷疑,或逃避做出重大決定。" }, { id:21, en:"The World", zh:"世界", symbol:"🌍", keywords:["完整","成就","整合","旅程終結","慶賀"], upright_core:"一個完整的循環圓滿結束,你達成了真正的成就。", reversed_core:"尚未完成,或滿足於未臻完美的結果。" }, ]; const SPREAD_CONFIG = { single: { count:1, positions:["核心訊息"] }, three: { count:3, positions:["過去・根源","現在・核心","未來・展望"] }, celtic: { count:5, positions:["現況・核心","挑戰・阻力","潛意識・根源","近期能量","最終結果"] }, }; // 問題類型分類器(符合塔羅諮詢原則) const QUESTION_TYPES = { love: { keywords: ["感情", "愛情", "關係", "伴侶", "婚姻", "約會", "單身", "復合", "暗戀", "桃花", "前任", "曖昧", "交往", "分手", "離婚", "結婚"], advicePool: [ "在愛中保持真誠,但也別忘記保護自己的心。", "真正的親密關係建立在相互尊重與理解之上。", "愛不是佔有,而是讓彼此都能自由成長。", "傾聽內心的聲音,它比外在的聲音更懂你的愛。", "關係中的挑戰,往往是成長的契機。", ], closingPool: [ "願愛如星光,溫柔照亮你的道路。", "心之所向,愛必隨行。", "真正的愛情,始於愛自己。", "愛的能量永遠與你同在。", ] }, career: { keywords: ["工作", "事業", "轉職", "升遷", "面試", "創業", "離職", "老闆", "同事", "薪水", "職場", "求職", "換工作", "開業"], advicePool: [ "你的才華值得被看見,但也要學會在適當的時機展現。", "事業的成功不僅是成就,更是對自我價值的實現。", "在職場中,專業能力與人際智慧同樣重要。", "每一次挑戰都是成長的契機,勇敢面對吧。", "相信自己的專業判斷,你比想象中更有能力。", ], closingPool: [ "願你的努力如種子,在適當時機綻放花果。", "事業如星軌,自有其運行節奏。", "真正的成功,是成為自己想成為的人。", "職場之路,星光相伴。", ] }, growth: { keywords: ["成長", "學習", "改變", "突破", "瓶頸", "迷茫", "方向", "目標", "夢想", "自我"], advicePool: [ "成長往往發生在舒適圈之外,勇敢邁出那一步。", "你內在的智慧永遠比你外在的眼睛看得更遠。", "每一次迷茫,都是重新認識自己的機會。", "相信過程,你正走在屬於你的道路上。", ], closingPool: [ "願你與真實的自己相遇。", "成長之路,星光指引。", "內在的光,永遠照亮前行的路。", ] }, general: { keywords: ["運勢", "未來", "選擇", "人生", "困惑", "指引", "建議", "怎麼辦", "如何"], advicePool: [ "相信宇宙的指引,你比想象中更強大。", "每一步都算數,即使是迷途也是旅程的一部分。", "放下恐懼,擁抱未知,奇蹟往往藏在轉角處。", "靜下心來,答案早已在你心中。", ], closingPool: [ "願星光指引你的道路。", "宇宙的智慧永遠與你同在。", "命運之輪轉動,你正走向屬於你的光。", "✨ 願祝福與你同行 ✦", ] } }; let state = { question: "", spread: "single", drawnCards: [], revealedCount: 0, }; // ────────────────────────────────────────────── // 生成星星 // ────────────────────────────────────────────── (function initStars() { const container = document.getElementById('starsContainer'); for (let i = 0; i < 80; i++) { const star = document.createElement('div'); star.className = 'star'; const size = Math.random() * 2.5 + 0.5; star.style.cssText = ` width:${size}px; height:${size}px; top:${Math.random()*100}%; left:${Math.random()*100}%; animation-delay:${Math.random()*3}s; animation-duration:${2+Math.random()*3}s; `; container.appendChild(star); } })(); // ────────────────────────────────────────────── // 牌陣選擇 // ────────────────────────────────────────────── function selectSpread(el) { document.querySelectorAll('.spread-opt').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); state.spread = el.dataset.spread; } // ────────────────────────────────────────────── // Step 1 → Step 2 // ────────────────────────────────────────────── function goToStep2() { const q = document.getElementById('questionInput').value.trim(); if (!q) { document.getElementById('step1Error').classList.remove('hidden'); return; } document.getElementById('step1Error').classList.add('hidden'); state.question = q; const cfg = SPREAD_CONFIG[state.spread]; const shuffled = [...TAROT_DECK].sort(() => Math.random() - 0.5); state.drawnCards = shuffled.slice(0, cfg.count).map(card => ({ ...card, reversed: Math.random() < 0.3, revealed: false, })); state.revealedCount = 0; document.getElementById('questionDisplay').textContent = `「${q}」`; const area = document.getElementById('cardsArea'); area.innerHTML = ''; state.drawnCards.forEach((card, i) => { const slot = document.createElement('div'); slot.className = 'card-slot'; slot.innerHTML = ` <div class="tarot-card face-down" id="card_${i}" onclick="revealCard(${i})"> <span class="card-back-symbol">✦</span> </div> <span class="card-slot-label">${cfg.positions[i]}</span> `; area.appendChild(slot); }); document.getElementById('step1Panel').classList.add('hidden'); document.getElementById('step2Panel').classList.remove('hidden'); } // ────────────────────────────────────────────── // 翻牌 // ────────────────────────────────────────────── function revealCard(i) { if (state.drawnCards[i].revealed) return; state.drawnCards[i].revealed = true; state.revealedCount++; const card = state.drawnCards[i]; const el = document.getElementById(`card_${i}`); el.classList.remove('face-down'); el.classList.add('revealed'); el.style.cursor = 'default'; el.innerHTML = ` <div class="card-face" style="${card.reversed ? 'transform:rotate(180deg)' : ''}"> <span class="card-arcana-symbol">${card.symbol}</span> <span class="card-name-en">${card.en}</span> <span class="card-name-zh">${card.zh}</span> <span class="${card.reversed ? 'card-reversed' : 'card-upright'}">${card.reversed ? '逆位' : '正位'}</span> </div> `; if (state.revealedCount === state.drawnCards.length) { document.getElementById('interpretBtn').classList.remove('hidden'); } } // ────────────────────────────────────────────── // Step 2 → Step 3 // ────────────────────────────────────────────── function goToStep3() { document.getElementById('step2Panel').classList.add('hidden'); document.getElementById('step3Panel').classList.remove('hidden'); document.getElementById('loadingArea').classList.remove('hidden'); // 模擬神諭解讀過程(2.5 秒,符合能量流動節奏) setTimeout(() => { const readingData = generateTarotReading(); renderReading(readingData); }, 2500); } // ────────────────────────────────────────────── // 【核心】本地塔羅解讀引擎(完整符合塔羅原則) // ────────────────────────────────────────────── function generateTarotReading() { const cfg = SPREAD_CONFIG[state.spread]; const question = state.question; // 1. 判斷問題類型 let qType = 'general'; for (const [type, config] of Object.entries(QUESTION_TYPES)) { if (config.keywords.some(k => question.includes(k))) { qType = type; break; } } const typeConfig = QUESTION_TYPES[qType]; // 2. 為每張牌生成解讀 const cardsReading = state.drawnCards.map((card, idx) => { const position = cfg.positions[idx]; const orientation = card.reversed ? 'reversed' : 'upright'; const coreMeaning = card[orientation + '_core']; // 根據牌陣位置調整語氣 let positionContext = ''; if (state.spread === 'three') { if (idx === 0) positionContext = '這段過往的能量塑造了當下的你,'; else if (idx === 1) positionContext = '此刻的你正面臨這樣的能量,'; else if (idx === 2) positionContext = '未來將朝向這樣的能量發展,'; } else if (state.spread === 'celtic') { const ctxMap = [ '當下的核心能量顯示,', '你面臨的挑戰是,', '潛意識深處,', '近期的能量流動指向,', '最終將走向,' ]; positionContext = ctxMap[idx] || ''; } // 生成個人化解讀(80-120 字) const interpretation = `${positionContext}${coreMeaning}這張牌出現在這裡,提醒你${card.keywords.slice(0, 2).join('與')}的能量正在運作。${card.reversed ? '逆位時,這份能量需要被重新審視與調整。' : '正位時,這份能量將全力支持你。'}`; // 能量描述(20-30 字) const energyDesc = card.reversed ? `需要覺察並轉化${card.keywords[0]}的阻塞能量` : `${card.keywords[0]}的能量正流動著,請好好運用`; return { position, card_zh: card.zh, card_en: card.en, symbol: card.symbol, orientation: card.reversed ? '逆位' : '正位', keywords: card.keywords, interpretation, energy: energyDesc }; }); // 3. 生成整體解讀(150-200 字) const uprightCount = state.drawnCards.filter(c => !c.reversed).length; const reversedCount = state.drawnCards.length - uprightCount; let synthesis = ''; if (uprightCount > reversedCount) { synthesis = `整體而言,這是一個充滿${state.drawnCards[0].keywords[0]}能量的牌陣。`; } else if (reversedCount > uprightCount) { synthesis = `牌陣中出現較多逆位,暗示某些能量需要被重新審視與調整。`; } else { synthesis = `正逆位能量平衡,代表你正處於一個需要权衡與選擇的時刻。`; } synthesis += `${typeConfig.advicePool[Math.floor(Math.random() * typeConfig.advicePool.length)]} `; // 加入牌陣流暢解讀 if (state.spread === 'three') { synthesis += `從過去到現在再到未來,能量呈現${state.drawnCards.map(c => c.zh).join('→')}的流動,`; synthesis += '這是一段清晰的演化旅程。'; } else if (state.spread === 'celtic') { synthesis += `五張牌共同描繪出完整的能量圖景,`; synthesis += '每個層面都相互呼應,指向同一個核心課題。'; } else { synthesis += '這張牌是你的核心指引,請用心感受它帶來的訊息。'; } // 4. 行動建議(80-120 字) const advice = `${typeConfig.advicePool[Math.floor(Math.random() * typeConfig.advicePool.length)]} ` + `塔羅建議你:${state.drawnCards[0].upright_core} ` + `記住,牌卡只是指引,真正的選擇權永遠在你手中。`; // 5. 詩意結語(25 字內) const closing = typeConfig.closingPool[Math.floor(Math.random() * typeConfig.closingPool.length)]; return { cards: cardsReading, synthesis, advice, closing }; } // ────────────────────────────────────────────── // 渲染解析結果 // ────────────────────────────────────────────── function renderReading(data) { document.getElementById('loadingArea').classList.add('hidden'); let html = '<div class="reading-section">'; data.cards.forEach((card, i) => { const border = i % 2 === 0 ? "rgba(201,168,76,0.4)" : "rgba(140,90,200,0.35)"; html += ` <div class="reading-card-block" style="border-left-color:${border}"> <div class="reading-card-header"> <span class="reading-card-icon">${card.symbol}</span> <div> <div class="reading-card-title">${card.card_zh} · ${card.orientation}</div> <div class="reading-card-pos">▸ ${card.position}</div> </div> </div> <p class="reading-text">${card.interpretation}</p> <div class="reading-keywords"> ${card.keywords.map(k => `<span class="kw-tag">${k}</span>`).join('')} </div> <p style="margin-top:12px;font-size:0.88rem;color:${border};font-style:italic;">${card.energy}</p> </div>`; }); html += ` <div class="synthesis-block"> <div class="synthesis-title">✦ 整體解讀</div> <p class="synthesis-text">${data.synthesis}</p> <div class="advice-block"> <div class="advice-label">◈ 星辰給予的指引</div> <p class="advice-text">${data.advice}</p> </div> </div> <div style="text-align:center;margin-top:36px;padding:24px;"> <p style="font-family:'Cinzel Decorative',serif;font-size:0.85rem;color:rgba(201,168,76,0.7);letter-spacing:0.2em;"> ✦ ${data.closing} ✦ </p> </div> </div>`; document.getElementById('readingContent').innerHTML = html; document.getElementById('restartBlock').classList.remove('hidden'); } // ────────────────────────────────────────────── // 重置 // ────────────────────────────────────────────── function restart() { state = { question:"", spread:"single", drawnCards:[], revealedCount:0 }; document.getElementById('questionInput').value = ''; document.getElementById('readingContent').innerHTML = ''; document.getElementById('loadingArea').classList.add('hidden'); document.getElementById('readingError').classList.add('hidden'); document.getElementById('restartBlock').classList.add('hidden'); document.getElementById('interpretBtn').classList.add('hidden'); document.querySelectorAll('.spread-opt').forEach(o => { o.classList.toggle('selected', o.dataset.spread === 'single'); }); document.getElementById('step3Panel').classList.add('hidden'); document.getElementById('step2Panel').classList.add('hidden'); document.getElementById('step1Panel').classList.remove('hidden'); } </script> </body> </html>
其他明天繼續~