快轉到主要內容
  1. 全部文章/

Hugo 可折疊聊天視窗短代碼 Shortcode

·3315 字·7 分鐘
目錄
Hugo Shortcode - 本文屬於一個選集。
§ 2: 本文

前言
#

在我生活向部落格中的這篇貼文 中,我想直接在文內顯示我跟朋友的對話內容。我可以直接截圖放上去,但一方面我想能不放圖片就不放圖片,避免增加網站的重量,另一方面我跟朋友的對話要是要發上來應該要一直打馬賽克,於是我以hugo stack 主题美化 這篇文章提到的「聊天氣泡 Shortcode」為基礎,透過 Perplexity 協助編寫並做了許多修改, 完成了這個 Shortcode,具體來說是兩個,以下會說明。


功能
#

  1. 自動配合深淺色主題
  2. 全參數可自訂
  3. 包含兩個 Shortcode(聊天視窗與對話框)
  4. 對話框 Shortcode 可單獨使用
  5. 可折疊聊天視窗
  6. 游標懸浮 Focus
  7. 多語言網站外觀文字翻譯

創建 Shortcode 短代碼
#

可折疊聊天視窗
#

/layouts/shortcodes路徑下創建collapsible-chatbox.html

<div class="ccb-container" data-collapsible="true">
    {{ if or (.Get "title") (.Get "count") }}
    <div class="ccb-container__header">
        {{ if .Get "title" }}
        <span class="ccb-container__title">{{ .Get "title" }}</span>
        {{ end }}
        {{ if .Get "count" }}
        <span class="ccb-container__count">{{ .Get "count" }} 則訊息</span>
        {{ end }}
    </div>
    {{ end }}
    <div class="ccb-messages">
        {{ .Inner }}
    </div>
</div>

{{/* 使用 partial 載入 JavaScript,防止重複載入 */}}
{{ partial "prevent-js-reload.html" (dict "Page" .Page "id" "collapsible-chatbox" "src" "js/collapsible-chatbox.js") }}

對話框
#

一樣在/layouts/shortcodes路徑下創建chat.html

{{ if eq (.Get "position") "left" }}
<div class="ccb-chat ccb-chat--other">
    <div class="ccb-chat__inner">
        {{ if .Get "date" }}
        <div class="ccb-chat__date">{{ .Get "date" }}</div>
        {{ end }}
        {{ if or (.Get "name") (.Get "timestamp") }}
        <div class="ccb-chat__meta">{{ .Get "name" }}{{ if and (.Get "name") (.Get "timestamp") }}&nbsp;&nbsp;&nbsp;{{ end }}{{ .Get "timestamp" }}</div>
        {{ end }}
        <div class="ccb-chat__text">
            {{ .Inner }}
        </div>
    </div>
</div>
{{ else if eq (.Get "position") "right" }}
<div class="ccb-chat ccb-chat--self">
    <div class="ccb-chat__inner">
        {{ if .Get "date" }}
        <div class="ccb-chat__date">{{ .Get "date" }}</div>
        {{ end }}
        {{ if or (.Get "name") (.Get "timestamp") }}
        <div class="ccb-chat__meta" style="text-align: right;">{{ .Get "timestamp" }}{{ if and (.Get "name") (.Get "timestamp") }}&nbsp;&nbsp;&nbsp;{{ end }}{{ .Get "name" }}</div>
        {{ end }}
        <div class="ccb-chat__text">
            {{ .Inner }}
        </div>
    </div>
</div>
{{ end }}

創建 Partial 檔案
#

參考我先前的文章「Hugo Partial範本防止JavaScript重複載入 」。


創建 JavaScript
#

手動建立或找到/static/js路徑,並新增collapsible-chatbox.js

