上一篇讓雷達圖學會了慢慢長出來,診斷文字也會晚一點點才浮現,結果頁終於有了一點「正在分析」的儀式感。
寫完之後我自己重新整理頁面玩了好幾次,玩到第三、四次,突然發現一件很尷尬的事:
我根本沒在看那張雷達圖。
我只看下面那行小字:
你今天最突出的是「放鬆」,最需要留意的是「心態」
看完就關掉了。
所以前面算座標、調緩動曲線、處理負分,到底是在幹嘛?
雖然有點不甘心,但想想也合理。使用者想知道的應該不是「我的六角形長什麼樣子」,而是:
「所以我現在該怎麼辦?」
既然程式已經找得出使用者最匱乏的維度,那乾脆真的幫他開一張藥。
哪怕是唬爛的也好。反正我們是「賽博紓壓診所」,不是真的診所。
資料我們其實早就有了。userScores 從 Day 11 開始就一直忠實記錄著六個維度的分數,現在只要做兩件事:
第一件事,我這次想用 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。
今天的範例跟之前一樣,把其他介面抽掉,只留下今天的重點。我直接模擬玩家「已經測驗完畢」的分數,刻意讓「心態」最低,然後看程式會開出什麼藥:
<!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,讓診所學會記住使用者的看診紀錄,就算重新整理頁面,甚至隔天再回來,病歷也不會不見。
明天,讓這間診所開始有「回診」這個概念。我們明天見!