iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
自我挑戰組

賽博紓壓診所:從零打造純前端心理測驗與互動紓壓網頁系列 第 10 篇

Day 10:心理測驗核心題庫與動態生成(feat. 廟宇求籤系統)

  • 分享至 

  • xImage
  •  

前言
不知不覺鐵人賽已經推進到第 10 天了!三分之一的里程碑達成~

前面幾天我們把專案的外觀、互動音效和過場動畫都打磨得很漂亮,就像是把一間診所的裝潢跟門面都弄好了一樣。不過,既然這是一個「賽博紓壓診所與解碼器」,真正的靈魂當然是裡面的心理測驗本身。

一開始在構思測驗畫面的時候,最暴力的做法就是直接在 HTML 裡面寫 10 個 到 ,然後把它們隱藏起來慢慢切換。但只要想到之後如果要改題目、加選項或是調整順序,光是找對應的 HTML 標籤絕對會改到懷疑人生。

更重要的是,我不想讓這 10 題都只是無聊的「A, B, C」單選題!為了扣合這個專案的惡搞設定,我決定在 10 題裡面安插 2 到 3 題的「求籤筒」機制——你不只要回答問題,還要交給命運來抽出隨機選項!今天我們就用 JavaScript 來實現這套混搭風的動態題庫系統。

核心觀念:資料驅動畫面與條件渲染
與其把文字寫死在 HTML 裡,不如把 HTML 當成一個「空殼」,讓 JavaScript 把資料填進去。
我們只需要準備兩個東西:
1.題庫陣列(Array of Objects): 裡面包含題目文字、選項陣列,還要多加一個屬性 type。「一般」的題目設為 normal,「求籤」的題目設為 draw。
2.渲染函式(Render Function): 每次換題時,判斷這一題的 type 是什麼。如果是 normal,就乖乖印出 A, B, C 三個按鈕;如果是 draw,就先印出一顆大大的「搖籤筒」按鈕,點擊後再用亂數決定抽到哪一個選項!

這樣一來,不管題庫長怎樣,HTML 結構永遠只有乾淨的一組,後續維護起來簡直身心舒暢!

動手寫 Code:建置題庫陣列與動態渲染邏輯
為了聚焦在今天的核心,這段程式碼我們單獨抽出了「測驗畫面」來做獨立示範。裡面包含了一道一般情境題、一道求籤題,以及如何把它們印到畫面上並處理抽籤的完整邏輯:

    /* 一般選項按鈕樣式 */
    .option-btn { 
        background-color: #f5f5f5; 
        border: 2px solid transparent; 
        border-radius: 14px; 
        padding: 14px; 
        width: 100%; 
        margin-bottom: 12px; 
        text-align: left; 
        cursor: pointer; 
        transition: all 0.2s; 
        font-size: 0.95rem; 
        color: #263238;
    }
    .option-btn:hover { 
        border-color: #1e88e5; 
        transform: scale(1.02); 
        background-color: #e3f2fd; 
    }

    /* 搖籤筒專屬按鈕樣式 (已改為廟宇紅) */
    .draw-btn {
        background-color: #d32f2f; /* 經典廟宇紅 */
        color: white;
        border: none;
        border-radius: 50px;
        padding: 15px 40px;
        font-size: 1.2rem;
        cursor: pointer;
        transition: 0.2s;
        font-weight: bold;
        box-shadow: 0 4px 15px rgba(211, 47, 47, 0.4); /* 紅色光暈 */
    }
    .draw-btn:hover {
        background-color: #b71c1c; /* 移上去變深紅色 */
        transform: translateY(-2px);
    }
    
    /* 抽籤結果顯示區 */
    #draw-result {
        margin-top: 20px;
        font-size: 1.1rem;
        font-weight: bold;
        color: #d32f2f;
        min-height: 30px;
    }
</style>
<div class="card" id="quiz-screen">
    <!-- HTML 容器空殼 -->
    <span id="progress-text" class="progress">Question 1 of 10</span>
    <h2 id="question-text">題目載入中...</h2>
    <div id="options-container"></div>
    <div id="draw-result"></div>
</div>