// Collapsible Chatbox
(function() {
    'use strict';
    
    // 防止重複初始化
    if (window.CollapsibleChatboxInitialized) {
        return;
    }
    window.CollapsibleChatboxInitialized = true;

    function initCollapsibleChatbox() {
        // 選取所有可折疊的聊天容器
        const chatContainers = document.querySelectorAll('.ccb-container[data-collapsible="true"]');

        chatContainers.forEach(function(container) {
            // 避免重複初始化
            if (container.classList.contains('ccb-initialized')) {
                return;
            }
            container.classList.add('ccb-initialized');

            const messagesWrapper = container.querySelector('.ccb-messages');
            
            // 檢查高度是否超過 300px
            const contentHeight = messagesWrapper.scrollHeight;

            if (contentHeight > 300) {
                // 加上折疊樣式
                container.classList.add('ccb-collapsible');

                // 創建漸層遮罩
                const gradient = document.createElement('div');
                gradient.className = 'ccb-gradient';
                messagesWrapper.appendChild(gradient);

                // 創建展開按鈕
                const btn = document.createElement('button');
                btn.className = 'ccb-expand-btn';
                btn.innerHTML = '<span class="ccb-expand-text">展開訊息</span><span class="ccb-collapse-text" style="display:none;">收起訊息</span>';

                // 插入按鈕到容器內
                container.appendChild(btn);

                // 按鈕點擊事件
                btn.addEventListener('click', function() {
                    const expandText = btn.querySelector('.ccb-expand-text');
                    const collapseText = btn.querySelector('.ccb-collapse-text');

                    if (container.classList.contains('ccb-collapsible')) {
                        // 展開
                        container.classList.remove('ccb-collapsible');
                        container.classList.add('ccb-expanded');
                        expandText.style.display = 'none';
                        collapseText.style.display = 'inline';
                    } else {
                        // 收起
                        container.classList.remove('ccb-expanded');
                        container.classList.add('ccb-collapsible');
                        expandText.style.display = 'inline';
                        collapseText.style.display = 'none';
                        // 平滑滾動回容器頂部
                        container.scrollIntoView({ behavior: 'smooth', block: 'start' });
                    }
                });
            }
        });
    }

    // DOM 載入完成後執行
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initCollapsibleChatbox);
    } else {
        // DOM 已經載入完成
        initCollapsibleChatbox();
    }
})();

加入 CSS 樣式
#

編輯/assets/css路徑下的custom.css,加入以下樣式:

/* ===== Collapsible Chatbox (CCB) 專用樣式 ===== */

:root {
    /* CCB 淺色模式變數 */
    --ccb-bg: #f5f5f5;
    --ccb-surface: #ffffff;
    --ccb-text: #000000;
    --ccb-text-secondary: #707070;
    --ccb-primary: rgba(0, 0, 0, 0.06);
    --ccb-primary-hover: rgba(0, 0, 0, 0.1);
    --ccb-self-bg: #b5e36d;
    --ccb-other-bg: #e9e9eb;
    --ccb-border: rgba(0, 0, 0, 0.1);
    --ccb-gradient-end: #ffffff;
    --ccb-container-bg: #ffffff;
    --ccb-container-shadow: rgba(0, 0, 0, 0.1);
}

.dark {
    /* CCB 深色模式變數 */
    --ccb-bg: #1a1a1a;
    --ccb-surface: #2a2a2a;
    --ccb-text: #ffffff;
    --ccb-text-secondary: #b1b1b1;
    --ccb-primary: rgba(255, 255, 255, 0.1);
    --ccb-primary-hover: rgba(255, 255, 255, 0.15);
    --ccb-self-bg: #b5e36d;
    --ccb-other-bg: #3a3a3a;
    --ccb-border: rgba(255, 255, 255, 0.1);
    --ccb-gradient-end: #2a2a2a;
    --ccb-container-bg: #2a2a2a;
    --ccb-container-shadow: rgba(0, 0, 0, 0.3);
}

