iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
自我挑戰組

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

Day 14:靈魂!雷達讓圖學會「對症下藥」

  • 分享至 

  • xImage
  •  

前言

上一篇讓雷達圖學會了慢慢長出來,診斷文字也會晚一點點才浮現,結果頁終於有了一點「正在分析」的儀式感。

寫完之後我自己重新整理頁面玩了好幾次,玩到第三、四次,突然發現一件很尷尬的事:

我根本沒在看那張雷達圖。

我只看下面那行小字:

你今天最突出的是「放鬆」,最需要留意的是「心態」

看完就關掉了。

所以前面算座標、調緩動曲線、處理負分,到底是在幹嘛?

雖然有點不甘心,但想想也合理。使用者想知道的應該不是「我的六角形長什麼樣子」,而是:

「所以我現在該怎麼辦?」

既然程式已經找得出使用者最匱乏的維度,那乾脆真的幫他開一張藥。

哪怕是唬爛的也好。反正我們是「賽博紓壓診所」,不是真的診所。


核心觀念:找出痛點,對症下藥

資料我們其實早就有了。userScores 從 Day 11 開始就一直忠實記錄著六個維度的分數,現在只要做兩件事:

  1. 找出分數最低的維度
  2. 用維度名稱去查對應的處方

第一件事,我這次想用 Object.entries() 搭配 reduce() 一行解決。

Object.entries() 會把物件轉成陣列:

// userScores 變成這樣
[["日常", 5], ["效率", 8], ["社交", 3], ["心態", -5], ["放鬆", 9], ["職涯", 4]]

然後 reduce() 的邏輯其實很單純:手上一直拿著「目前最小的那一組」,看到更小的就換手。跟排隊挑最矮的人沒什麼兩樣。

const weakestDimension = Object.entries(userScores).reduce((min, current) => {
    return current[1] < min[1] ? current : min;
})[0];

最後面那個 [0] 是只拿維度名稱,不拿分數。

有了名稱,第二件事就簡單了。準備一個「處方籤資料庫」,維度當 Key,唬爛建議當 Value:

const advice = prescriptionDB[weakestDimension];

查表一行搞定,完全不用寫六次 if-else。


動手寫 Code:處方籤系統

