iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
自我挑戰組

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

Day 26:出院通知書產生器,一份病歷四種語氣

  • 分享至 

  • xImage
  •  

前言

昨天讓大家可以把怨言丟上牆之後,我回頭去看了一圈市面上其他的心理測驗網站,想參考一下別人怎麼做「分享」這個功能。

結果看到的大部分都是同一套:一排社群圖示排排站,點下去全部複製同一段制式文字,差別只有連結網址後面接的平台名稱不一樣。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);
}

四個平台共用同一份診斷資料,輸出的文字卻完全不像出自同一個人手筆,這才是今天想做出來的效果。


動手寫 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: 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 承諾的「隨時隨地都能用」,總不能只活在我自己的電腦裡。我們明天見!**


上一篇
Day 25:開一面牆,讓大家一起吐苦水!
系列文
賽博紓壓診所:從零打造純前端心理測驗與互動紓壓網頁 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言