新版簽到版建立!

2026/01/02

Categories: 技術/程式 Tags:

全文約 1552 字,預計閱讀 4 分鐘

之前的 guestbook 掛掉後,原作好像一直還沒處理,再加上看到廢文小天地的分享,覺得好像沒有太難就也做了一個。(並沒有,搞了兩小時,好難。)

我是單純只用了廢文小天地提到的版本一,比較單純,表格也是直接參照(抄襲)。

歡迎到簽到頁面來!只可惜以前留言的,就真的沒了QQ

不過現在放 google 雲端中,Eddie 有自動同步到 nas 就不用怕!

如果你打算也做一個,可以參照以下:

  1. 建立 Google sheet,每行標題爲:

留言時間 留言名稱 留言網址 留言內容 回覆名稱 回覆內容

  1. 在 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);
}
  1. 製作一個 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>
>> Home