<script>
    // 1. 建立題庫資料庫 (包含一般題型與求籤題型)
    const quizData = [
        {
            type: "normal",
            question: "早上鬧鐘響了,但你昨天熬夜趕專題,現在的你會:",
            options: [
                { text: "A. 直接按掉,催眠自己教授今天會請假", scores: { "日常": -2, "放鬆": 3 } },
                { text: "B. 痛苦地爬起來,靠三杯黑咖啡續命", scores: { "效率": 3, "心態": -2 } },
                { text: "C. 傳訊息跟同學求救「幫點名」", scores: { "社交": 2, "心態": 1 } }
            ]
        },
        {
            type: "draw",
            question: "來到廟宇,面對未知的死線,請誠心搖動籤筒抽出今日運勢:",
            options: [
                { text: "【大吉】教授突然宣布延期交件!", scores: { "放鬆": 5 } },
                { text: "【中吉】剛好找到神級開源程式碼,無痛套用。", scores: { "效率": 4 } },
                { text: "【大凶】電腦無預警死機,而且你沒存檔。", scores: { "心態": -5 } }
            ]
        }
    ];

    let currentQuestionIndex = 0;

    // 2. 動態渲染題目的核心函式
    function loadQuestion(index) {
        const qData = quizData[index];
        const optionsContainer = document.getElementById('options-container');
        const drawResult = document.getElementById('draw-result');
        
        // 更新進度與文字,並清空上一題的殘留內容
        document.getElementById('progress-text').innerText = `Question ${index + 1} of ${quizData.length}`;
        document.getElementById('question-text').innerText = qData.question;
        optionsContainer.innerHTML = ''; 
        drawResult.innerText = '';

        // 3. 判斷題型,決定要長出什麼按鈕
        if (qData.type === "normal") {
            // 一般題型:直接印出所有選項
            qData.options.forEach((opt) => {
                const btn = document.createElement('button');
                btn.className = 'option-btn';
                btn.innerText = opt.text;
                btn.onclick = () => handleSelectOption(opt.scores);
                optionsContainer.appendChild(btn);
            });
        } else if (qData.type === "draw") {
            // 求籤題型:印出一顆搖籤筒按鈕
            const drawBtn = document.createElement('button');
            drawBtn.className = 'draw-btn';
            drawBtn.innerText = '🎲 搖動籤筒';
            drawBtn.onclick = () => handleDraw(qData.options);
            optionsContainer.appendChild(drawBtn);
        }
    }

    // 處理求籤邏輯
    function handleDraw(options) {
        const drawResult = document.getElementById('draw-result');
        // 隨機選出一個選項
        const randomOption = options[Math.floor(Math.random() * options.length)];
        
        // 顯示抽籤結果,稍微停頓一下再跳下一題,製造驚喜感
        drawResult.innerText = `鏘鏘!你抽到了:\n${randomOption.text}`;
        
        setTimeout(() => {
            handleSelectOption(randomOption.scores);
        }, 2000); // 停頓 2 秒讓玩家看清楚結果
    }

    // 處理選項點擊與換題邏輯
    function handleSelectOption(scores) {
        if (currentQuestionIndex < quizData.length - 1) {
            currentQuestionIndex++;
            loadQuestion(currentQuestionIndex);
        } else {
            alert("測驗結束!準備進入結果報表。");
        }
    }

    // 網頁載入時立刻渲染第一題
    window.onload = () => {
        loadQuestion(currentQuestionIndex);
    };
</script>

開發心得與設計考量
在想這個專題時,我一直覺得如果只是一直點 A, B, C,做到第 5 題大概就會開始想打哈欠了。混入「求籤」這個機制,不僅很符合我們主打的紓壓風格,也把單向的心理測驗變成有點像小遊戲的體驗(惡言消滅器同理)。

技術層面上,雖然用 JavaScript 建立 DOM 元素(document.createElement())看起來程式碼稍微長了一點點,但在綁定點擊事件時真的非常乾淨,不會發生引號卡來卡去、字串串接錯誤的慘劇。而且只要維護好 quizData 這個陣列,想隨時擴充到 10 題甚至 20 題都輕鬆的很!

另外,大家應該有注意到我在資料裡預先埋了 scores 這個物件,這就是為了之後複雜的分析報表鋪路。每個人抽到的籤跟做的選擇,都會影響到不同維度的心理健康值。

今日結語與明日預告
今天我們寫出了整個專題的核心資料結構,成功讓 JavaScript 負責產出題目,還實作了充滿驚喜感的抽籤系統!

既然今天埋了 scores 的伏筆,明天 Day 11:維度計分機制:如何將零碎選項匯聚成六角雷達圖的數據,我們就要把這些點擊背後的分數真正「存起來」,建立一套多維度的累加系統,為之後的診斷報告做好萬全準備!我們明天見!


上一篇
Day 09:情緒點擊狂熱:實作「惡言消滅器」浮動元素與互動消滅
下一篇
Day 11:維度計分機制:如何將零碎選項匯聚成數據
系列文
賽博紓壓診所:從零打造純前端心理測驗與互動紓壓網頁 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言