前言 #
在我生活向部落格中的這篇貼文 中,我想直接在文內顯示我跟朋友的對話內容。我可以直接截圖放上去,但一方面我想能不放圖片就不放圖片,避免增加網站的重量,另一方面我跟朋友的對話要是要發上來應該要一直打馬賽克,於是我以hugo stack 主题美化 這篇文章提到的「聊天氣泡 Shortcode」為基礎,透過 Perplexity 協助編寫並做了許多修改, 完成了這個 Shortcode,具體來說是兩個,以下會說明。
功能 #
- 自動配合深淺色主題
- 全參數可自訂
- 包含兩個 Shortcode(聊天視窗與對話框)
- 對話框 Shortcode 可單獨使用
- 可折疊聊天視窗
- 游標懸浮 Focus
- 多語言網站外觀文字翻譯
創建 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") }} {{ 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") }} {{ 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 樣式
完整使用範例 #
-
完整功能
對話框標題+訊息數量+日期+訊息時間+名字{{< 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 則訊息 -
隱藏對話框 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 >}} -
純訊息
{{< collapsible-chatbox >}} {{< chat position="left" >}} 你好 {{< /chat >}} {{< chat position="right" >}} 你好 {{< /chat >}} {{< /collapsible-chatbox >}} -
單獨使用對話框 Shortcode
{{< chat position="left" >}} 你好 {{< /chat >}} {{< chat position="right" >}} 你好 {{< /chat >}}你好你好
可自訂參數 #
-
collapsible-chatbox參數參數 類型 必填 預設值 說明 title字串 否 無 聊天容器標題 count字串/數字 否 無 訊息數量(如 “8”) -
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: 收起訊息