昨天讓大家可以把怨言丟上牆之後,我回頭去看了一圈市面上其他的心理測驗網站,想參考一下別人怎麼做「分享」這個功能。
結果看到的大部分都是同一套:一排社群圖示排排站,點下去全部複製同一段制式文字,差別只有連結網址後面接的平台名稱不一樣。FB 複製一次、IG 複製一次、LINE 複製一次,貼出去的內容幾乎一模一樣,頂多換一下 hashtag 數量。
我盯著那排一模一樣的圖示看了很久,覺得這樣有點可惜。
同一份診斷結果,貼到 Threads 跟貼到 FB,氣氛明明完全不同。Threads 上大家講話比較隨興,一句「耗損 87%,救命」就夠;FB 上長輩或老師可能也會看到,寫得太崩潰搞不好會被約談。
所以今天的分享功能,我想做得不一樣一點:
同一份診斷資料,根據要貼去哪個平台,自動換一套語氣,生成一張「出院通知書」。
這個想法其實延續了這幾天一直在重複的那個模式:資料跟呈現方式分開處理。
手上的診斷資料長這樣:
const diagnosis = {
drain: 72,
weakest: "心態",
advice: "對著鏡子大喊三聲「我已經很努力了」,然後去睡覺。"
};
接下來要做的,不是寫一段固定文案複製四次,而是準備四個「語氣模板」,每個模板吃同一份 diagnosis,吐出完全不同風格的文字:
const templates = {
ig: (d) => `🏥 今日賽博診斷\n電力剩餘 ${100 - d.drain}%\n#腦內電池 #${d.weakest}不夠用 #賽博紓壓診所`,
threads: (d) => `耗損 ${d.drain}%,${d.weakest}已陣亡 🫠`,
line: (d) => `欸我剛測完\n今天電池剩 ${100 - d.drain}%\n醫生說我「${d.weakest}」快不行了\n處方:${d.advice}`,
fb: (d) => `【賽博紓壓診所 官方出院通知書】\n本人今日接受心理壓力檢測,綜合評估電量剩餘 ${100 - d.drain}%,建議多加休息。`
};
IG 走標籤體,大量使用 hashtag;Threads 走迷因體,一句話帶梗就收尾;LINE 走閒聊體,像在跟朋友碎念;FB 走公告體,故意寫得一本正經,反差本身就是笑點。
使用者點哪個平台的按鈕,就套用哪個模板,產生對應的文字,再丟給 Clipboard API 複製:
function share(platform) {
const text = templates[platform](diagnosis);
navigator.clipboard.writeText(text);
}
四個平台共用同一份診斷資料,輸出的文字卻完全不像出自同一個人手筆,這才是今天想做出來的效果。
今天把四種語氣模板跟複製邏輯都做出來,點下平台按鈕可以直接預覽文案,也會嘗試開啟對應平台:
<!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: 25px;
border-radius: 20px;
box-shadow: 0 12px 30px rgba(0,0,0,0.15);
width: 100%;
max-width: 480px;
}
h2 { color: #1e88e5; margin: 0 0 6px; text-align: center; }
.subtitle { color: #546e7a; font-size: 0.85rem; text-align: center; margin-bottom: 18px; }
.diagnosis-box {
background: #e3f2fd;
border-radius: 14px;
padding: 14px;
font-size: 0.88rem;
color: #1e88e5;
margin-bottom: 18px;
line-height: 1.7;
}
.platform-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 16px;
}
.platform-btn {
background-color: #f5f5f5;
border: 2px solid transparent;
border-radius: 12px;
padding: 12px;
font-size: 0.9rem;
cursor: pointer;
color: #263238;
text-align: left;
transition: 0.2s;
}
.platform-btn:hover { border-color: #1e88e5; background: #e3f2fd; }
.platform-btn .tone { display: block; font-size: 0.7rem; color: #90a4ae; margin-top: 2px; }
.preview-box {
background: #fdfdfd;
border: 2px dashed #90caf9;
border-radius: 12px;
padding: 14px;
font-size: 0.85rem;
color: #263238;
white-space: pre-line;
min-height: 60px;
margin-bottom: 12px;
}
.toast {
text-align: center;
font-size: 0.8rem;
color: #2e7d32;
min-height: 20px;
}
</style>
</head>
<body>
<div class="card">
<h2>📋 出院通知書產生器</h2>
<p class="subtitle">同一份診斷,四種語氣,自己挑一種出院</p>
<div class="diagnosis-box">
🔋 電力剩餘:<b>28%</b><br>
⚠️ 最缺:<b>心態</b><br>
💊 處方:對著鏡子大喊三聲「我已經很努力了」,然後去睡覺
</div>
<!-- 今天的重點:同一份資料,四個按鈕對應四種語氣模板 -->
<div class="platform-grid">
<button class="platform-btn" onclick="share('ig')">
Instagram
<span class="tone">標籤體 · 大量 hashtag</span>
</button>
<button class="platform-btn" onclick="share('threads')">
Threads
<span class="tone">迷因體 · 一句話帶梗</span>
</button>
<button class="platform-btn" onclick="share('line')">
LINE
<span class="tone">閒聊體 · 像在跟朋友碎念</span>
</button>
<button class="platform-btn" onclick="share('fb')">
Facebook
<span class="tone">公告體 · 一本正經的反差</span>
</button>
</div>
<div class="preview-box" id="preview">選一個平台,看看文案長什麼樣</div>
<div class="toast" id="toast"></div>
</div>
<script>
// 固定的示範診斷資料
const diagnosis = {
drain: 72,
weakest: "心態",
advice: "對著鏡子大喊三聲「我已經很努力了」,然後去睡覺"
};
// ---------- 四種語氣模板,吃同一份資料,長出完全不同的文字 ----------
const templates = {
ig: (d) => `🏥 今日賽博診斷\n電力剩餘 ${100 - d.drain}%\n#腦內電池 #${d.weakest}不夠用 #賽博紓壓診所`,
threads: (d) => `耗損 ${d.drain}%,${d.weakest}已陣亡 🫠`,
line: (d) => `欸我剛測完\n今天電池剩 ${100 - d.drain}%\n醫生說我「${d.weakest}」快不行了\n處方:${d.advice}`,
fb: (d) => `【賽博紓壓診所 官方出院通知書】\n本人今日接受心理壓力檢測,綜合評估電量剩餘 ${100 - d.drain}%。\n建議多加休息,避免過度操勞。`
};
// 各平台能不能真的「預填文字」直接開啟分享視窗,各自規則不一樣
const platformLinks = {
// LINE 的網址格式支援把文字塞進訊息欄,這個算幸運兒
line: (text) => `https://line.me/R/msg/text/?${encodeURIComponent(text)}`,
// FB 的分享對話框只吃網址,塞自訂文字早就被官方拔掉了,開啟後還是要自己貼
fb: () => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(location.href)}`
// IG、Threads 完全沒有網頁版的預填管道,只能複製文字、請使用者自己去貼
};
async function share(platform) {
const text = templates[platform](diagnosis);
document.getElementById("preview").innerText = text;
const copied = await copyToClipboard(text);
showToast(copied ? "文案已複製,貼上去就能發了" : "複製失敗,請手動選取文字");
if (platformLinks[platform]) {
window.open(platformLinks[platform](text), "_blank");
} else if (platform === "ig" || platform === "threads") {
// 這兩個平台沒有網頁版的預填管道,只能提醒使用者自己貼
showToast("文案已複製,打開 " + (platform === "ig" ? "Instagram" : "Threads") + " 貼上即可");
}
}
// ---------- Clipboard API,順便處理不支援的情況 ----------
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (e) {
// 舊瀏覽器或權限被擋掉的時候,退回古老的 execCommand 做法
try {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
return true;
} catch (e2) {
return false;
}
}
}
function showToast(msg) {
const toast = document.getElementById("toast");
toast.innerText = msg;
setTimeout(() => { toast.innerText = ""; }, 3000);
}
</script>
</body>
</html>
再存成 .html 打開,四個平台按鈕輪流點點看,預覽欄會跳出完全不同語氣的文案,LINE 甚至會直接幫你開好分享視窗、文字都填好了,IG 跟 Threads 則是複製完提醒你自己去貼。
四個平台裡,只有 LINE 真的支援網址預填文字
一開始我以為每個社群平台都會提供一個「分享連結+夾帶文字」的網址格式,結果查了才發現只有 LINE 的 line.me/R/msg/text/ 這組網址真的吃得下文字參數。FB 的分享對話框很多年前就把自訂文字那個參數拔掉了,只認網址;IG 跟 Threads 根本沒有對應的網頁版分享連結,純網頁應用完全碰不到。這個限制只能誠實接受,複製文字、開啟 App、請使用者自己貼,是目前前端能做到的極限。
navigator.clipboard.writeText() 不是每個環境都能用
這個 API 需要 HTTPS 環境,而且某些瀏覽器設定下會直接被權限擋掉,呼叫會丟出例外。所以外面包了一層 try...catch,失敗的話退回去用更老派的 document.execCommand("copy"),靠一個隱藏的 textarea 元素土法煉鋼完成複製,雙重保險下至少不會讓整個分享功能直接掛掉。
四個模板刻意不共用同一句開場白
一開始偷懶想說四個模板只是「換個句子結構」,後來發現如果開頭都用「🏥 今日賽博診斷」,看起來還是太像同一個人寫的。刻意讓 LINE 版本用「欸我剛測完」這種口語開場,FB 版本用「本人今日接受」這種公文語氣開場,語氣的差異感才真正出來,不只是排版不一樣而已。
分享功能也能是「一份資料、多種詮釋」的延伸
做到這裡才發現,昨天發洩牆的「同一份陣列、兩種渲染模式」,跟今天「同一份診斷資料、四種語氣模板」,其實是同一種思路換了個場景。這個模式在這系列裡出現得越來越自然,已經變成我寫新功能時的預設思考方式:先問資料長什麼樣,再問要用幾種方式呈現它。
誠實面對平台限制,比硬做一個假的「一鍵分享」更有價值
查資料的過程中,發現很多做法是硬要用 window.open 開一個假的分享對話框,結果點進去還是一片空白,使用者反而更困惑。與其硬凹出一個看起來完整、實際上沒用的按鈕,不如老實告訴使用者「這裡只能複製,請自己去貼」,體驗上反而更可靠。
語氣模板這個設計,之後想繼續擴充
目前只做了四個平台,但這個 templates 物件的結構,之後想加「Discord 嗆爆體」或「Email 正式申訴體」都只要多加一個 function,完全不影響既有的程式碼。這種可以隨意長出新分支的結構,寫起來特別有成就感。
今天我們把分享功能做得不太一樣,讓同一份診斷資料套上四種完全不同的語氣模板,變成一張可以依照平台調整口氣的出院通知書,順便老實面對了 IG、Threads、FB 在網頁端沒辦法預填文字的現實限制,用 Clipboard API 跟舊版相容的 execCommand 補上這個缺口。
分享功能做出來之後,我拿去手機上試了一輪,結果發現事情沒有想像中順利。
某支舊手機點按鈕完全沒反應,後來才查到是因為那支手機的瀏覽器設定下 Clipboard API 直接被鎖住;另一支手機則是進站之後要先點兩下畫面,音效才願意發出來,跟電腦上的體驗完全不一樣。
這讓我回想起 Day 1 寫下那篇初衷文的時候,心裡想的其實是「做一個大家都能用的紓壓小工具」。從那天到現在功能是一天比一天多,但我幾乎都只在自己電腦上的瀏覽器開發跟測試,從來沒有真正停下來檢查過:**這些程式碼現在到底有多亂、手機上到底跑不跑得動、畫面有沒有偷偷在卡頓。
**
這三件事湊在一起,剛好是收尾前該做的一次大掃除。
所以明天 Day 27,打算一次處理三件事:把這段時間東拼西湊、散落各處的 JavaScript 跟 CSS 做一次程式碼重構,整理成比較模組化的結構;順手把手機跟不同瀏覽器的相容性問題抓出來,尤其是觸控操作跟 Web Audio、Clipboard API 在 iOS Safari 上的各種奇怪限制;最後再看看畫面有沒有不必要的抖動,把 DOM 渲染的效能調一調。
**
畢竟 Day 1 承諾的「隨時隨地都能用」,總不能只活在我自己的電腦裡。我們明天見!**