/* ===== 聊天容器樣式 ===== */
.ccb-container {
    background-color: var(--ccb-container-bg);
    border-radius: 15px;
    padding: 20px;
    margin-bottom: 30px;
    box-shadow: 0 2px 10px var(--ccb-container-shadow);
    position: relative;
    overflow: hidden;
    transition: all 0.3s ease;
}

/* 聊天容器標題 */
.ccb-container__header {
    display: flex;
    align-items: center;
    padding-bottom: 15px;
    border-bottom: 1px solid var(--ccb-border);
    margin-bottom: 15px;
}

.ccb-container__title {
    font-size: 16px;
    font-weight: 600;
    color: var(--ccb-text);
    flex: 1;
}

.ccb-container__count {
    font-size: 12px;
    color: var(--ccb-text-secondary);
    background: var(--ccb-bg);
    padding: 4px 12px;
    border-radius: 12px;
}

/* 聊天訊息列表 */
.ccb-messages {
    position: relative;
}

/* 折疊狀態 */
.ccb-container.ccb-collapsible .ccb-messages {
    max-height: 300px;
    overflow: hidden;
    transition: max-height 0.3s ease;
}

/* 展開狀態 */
.ccb-container.ccb-expanded .ccb-messages {
    max-height: none;
}

/* 漸層遮罩 */
.ccb-gradient {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 100px;
    background: linear-gradient(to bottom, transparent, var(--ccb-gradient-end));
    pointer-events: none;
    z-index: 2;
    opacity: 1;
    transition: opacity 0.3s ease;
}

.ccb-container.ccb-expanded .ccb-gradient {
    opacity: 0;
}

/* 展開按鈕 */
.ccb-expand-btn {
    display: block;
    width: 100%;
    padding: 12px;
    margin-top: 15px;
    background: var(--ccb-primary);
    color: var(--ccb-text);
    border: 1px solid var(--ccb-border);
    border-radius: 10px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 500;
    text-align: center;
    transition: all 0.2s;
}

.ccb-expand-btn:hover {
    background: var(--ccb-primary-hover);
}

