之前的 guestbook 掛掉後,原作好像一直還沒處理,再加上看到廢文小天地的分享,覺得好像沒有太難就也做了一個。(並沒有,搞了兩小時,好難。)
我是單純只用了廢文小天地提到的版本一,比較單純,表格也是直接參照(抄襲)。
歡迎到簽到頁面來!只可惜以前留言的,就真的沒了QQ
不過現在放 google 雲端中,Eddie 有自動同步到 nas 就不用怕!
如果你打算也做一個,可以參照以下:
- 建立 Google sheet,每行標題爲:
留言時間 留言名稱 留言網址 留言內容 回覆名稱 回覆內容
- 在 Google sheet 中點擊擴充功能,App Script,內容如下,部署爲網頁應用程式,此時會得到一個網址
function doPost(e) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
try {
// 取得 POST 過來的原始資料
const contents = e.postData.contents;
const data = JSON.parse(contents);
// 檢查有沒有必要欄位,如果沒有則不執行
if (!data.message) {
return ContentService.createTextOutput("No message").setMimeType(ContentService.MimeType.TEXT);
}
// 寫入資料
sheet.appendRow([
new Date(), // 留言時間
data.name || "匿名", // 留言名稱
data.url || "", // 留言網址
data.message, // 留言內容
"", // 回覆名稱 (留空)
"" // 回覆內容 (留空)
]);
// 回傳成功 (雖然 no-cors 模式下前端看不到回傳,但這能確保 GAS 正常結束)
return ContentService
.createTextOutput(JSON.stringify({ status: "ok" }))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
// 萬一出錯,把錯誤寫在試算表最後一列協助偵錯 (選做)
sheet.appendRow([new Date(), "ERROR", "", err.toString()]);
return ContentService
.createTextOutput(JSON.stringify({ status: "error", error: err.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
function doGet(e) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const rows = sheet.getDataRange().getValues();
// 移除標題列
rows.shift();
// 將陣列轉化為物件陣列,方便前端呼叫
const result = rows.map(r => {
return {
time: r[0],
name: r[1],
url: r[2],
message: r[3],
replyName: r[4],
replyContent: r[5]
};
});
return ContentService
.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
- 製作一個 content/guestbook.md,並貼上以下內容,css 部分再自己改一下。
<form id="guestbook">
<input id="name" placeholder="名字">
<p>
<input id="url" placeholder="網站(選填)">
<p>
<textarea id="message" placeholder="留言"></textarea>
<p>
<button type="button" id="send">送出</button>
</form>
<hr>
<ul id="messages"></ul>
<script>
// ⚠️ 請務必替換成你最新的 /exec 網址
const API = "https://script.google.com/macros/s/你的獨特網址/exec";
/**
* 載入留言邏輯
*/
async function loadMessages() {
const ul = document.getElementById("messages");
try {
const response = await fetch(API);
if (!response.ok) throw new Error("API 回應失敗");
const data = await response.json();
ul.innerHTML = "";
if (!data || data.length === 0) {
ul.innerHTML = "<li style='color: white;'>目前還沒有留言。</li>";
return;
}
data.reverse().forEach(r => {
// 相容性處理:判斷是物件還是陣列
const name = r.name || r[1] || "匿名";
const url = r.url || r[2] || "";
const message = r.message || r[3] || "";
const time = r.time || r[0] || "";
const replyName = r.replyName || r[4] || "";
const replyContent = r.replyContent || r[5] || "";
if (!message) return;
const li = document.createElement("li");
// 保持白字,移除邊框或使用深色邊框
li.style.cssText = "margin-bottom: 30px; list-style: none; border-bottom: 1px solid #444; padding-bottom: 20px; color: white;";
const dateStr = time ? new Date(time).toLocaleString() : "";
// 1. 處理訪客名稱超連結 (若有網址則顯示藍色或淺藍色連結,否則顯示白字粗體)
const displayName = url
? `<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer"; text-decoration: underline;">${escapeHtml(name)}</a>`
: `<strong style="color: white;">${escapeHtml(name)}</strong>`;
// 訪客留言內容 HTML
li.innerHTML = `
<div style="margin-bottom: 8px;">
${displayName}
<small style="color: #bbb; margin-left: 10px;">${dateStr}</small>
</div>
<div style="white-space: pre-wrap; line-height: 1.6; color: white;">${escapeHtml(message)}</div>
`;
// 2. 處理回覆區塊 (樣式與訪客一致,增加左側線條與縮排)
if (replyContent) {
const replyDiv = document.createElement("div");
replyDiv.style.cssText = `
margin-top: 15px;
margin-left: 30px;
padding-left: 15px;
border-left: 2px solid #666;
color: white;
`;
replyDiv.innerHTML = `
<div style="margin-bottom: 5px;">
<strong style="color: white;">${escapeHtml(replyName || "站長")}</strong>
<small style="color: #bbb; margin-left: 8px;">回覆:</small>
</div>
<div style="white-space: pre-wrap; line-height: 1.6; color: white;">${escapeHtml(replyContent)}</div>
`;
li.appendChild(replyDiv);
}
ul.appendChild(li);
});
} catch (err) {
console.error("載入失敗:", err);
ul.innerHTML = "<li style='color: white;'>載入留言失敗,請檢查網路。</li>";
}
}
/**
* 送出留言邏輯
*/
document.getElementById("send").addEventListener("click", async () => {
const btn = document.getElementById("send");
const name = document.getElementById("name").value.trim();
const url = document.getElementById("url").value.trim();
const message = document.getElementById("message").value.trim();
if (!message) return alert("請輸入內容");
btn.disabled = true;
btn.innerText = "送出中...";
try {
await fetch(API, {
method: "POST",
mode: "no-cors",
headers: { "Content-Type": "text/plain" },
body: JSON.stringify({ name: name || "匿名", url, message })
});
alert("留言已送出!");
location.reload();
} catch (err) {
alert("送出失敗");
btn.disabled = false;
btn.innerText = "送出";
}
});
/**
* 防止 XSS 攻擊
*/
function escapeHtml(str) {
if (!str) return "";
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
// 執行載入
loadMessages();
</script>