https://laihao.vaserver.com/tarot.html?fbclid=IwY2xjawRBF8xleHRuA2FlbQIxMABicmlkETFNRzlhS0tkeWNyYWhrYmltc3J0YwZhcHBfaWQQMjIyMDM5MTc4ODIwMDg5MgABHhShVQM_xXrP3_t7gStWxg0CfnzRrElLZp0reya4rA0Ar07DXIIRYo3Bd0kq_aem_ES0rD3EoUI2AZyPJ40lUEQ
-
<!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:用於內文,支援繁體中文
CSS 樣式(第 14–118 行)
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
-
* { ... }:全域重置,所有元素使用 border-box 模型,並清除預設邊距與內距。
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(...), url("data:image/svg+xml,...");
}
- 設定深藍黑色背景(
#050d1a),米白色文字(#e8dfc8)。
- 使用
flex 垂直居中內容,並設定上下左右內距。
-
background-image 包含:
- 兩個徑向漸層(紫色與藍色光暈)
- 一個內嵌 SVG 圖案(極淡的幾何重複紋理)
.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; } }
-
.stars:固定定位的滿版容器,用於放置星星,pointer-events: none 確保不阻擋點擊。
-
.star:絕對定位的白色圓點,套用 twinkle 動畫(透明度 0.1 ↔ 0.7 無限交替)。
.container { position: relative; z-index: 1; width: 100%; max-width: 760px; }
- 主內容容器,相對於
.stars 在上層(z-index: 1),最大寬度 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; }
- 標題區塊:
-
title-symbol:頂部的 ✦ ☽ ✦ 符號,金色(#c9a84c)
-
title-main:主標題「神秘塔羅占卜」,使用 clamp() 實現響應式字體(最小 1.6rem,最大 2.6rem)
-
title-sub:副標題,灰藍色
.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); }
- 步驟指示器:三個圓點,
.active 表示當前步驟(亮金色 + 發光),.done 表示已完成(半透明金色)。
.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);
}
- 牌陣選擇區:3 欄網格,每個選項為可點擊卡片,
.selected 時高亮金色邊框與背景。
.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; }
- 按鈕樣式:
-
.btn-primary:金色漸層背景 + 陰影(主要動作按鈕)
-
.btn-ghost:透明背景 + 金色邊框(次要按鈕)
-
.btn-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; }
- 牌卡樣式:
- 預設為牌背(
face-down),深藍漸層 + 金色斜紋圖案(repeating-linear-gradient)
- 中央顯示
✦ 符號
.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; }
- 翻開後的牌面(
revealed):
- 金色邊框 + 發光陰影
-
.card-face:垂直排列牌義符號、英文名、中文名、正/逆位標籤
- 正位綠色(
#5a9b6a)、逆位紅色(#9b5a5a)
.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; }
- 解讀區塊樣式:
- 每張牌左側金色豎線
- 關鍵字以圓角標籤(
.kw-tag)呈現
.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; }
- 重置區塊:頂部金色分隔線,用於占卜結束後顯示「再次占卜」按鈕。
-
.hidden:通用隱藏類別(display: none)。
@media (max-width: 680px) { ... }
@media (max-width: 400px) { ... }
- 響應式設計:
- 680px 以下:牌陣選項改為單欄、牌卡縮小
- 400px 以下:牌卡再縮小、按鈕內距減少
HTML 主體(第 121–200 行左右)
</style>
</head>
<body>
<div class="stars" id="starsContainer"></div>
- 結束
<style> 與 <head>,開始 <body>。
-
<div class="stars">:星星背景容器,後續由 JavaScript 動態生成星星。
<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>
Step 1 面板(第 201–240 行左右)
<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>
-
#step1Panel:第一步面板。
- 步驟指示器:三個圓點,第一個為
.active。
- 問題輸入區:
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>
- 牌陣選擇區:三個
.spread-opt,第一個預設 .selected,點擊觸發 selectSpread(this)。
- 「召喚牌卡」按鈕:點擊觸發
goToStep2()。
- 錯誤訊息:預設
.hidden,若未輸入問題則顯示。
Step 2 面板(第 241–260 行左右)
<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>
-
#step2Panel:預設 .hidden,進入 Step 2 時移除。
- 步驟指示器:第一個
.done,第二個 .active。
-
#questionDisplay:顯示使用者輸入的問題。
-
#cardsArea:動態插入牌卡的容器。
-
#interpretBtn:預設 .hidden,所有牌翻開後顯示。
Step 3 面板(第 261–275 行左右)
<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>
-
#step3Panel:預設 .hidden。
-
#loadingArea:載入動畫,預設 .hidden。
-
#readingContent:動態插入解讀結果。
-
#restartBlock:占卜結束後顯示,點擊「再次占卜」觸發 restart()。
</div> <!-- .container -->
JavaScript 邏輯(第 276 行–結尾)
塔羅牌資料庫(第 278–320 行左右)
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:"操弄、欺騙,或能力尚未充分發揮。" },
// ... 共 22 張牌(id 0–21)
];
-
TAROT_DECK:陣列,包含 22 張大阿爾克那牌,每張牌為物件,包含:
-
id:編號(0–21)
-
en:英文名
-
zh:中文名
-
symbol:Emoji 符號
-
keywords:關鍵字陣列
-
upright_core:正位核心牌義
-
reversed_core:逆位核心牌義
牌陣設定(第 322–328 行左右)
const SPREAD_CONFIG = {
single: { count:1, positions:["核心訊息"] },
three: { count:3, positions:["過去・根源","現在・核心","未來・展望"] },
celtic: { count:5, positions:["現況・核心","挑戰・阻力","潛意識・根源","近期能量","最終結果"] },
};
-
SPREAD_CONFIG:三種牌陣的設定:
-
count:抽牌數量
-
positions:每張牌的位置意義
問題類型分類器(第 330–360 行左右)
const QUESTION_TYPES = {
love: {
keywords: ["感情", "愛情", "關係", "伴侶", "婚姻", "約會", "單身", "復合", "暗戀", "桃花", "前任", "曖昧", "交往", "分手", "離婚", "結婚"],
advicePool: [ "在愛中保持真誠,但也別忘記保護自己的心。", ... ],
closingPool: [ "願愛如星光,溫柔照亮你的道路。", ... ],
},
career: { ... },
growth: { ... },
general: { ... },
};
-
QUESTION_TYPES:四種問題類型,每種包含:
-
keywords:用於比對使用者問題的關鍵字
-
advicePool:建議語庫(陣列)
-
closingPool:結語庫(陣列)
狀態變數(第 362–367 行左右)
let state = {
question: "",
spread: "single",
drawnCards: [],
revealedCount: 0,
};
-
state:全域狀態物件,記錄:
-
question:使用者輸入的問題
-
spread:選擇的牌陣(single/three/celtic)
-
drawnCards:已抽取的牌(陣列)
-
revealedCount:已翻開的牌數
星星背景初始化(第 371–382 行左右)
(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);
}
})();
- 立即執行函數
initStars():
- 取得
#starsContainer
- 迴圈 80 次,每次建立一個
.star 元素
- 隨機設定大小、位置、動畫延遲與持續時間
- 加入容器
牌陣選擇函數(第 386–391 行左右)
function selectSpread(el) {
document.querySelectorAll('.spread-opt').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
state.spread = el.dataset.spread;
}
-
selectSpread(el):
- 移除所有
.spread-opt 的 .selected 類別
- 為當前點擊的元素加上
.selected
- 更新
state.spread 為該元素的 data-spread 值
Step 1 → Step 2(第 395–417 行左右)
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;
- 取得並修剪使用者輸入的問題(
q)。
- 若為空,顯示錯誤訊息並返回。
- 否則隱藏錯誤訊息,將問題存入
state.question。
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;
- 取得當前牌陣設定(
cfg)。
- 洗牌:複製
TAROT_DECK 並隨機排序(Math.random() - 0.5)。
- 抽取前
cfg.count 張牌,每張牌:
- 複製原牌所有屬性
- 隨機決定是否逆位(30% 機率)
- 設定
revealed: false(尚未翻開)
- 重置
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);
});
- 顯示問題於
#questionDisplay。
- 清空
#cardsArea。
- 為每張牌建立
.card-slot:
- 牌卡為
.tarot-card face-down,ID 為 card_${i},點擊觸發 revealCard(i)
- 下方標籤顯示該牌陣位置(如「過去・根源」)
document.getElementById('step1Panel').classList.add('hidden');
document.getElementById('step2Panel').classList.remove('hidden');
}
- 隱藏 Step 1 面板,顯示 Step 2 面板。
翻牌函數(第 421–440 行左右)
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';
- 若該牌已翻開,直接返回。
- 否則標記為已翻開,
revealedCount 加 1。
- 取得牌卡元素,移除
.face-down,加上 .revealed,游標改為預設。
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>
`;
- 設定牌卡內容:
-
.card-face:若為逆位,旋轉 180 度
- 顯示符號、英文名、中文名、正/逆位標籤
if (state.revealedCount === state.drawnCards.length) {
document.getElementById('interpretBtn').classList.remove('hidden');
}
}
Step 2 → Step 3(第 444–455 行左右)
function goToStep3() {
document.getElementById('step2Panel').classList.add('hidden');
document.getElementById('step3Panel').classList.remove('hidden');
document.getElementById('loadingArea').classList.remove('hidden');
setTimeout(() => {
const readingData = generateTarotReading();
renderReading(readingData);
}, 2500);
}
- 隱藏 Step 2,顯示 Step 3 與載入動畫。
- 2.5 秒後:
- 呼叫
generateTarotReading() 生成解讀資料
- 呼叫
renderReading() 渲染結果
塔羅解讀引擎(第 459–520 行左右)
function generateTarotReading() {
const cfg = SPREAD_CONFIG[state.spread];
const question = state.question;
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];
- 預設問題類型為
general。
- 遍歷
QUESTION_TYPES,若問題包含該類型的任何關鍵字,則設定 qType 並跳出。
- 取得對應類型的設定(
typeConfig)。
const cardsReading = state.drawnCards.map((card, idx) => {
const position = cfg.positions[idx];
const orientation = card.reversed ? 'reversed' : 'upright';
const coreMeaning = card[orientation + '_core'];
- 為每張牌生成解讀:
- 取得位置名稱(如「過去・根源」)
- 判斷正/逆位(
orientation)
- 取得對應的核心牌義(
coreMeaning)
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] || '';
}
- 根據牌陣與索引,加入位置情境語氣(
positionContext)。
const interpretation = `${positionContext}${coreMeaning}這張牌出現在這裡,提醒你${card.keywords.slice(0, 2).join('與')}的能量正在運作。${card.reversed ? '逆位時,這份能量需要被重新審視與調整。' : '正位時,這份能量將全力支持你。'}`;
const energyDesc = card.reversed
? `需要覺察並轉化${card.keywords[0]}的阻塞能量`
: `${card.keywords[0]}的能量正流動著,請好好運用`;
- 組合個人化解讀(
interpretation):
- 位置情境 + 核心牌義 + 關鍵字提醒 + 正/逆位說明
- 能量描述(
energyDesc):一句話描述該牌能量狀態
return {
position,
card_zh: card.zh,
card_en: card.en,
symbol: card.symbol,
orientation: card.reversed ? '逆位' : '正位',
keywords: card.keywords,
interpretation,
energy: energyDesc
};
});
- 回傳每張牌的解讀物件(包含位置、牌名、符號、正逆位、關鍵字、解讀文字、能量描述)。
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 += '這張牌是你的核心指引,請用心感受它帶來的訊息。';
}
const advice = `${typeConfig.advicePool[Math.floor(Math.random() * typeConfig.advicePool.length)]} ` +
`塔羅建議你:${state.drawnCards[0].upright_core} ` +
`記住,牌卡只是指引,真正的選擇權永遠在你手中。`;
const closing = typeConfig.closingPool[Math.floor(Math.random() * typeConfig.closingPool.length)];
return { cards: cardsReading, synthesis, advice, closing };
}
- 生成行動建議(
advice):
- 隨機建議 + 第一張牌的正位核心牌義 + 提醒選擇權在自己
- 隨機選擇一則結語(
closing)
- 回傳完整解讀物件(
cards、synthesis、advice、closing)
渲染解讀結果(第 524–560 行左右)
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 字串,開始
.reading-section。
- 遍歷每張牌的解讀:
- 交替使用金色或紫色邊框(
border)
- 建立
.reading-card-block,包含:
- 牌義符號、牌名、正逆位、位置
- 解讀文字
- 關鍵字標籤
- 能量描述
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');
}
- 加入整體解讀區塊(
.synthesis-block)與行動建議(.advice-block)。
- 加入詩意結語(
data.closing)。
- 將 HTML 插入
#readingContent。
- 顯示重置區塊(
#restartBlock)。
重置函數(第 564–580 行左右)
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');
}
- 重置
state 為初始值。
- 清空問題輸入框與解讀內容。
- 隱藏載入動畫、錯誤訊息、重置區塊、解讀按鈕。
- 將牌陣選擇重置為「單張牌陣」(
.selected)。
- 隱藏 Step 3 與 Step 2,顯示 Step 1。
結尾(最後幾行)
</script>
</body>
</html>
- 結束
<script>、<body>、<html>。