.ccb-expand-text,
.ccb-collapse-text {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

/* ===== 聊天訊息樣式 ===== */
.ccb-chat {
    margin: 10px 0;
    padding: 10px;
    position: relative;
    transition: transform 0.2s;
    max-width: 80%;
    min-width: 15%;
}

.ccb-chat:hover {
    transform: scale(1.02);
}

/* 右側對話框(自己) */
.ccb-chat--self {
    text-align: left;
    background-color: var(--ccb-self-bg);
    color: var(--ccb-text);
    border-radius: 15px;
    width: fit-content;
    margin-left: auto;
}

.ccb-chat--self::before {
    content: "";
    position: absolute;
    right: -18px;
    bottom: 5px;
    transform: translateY(-50%);
    border-width: 15px 0 0 20px;
    border-style: solid;
    border-color: transparent transparent transparent var(--ccb-self-bg);
}

/* 左側對話框(其他人) */
.ccb-chat--other {
    text-align: left;
    background-color: var(--ccb-other-bg);
    color: var(--ccb-text);
    border-radius: 15px;
    position: relative;
    width: fit-content;
}

.ccb-chat--other::before {
    content: "";
    position: absolute;
    left: -18px;
    bottom: 5px;
    transform: translateY(-50%);
    border-width: 15px 20px 0 0;
    border-style: solid;
    border-color: transparent var(--ccb-other-bg) transparent transparent;
}

/* 消息元數據樣式(名稱和時間戳) */
.ccb-chat__meta {
    font-weight: bold;
    font-size: 0.67em;
    color: var(--ccb-text-secondary);
    margin-bottom: 5px;
}

.ccb-chat--self .ccb-chat__meta {
    text-align: right;
    color: #707070;
}

/* 日期顯示樣式 */
.ccb-chat__date {
    font-size: 0.6em;
    color: #707070;
    margin-bottom: 3px;
    opacity: 0.8;
}

.ccb-chat--self .ccb-chat__date {
    text-align: right;
}

/* 消息文本樣式 */
.ccb-chat__text {
    font-size: 0.9em;
    margin-left: 10px;
    word-break: break-word;
    color: #000000;
}

/* 深色模式下對方的訊息文字顏色 */
.dark .ccb-chat--other .ccb-chat__text {
    color: #ffffff;
}

/* 響應式設計 */
@media (max-width: 600px) {
    .ccb-chat {
        max-width: 85%;
    }
}

檔案結構
#

你的網站目錄/
├── layouts/
│   ├── partials/
│   │   └── prevent-js-reload.html       ← 防重複載入
│   └── shortcodes/
│       ├── collapsible-chatbox.html    ← 聊天視窗
│       └── chat.html                    ← 對話框
├── static/
│   └── js/
│       └── collapsible-chatbox.js      ← JavaScript
└── assets/
    └── css/
        └── custom.css                   ← CSS 樣式

完整使用範例
#

  1. 完整功能
    對話框標題+訊息數量+日期+訊息時間+名字

    {{< collapsible-chatbox title="對話框" count="4" >}}
    {{< chat position="left" name="小明" timestamp="00:00 date="2025年11月2日" >}}
    你好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好
    {{< /chat >}}
    
    {{< chat position="right" name="我" timestamp="01:00" date="2025年11月2日" >}}
    你好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好
    {{< /chat >}}
    
    {{</* chat position="right" name="我" timestamp="01:00" date="2025年11月2日" >}}
    晚安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安
    {{< /chat >}}
    
    {{< chat position="right" name="小明" timestamp="01:30" date="2025年11月2日" >}}
    晚安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安
    {{< /chat >}}
    {{< /collapsible-chatbox >}}
    
    對話框 4 則訊息
    2025年11月2日
    小明   00:00
    你好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好
    2025年11月2日
    01:00   我
    你好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好好
    2025年11月2日
    我   01:00
    晚安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安
    2025年11月2日
    01:30   小明
    晚安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安安
  2. 隱藏對話框 Header

    {{< collapsible-chatbox >}}
    {{< chat position="left" name="我" timestamp="00:00" date="2025年11月2日" >}}
    你好
    {{< /chat>}}
    
    {{< chat position="right" name="小明" timestamp="01:00" date="2025年11月2日" >}}
    你好
    {{< /chat >}}
    {{< /collapsible-chatbox >}}
    

    2025年11月2日
    我   00:00
    你好
    2025年11月2日
    01:00   小明
    你好
  3. 純訊息

    {{< collapsible-chatbox >}}
    {{< chat position="left" >}}
    你好
    {{< /chat >}}
    
    {{< chat position="right" >}}
    你好
    {{< /chat >}}
    {{< /collapsible-chatbox >}}
    

    你好
    你好
  4. 單獨使用對話框 Shortcode

    {{< chat position="left" >}}
    你好
    {{< /chat >}}
    
    {{< chat position="right" >}}
    你好
    {{< /chat >}}
    
    你好
    你好

可自訂參數
#

  1. collapsible-chatbox 參數

    參數 類型 必填 預設值 說明
    title 字串 聊天容器標題
    count 字串/數字 訊息數量(如 “8”)
  2. chat 參數

    參數 類型 必填 預設值 說明
    position 字串 "left""right"
    name 字串 發信人名稱
    timestamp 字串 時間(如 “00:00”)
    date 字串 日期(如 “2025年11月2日”)

多語言網站外觀文字翻譯
#

將折疊式聊天視窗 Partial 範本collapsible-chatbox.html改為:

<div class="ccb-container" data-collapsible="true">
    {{ if or (.Get "title") (.Get "count") }}
    <div class="ccb-container__header">
        {{ if .Get "title" }}
        <span class="ccb-container__title">{{ .Get "title" }}</span>
        {{ end }}
        {{ if .Get "count" }}
        <span class="ccb-container__count">{{ .Get "count" }} {{ i18n "shortcode.collapsible-chatbox.messages" }}</span>
        {{ end }}
    </div>
    {{ end }}
    <div class="ccb-messages">
        {{ .Inner }}
    </div>
</div>

{{ if not (.Page.Scratch.Get "ccb-js-loaded") }}
  {{ .Page.Scratch.Set "ccb-js-loaded" true }}
  <script>
    document.documentElement.dataset.ccbExpandText = "{{ i18n "shortcode.collapsible-chatbox.expand" }}";
    document.documentElement.dataset.ccbCollapseText = "{{ i18n "shortcode.collapsible-chatbox.collapse" }}";
  </script>
  <script src="{{ "js/collapsible-chatbox.js" | relURL }}" defer></script>
{{ end }}

collapsible-chatbox.js改為:

// Collapsible Chatbox
(function() {
    'use strict';
    
    // 防止重複初始化
    if (window.CollapsibleChatboxInitialized) {
        return;
    }
    window.CollapsibleChatboxInitialized = true;

    function initCollapsibleChatbox() {
        // 從 data 屬性取得翻譯文字
        const expandText = document.documentElement.dataset.ccbExpandText || 'Expand Messages';
        const collapseText = document.documentElement.dataset.ccbCollapseText || 'Collapse Messages';

        // 選取所有可折疊的聊天容器
        const chatContainers = document.querySelectorAll('.ccb-container[data-collapsible="true"]');

        chatContainers.forEach(function(container) {
            // 避免重複初始化
            if (container.classList.contains('ccb-initialized')) {
                return;
            }
            container.classList.add('ccb-initialized');

            const messagesWrapper = container.querySelector('.ccb-messages');
            
            // 檢查高度是否超過 300px
            const contentHeight = messagesWrapper.scrollHeight;

            if (contentHeight > 300) {
                // 加上折疊樣式
                container.classList.add('ccb-collapsible');

                // 創建漸層遮罩
                const gradient = document.createElement('div');
                gradient.className = 'ccb-gradient';
                messagesWrapper.appendChild(gradient);

                // 創建展開按鈕(使用翻譯文字)
                const btn = document.createElement('button');
                btn.className = 'ccb-expand-btn';
                btn.innerHTML = '<span class="ccb-expand-text">' + expandText + '</span><span class="ccb-collapse-text" style="display:none;">' + collapseText + '</span>';

                // 插入按鈕到容器內
                container.appendChild(btn);

                // 按鈕點擊事件
                btn.addEventListener('click', function() {
                    const expandTextEl = btn.querySelector('.ccb-expand-text');
                    const collapseTextEl = btn.querySelector('.ccb-collapse-text');

                    if (container.classList.contains('ccb-collapsible')) {
                        // 展開
                        container.classList.remove('ccb-collapsible');
                        container.classList.add('ccb-expanded');
                        expandTextEl.style.display = 'none';
                        collapseTextEl.style.display = 'inline';
                    } else {
                        // 收起
                        container.classList.remove('ccb-expanded');
                        container.classList.add('ccb-collapsible');
                        expandTextEl.style.display = 'inline';
                        collapseTextEl.style.display = 'none';
                        // 平滑滾動回容器頂部
                        container.scrollIntoView({ behavior: 'smooth', block: 'start' });
                    }
                });
            }
        });
    }

    // DOM 載入完成後執行
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initCollapsibleChatbox);
    } else {
        // DOM 已經載入完成
        initCollapsibleChatbox();
    }
})();

最後修改/i18n路徑下的設定檔,以zh-TW.yaml為例,在其中添加:

shortcode:
  collapsible-chatbox:
    messages: 則訊息
    expand: 查看更多訊息
    collapse: 收起訊息
透過郵件回覆
YoZ 柚子
作者
YoZ 柚子
韓國實用音樂系留學生/FOSS Nerd/ISTJ/襯衫、墨鏡愛好者
Hugo Shortcode - 本文屬於一個選集。
§ 2: 本文

相關文章