今天的範例跟之前一樣,把其他介面抽掉,只留下今天的重點。我直接模擬玩家「已經測驗完畢」的分數,刻意讓「心態」最低,然後看程式會開出什麼藥:

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>賽博紓壓診所:處方籤系統示範</title>
<style>
    body {
        font-family: 'Segoe UI', Microsoft JhengHei, sans-serif;
        background: #e3f2fd;
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 100vh;
        margin: 0;
        padding: 20px;
    }
    .card {
        background: white;
        padding: 30px;
        border-radius: 20px;
        box-shadow: 0 12px 30px rgba(0,0,0,0.15);
        width: 100%;
        max-width: 500px;
        text-align: center;
    }
    h2 { color: #1e88e5; margin-bottom: 20px; }

    .diagnosis-box {
        background: #e3f2fd;
        border-radius: 14px;
        padding: 16px;
        margin: 15px 0;
        font-size: 1rem;
        font-weight: bold;
        color: #1e88e5;
    }

    /* 惡搞處方籤:用虛線邊框刻出收據感,一開始先隱藏 */
    .prescription-box {
        background: #fdfdfd;
        border: 2px dashed #90caf9;
        border-radius: 12px;
        padding: 16px;
        margin-top: 15px;
        text-align: left;
        opacity: 0;
        transform: translateY(10px);
        transition: opacity 0.6s ease, transform 0.6s ease;
    }
    .prescription-box.show {
        opacity: 1;
        transform: translateY(0);
    }
    .prescription-box h3 {
        margin: 0 0 8px 0;
        color: #d32f2f;
        font-size: 1rem;
    }
    .prescription-box p {
        margin: 0;
        color: #263238;
        font-size: 0.9rem;
        line-height: 1.5;
    }

    .btn {
        background-color: #1e88e5;
        color: white;
        border: none;
        border-radius: 50px;
        padding: 12px 30px;
        font-size: 1rem;
        cursor: pointer;
        margin-top: 20px;
    }
    .btn:hover { background-color: #1565c0; }
</style>
</head>
<body>

    <div class="card">
        <h2>🏥 賽博病歷診斷報告</h2>

        <div class="diagnosis-box" id="diagnosis-text">分析中...</div>

        <!-- 處方籤容器,等 JS 開藥 -->
        <div class="prescription-box" id="prescription-container"></div>

        <button class="btn" onclick="location.reload()">重新掛號</button>
    </div>

    <script>
        // 模擬 Day 11 累積出來的六維度分數(刻意讓「心態」最低)
        const userScores = {
            "日常": 5,
            "效率": 8,
            "社交": 3,
            "心態": -5,
            "放鬆": 9,
            "職涯": 4
        };

        // 惡搞處方籤資料庫:一個維度對應一張藥
        const prescriptionDB = {
            "日常": "處方:今天不用早睡了,反正你也不會睡。去買杯手搖飲,全糖去冰,糖分才是良藥。",
            "效率": "處方:把手機關機兩小時,看著天花板發呆。這也是一種產出,只是產出是「無」。",
            "社交": "處方:取消今晚所有的局,你需要跟棉被獨處的 me time,誰傳訊息都當作沒看到。",
            "心態": "處方:對著鏡子大喊三聲「我已經很努力了」,然後去睡覺,明天的事明天再說。",
            "放鬆": "處方:去便利商店走一圈,什麼都不買,純欣賞架上的零食,療癒效果驚人。",
            "職涯": "處方:打開求職網站逛十分鐘,看看別人的職缺內容,你會突然覺得現在好像還行。"
        };

        // 找出最高分與最低分(診斷文字用)
        function showDiagnosis() {
            const entries = Object.entries(userScores);
            const top = entries.reduce((max, cur) => cur[1] > max[1] ? cur : max);
            const low = entries.reduce((min, cur) => cur[1] < min[1] ? cur : min);

            document.getElementById('diagnosis-text').innerText =
                `診斷結果:你今天最突出的是「${top[0]}」(${top[1]}分),最需要留意的是「${low[0]}」(${low[1]}分)`;
        }

        // 今天的重點:找出最弱維度,並開藥
        function generatePrescription() {
            const weakestDimension = Object.entries(userScores).reduce((min, current) => {
                return current[1] < min[1] ? current : min;
            })[0];

            const advice = prescriptionDB[weakestDimension];
            const box = document.getElementById('prescription-container');

            box.innerHTML = `
                <h3>💊 醫師處方籤(缺乏:${weakestDimension})</h3>
                <p>${advice}</p>
            `;

            // 稍微等一下再加上 show,transition 才會有動畫效果
            setTimeout(() => box.classList.add('show'), 50);
        }

        // 先顯示診斷,過 0.4 秒再開藥,讓畫面有先後順序
        window.onload = () => {
            showDiagnosis();
            setTimeout(generatePrescription, 400);
        };
    </script>
</body>
</html>

幾個我覺得值得說的小細節

1. 分數一樣低的時候怎麼辦?

current[1] < min[1] 用的是「小於」,不是「小於等於」。所以如果兩個維度同分,先出現的那個會留下來,後面的不會把它換掉。也就是說,遇到平手時,結果會固定偏向順序比較前面的維度,不會每次重新整理就亂跳。

2. 為什麼要在 setTimeout 裡面才加上 show?

如果 innerHTML 剛塞完內容就立刻 classList.add('show'),瀏覽器有時候會直接把最終狀態畫出來,中間的過渡動畫就不見了。稍微等 50 毫秒,讓瀏覽器先把「透明」的狀態畫過一次,再切換到「顯示」,transition 才會真的跑起來。

3. 為什麼診斷跟處方要錯開?

一開始我偷懶讓它們同時出現,結果資訊一次砸太多,根本來不及看完診斷就被處方籤搶走注意力。錯開 0.4 秒之後,感覺就像醫生先看完報告,才慢慢提筆開藥。


開發心得與設計考量

老實說,一開始看到 reduce() 我第一個反應是:「又來。」

這個函式我每次看到都要先當機兩秒。但這次硬著頭皮拆開來看,才發現它做的事很單純:手上永遠只留一個目前最小的,看到更小的就換手。理解之後,再看到它就沒那麼怕了。

另外一個很有感的地方,是前面資料結構的選擇。Day 11 決定用物件而不是六個獨立變數(dailyScore、efficiencyScore……)來存分數,當時只是覺得比較整齊,沒想到到了今天整批紅利一起兌現:

  • 找最低分:一個 reduce
  • 找最高分:一個 reduce
  • 查處方:一次物件存取
  • 未來想新增維度:處方庫多加一行就好

如果當初是六個獨立變數,今天光是比大小就要寫一堆 if-else,光想想就累www


今日結語與明日預告

今天我們用 Object.entries() 加上 reduce() 找出使用者分數最低的維度,再用物件查表開出專屬的惡搞處方籤,讓整個測驗從「算分數、畫圖」進化成「真的會給建議」。

不過玩到這裡,我又發現一個很現實的問題:

這張處方籤,重新整理就不見了。

如果使用者今天測完,明天又想回來看看自己這幾天的狀態有沒有變化呢?現在的診所完全沒有「記得」這件事的能力,每一次來都像第一次看診。

所以明天 Day 15,我們要來處理資料持久化:用瀏覽器的 localStorage,讓診所學會記住使用者的看診紀錄,就算重新整理頁面,甚至隔天再回來,病歷也不會不見。

明天,讓這間診所開始有「回診」這個概念。我們明天見!


上一篇
Day 13:讓雷達圖動起來:從靜態結果到真正的互動感
下一篇
Day 15:重新整理就失憶?讓診所學會「記住你」
系列文
賽博紓壓診所:從零打造純前端心理測驗與互動紓壓網頁 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言