iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
自我挑戰組

Laravel 讀碼源系列 第 4

解釋神秘塔羅占卜內容的code

  • 分享至 

  • xImage
  •  

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="例如:我在這段感情中應該如何走下一步?&#10;或:這次轉職機會對我是否適合?"></textarea>
  • #step1Panel:第一步面板。
  • 步驟指示器:三個圓點,第一個為 .active
  • 問題輸入區:textarea 帶有範例提示(&#10; 為換行字元)。
  <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
  • 回傳完整解讀物件(cardssynthesisadviceclosing

渲染解讀結果(第 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>

上一篇
算出神秘塔羅占卜內容的code
下一篇
算出本週幸運度調查內容
系列文
Laravel 讀碼源8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言