2025年10月27日 星期一

【手把手教學】打造即時更新的 Google 試算表連結系統(Apps Script + HTML 應用)

 

【手把手教學】打造即時更新的 Google 試算表連結系統(Apps Script + HTML 應用)

您是否厭倦了每次網站連結有變動,都必須手動修改網頁程式碼?本教學將教您如何利用 Google 試算表作為後端資料庫,結合 Google Apps Script 和前端 HTML/JavaScript 技術,打造一個無需修改程式碼,只需更新試算表內容,網頁連結清單即可即時更新的活動相簿或資源連結系統。

🎯 最終成果預覽

您將會得到一個獨立的 HTML 網頁(可儲存於本機或您的網站空間),它能:

  • 自動讀取 Google 試算表中的「序號」、「相簿名稱」和「相簿網址」。

  • 按照「序號」排序顯示連結清單。

  • 支援左右翻頁,每頁顯示 10 個連結。

  • 具備 RWD 響應式網頁設計,在電腦和手機上都能完美顯示(電腦為兩欄,手機為單欄)。

  • 無需伺服器,完全免費

呈現方式(鹿陽國小網站)

鹿陽活動相簿



🛠️ 準備工作

  1. Google 帳號: 這是所有 Google 服務的基礎。

  2. Google 試算表: 用來儲存連結資料。

  3. Google Apps Script: 用來將試算表資料轉換成網頁可讀取的 JSON 格式。


步驟一:設定 Google 試算表資料庫

首先,我們需要一個結構化的資料來源。

  1. 建立試算表: 新建一個 Google 試算表,並將第一列設定為標題列。

  2. 設定欄位: 建立以下三個欄位:

    • A 欄:序號 (必須為數字):用於控制連結的顯示順序。

    • B 欄:相簿名稱:顯示在網頁上的連結文字。

    • C 欄:相簿網址:點擊後要前往的目標網址。

序號相簿名稱相簿網址
120250812_校園相片https://photos.app.goo.gl/...
2鹿陽校園https://photos.app.goo.gl/...
1120250917_班親會google.com (若無 https:// 會自動補上)
.........

步驟二:部署 Apps Script 服務(JSON API)

Google Apps Script 是將試算表資料轉為網頁可讀格式(JSON)的關鍵。

  1. 開啟 Apps Script 編輯器: 在試算表上方菜單,點擊「擴充功能」→「Apps Script」。

  2. 貼上程式碼: 將以下 Apps Script 程式碼貼入 程式碼.gs 檔案中,替換原有的內容。

程式碼.gs 內容 ()

JavaScript
function doGet() {
  // 取得當前作用中試算表的第一個工作表
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
  
  // 取得資料範圍:從第二列第一欄開始,讀取到最後一列,共讀取 3 欄 (序號、相簿名稱、相簿網址)
  var data = sheet.getRange(2, 1, sheet.getLastRow() - 1, 3).getValues();
  
  // 按序號排序(第一欄,索引為 0),確保連結列表是按照試算表中的序號排序顯示
  data.sort(function(a, b) {
    return a[0] - b[0];
  });
  
  // 將資料轉換為 JSON 字串並設定 MIME 類型
  var jsonOutput = ContentService.createTextOutput(JSON.stringify(data));
  jsonOutput.setMimeType(ContentService.MimeType.JSON);
  
  return jsonOutput;
}
  1. 儲存專案: 點擊上方的儲存圖標。

  2. 部署成網路應用程式:

    • 點擊右上角的「部署」→「新增部署作業」。

    • 在「選取類型」中選擇「網路應用程式 (Web app)」。

    • 執行身分:選擇「您自己」(這樣它就能讀取您的試算表)。

    • 存取權:選擇「任何人」(這樣您的網頁才能讀取)。

    • 點擊「部署」。

  3. 複製 URL: 部署成功後,會彈出一個視窗,請複製「網頁應用程式網址」(URL),這個網址就是我們的 JSON API 接口,請妥善保存。


步驟三:建立前端 HTML 頁面

接下來,我們將建立一個獨立的 photo.html 檔案,用於讀取 Apps Script 提供的 JSON 資料,並將其美化呈現。

  1. 建立 photo.html 檔案: 在您的電腦上建立一個名為 photo.html 的純文字檔案。

  2. 貼上程式碼: 將以下 HTML 程式碼貼入 photo.html 檔案中。

photo.html 內容

請注意,您需要將程式碼中一個重要的常數替換成您在步驟二-5中複製的 Apps Script URL。

HTML
<!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>
        /* 樣式代碼,用於呈現圖片中的兩欄 RWD 佈局、顏色和動畫效果 */
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            background-color: #f4f4f9;
            color: #333;
        }

        .container {
            max-width: 900px;
            margin: 50px auto;
            padding: 20px;
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            position: relative;
        }

        /* 標題區塊 */
        .header {
            border-bottom: 2px solid #e1b452;
            padding-bottom: 10px;
            margin-bottom: 20px;
            display: flex;
            justify-content: space-between;
            align-items: flex-end;
        }
        
        h1 {
            color: #5a5a5a;
            font-size: 1.8em;
            margin: 0;
        }

        /* 模擬圖片中的建築裝飾 */
        .decoration {
            width: 100px;
            height: 30px;
            background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 30"><path fill="%23e1b452" d="M 0 30 L 0 10 Q 5 0, 10 10 T 20 10 T 30 10 T 40 10 T 50 10 T 60 10 T 70 10 T 80 10 T 90 10 L 100 0 L 100 30 Z"/></svg>');
            background-size: contain;
            background-repeat: no-repeat;
        }

        /* 連結容器 - 桌面模式 (5行 x 2欄) */
        #link-list {
            position: relative;
            overflow: hidden; 
            height: 380px; 
        }
        
        .page-container {
            width: 100%;
            display: flex;
            transition: transform 0.5s ease-in-out;
        }
        
        .link-page {
            min-width: 100%; 
            box-sizing: border-box;
            padding: 0 10px;
        }

        /* 網格佈局:兩欄,每欄五個 */
        .link-grid {
            list-style-type: none;
            padding: 0;
            display: grid;
            grid-template-columns: 1fr 1fr; 
            gap: 15px 30px; 
        }

        li {
            padding: 15px 12px; 
            background-color: #fff8e8; 
            display: flex;
            align-items: center;
            border-bottom: 1px solid #fff8e8;
        }

        li:hover {
            background-color: #ffe6b0; 
            transition: background-color 0.3s;
        }

        .link-icon {
            margin-right: 15px;
            color: #e1b452; 
            font-size: 1.2em;
        }
        
        a {
            text-decoration: none;
            color: #333; 
            font-size: 1em;
            flex-grow: 1;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        a:hover {
            text-decoration: none;
            color: #007bff;
        }

        a:visited {
            color: #333;
        }
        
        #loading {
            font-size: 1.2em;
            color: #888;
            text-align: center;
            padding: 50px;
        }
        
        /* 翻頁控制項 */
        .pagination-controls {
            position: absolute;
            top: 50%;
            width: 100%;
            display: flex;
            justify-content: space-between;
            pointer-events: none; 
            transform: translateY(-50%);
        }
        
        .arrow {
            font-size: 2.5em;
            color: #e1b452;
            cursor: pointer;
            padding: 0 5px;
            pointer-events: auto; 
            user-select: none;
            opacity: 0.7;
            transition: opacity 0.3s;
        }
        
        .arrow:hover:not(.disabled) {
            opacity: 1;
        }
        
        .arrow.disabled {
            color: #ccc;
            cursor: default;
            opacity: 0.3;
        }

        /* 頁碼點點 */
        .page-dots {
            text-align: center;
            margin-top: 15px;
        }
        
        .dot {
            height: 10px;
            width: 10px;
            margin: 0 4px;
            background-color: #ddd;
            border-radius: 50%;
            display: inline-block;
            transition: background-color 0.3s;
        }
        
        .dot.active {
            background-color: #e1b452;
        }


        /* RWD Media Query: 響應式網頁設計 - 寬度不足時切換為單欄 */
        @media (max-width: 600px) {
            .container {
                max-width: 100%; 
                margin: 20px 10px; 
                padding: 15px;
            }

            .header h1 {
                font-size: 1.5em;
            }

            .decoration {
                display: none; 
            }

            /* 連結容器改為單欄 (10個項目垂直堆疊) */
            .link-grid {
                grid-template-columns: 1fr; 
                gap: 0; 
            }
            
            /* 調整列表高度以容納 10 個項目堆疊 */
            #link-list {
                height: 550px; 
            }

            li {
                padding: 12px 10px;
                background-color: #fff8e8;
                border-bottom: 1px solid #e9e9e9; 
            }
            
            li:last-child {
                border-bottom: none;
            }
            
            /* 翻頁箭頭調整 */
            .arrow {
                font-size: 2em;
                padding: 0 10px;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>鹿陽活動相簿</h1>
            <div class="decoration"></div>
        </div>
        
        <div id="loading">讀取中...</div>
        
        <div id="link-list">
            <div class="page-container" id="page-container">
                </div>
            
            <div class="pagination-controls">
                <span id="prev-btn" class="arrow disabled" onclick="manualChangePage(-1)" role="button" aria-controls="link-list" aria-label="上一頁">&lt;</span>
                <span id="next-btn" class="arrow" onclick="manualChangePage(1)" role="button" aria-controls="link-list" aria-label="下一頁">&gt;</span>
            </div>
        </div>
        
        <div class="page-dots" id="page-dots" role="tablist">
            </div>

    </div>

    <script>
        // **重要:將此 URL 替換為您部署 Apps Script 網頁應用程式後的 URL**
        // 請將此行替換成您在步驟二-5中複製的實際網址
        const APPS_SCRIPT_URL = 'https://script.google.com/macros/s/AKfycbxj8-L4N7lhnYcqbgLiEdWBbg7s_Saoxe-fZaQXpTB7_YYq3qKl_zCuFz-Lk4aBMdBvkQ/exec'; 

        const pageContainer = document.getElementById('page-container');
        const loadingMessage = document.getElementById('loading');
        const prevBtn = document.getElementById('prev-btn');
        const nextBtn = document.getElementById('next-btn');
        const pageDots = document.getElementById('page-dots');
        
        const LINKS_PER_PAGE = 10; 
        const AUTO_SLIDE_INTERVAL = 5000; 
        
        let allData = [];
        let currentPage = 0;
        let totalPages = 0;
        let autoSlideTimer; 

        /**
         * 繪製單一頁面的連結
         */
        function renderPage(pageIndex) {
            const start = pageIndex * LINKS_PER_PAGE;
            const end = start + LINKS_PER_PAGE;
            const pageData = allData.slice(start, end);
            
            const pageDiv = document.createElement('div');
            pageDiv.classList.add('link-page');

            const linkGrid = document.createElement('ul');
            linkGrid.classList.add('link-grid');
            
            pageData.forEach(row => {
                const index = row[0]; 
                const name = row[1];  
                let url = row[2];     

                if (name && url) {
                    // 如果網址不是以 https:// 或 http:// 開頭,則自動加上 https://
                    if (typeof url === 'string' && !url.match(/^https?:\/\//)) {
                        url = 'https://' + url;
                    }

                    const listItem = document.createElement('li');
                    // 使用三角形圖標代替序號,符合無障礙網站要求,使用 target="_blank" 和 title 說明會另開新視窗
                    listItem.innerHTML = `<span class="link-icon" aria-hidden="true">▶</span> <a href="${url}" target="_blank" title="${name}(另開新視窗)" rel="noopener noreferrer">${name}</a>`;
                    linkGrid.appendChild(listItem);
                }
            });
            
            pageDiv.appendChild(linkGrid);
            pageContainer.appendChild(pageDiv);
        }

        /**
         * 更新翻頁按鈕和頁碼點點的狀態
         */
        function updateControls() {
            prevBtn.classList.toggle('disabled', currentPage === 0);
            nextBtn.classList.toggle('disabled', currentPage === totalPages - 1 || totalPages === 0);

            // 更新頁碼點點
            pageDots.innerHTML = '';
            for (let i = 0; i < totalPages; i++) {
                const dot = document.createElement('span');
                dot.classList.add('dot');
                // 為點點新增無障礙屬性
                dot.setAttribute('role', 'tab'); 
                dot.setAttribute('aria-label', `前往第 ${i + 1} 頁`);
                dot.setAttribute('tabindex', '0'); // 使其可被鍵盤選中
                
                if (i === currentPage) {
                    dot.classList.add('active');
                    dot.setAttribute('aria-selected', 'true');
                } else {
                    dot.setAttribute('aria-selected', 'false');
                }
                
                // 點擊點點時暫停自動播放
                dot.onclick = () => {
                    stopAutoSlide();
                    goToPage(i);
                };
                // 支援鍵盤 Enter 鍵切換
                dot.onkeypress = (e) => {
                    if (e.key === 'Enter') {
                        stopAutoSlide();
                        goToPage(i);
                    }
                };
                
                pageDots.appendChild(dot);
            }
        }

        /**
         * 切換頁面 (核心邏輯)
         */
        function goToPage(pageIndex) {
            if (pageIndex < 0 || pageIndex >= totalPages) return;
            
            currentPage = pageIndex;
            const offset = -currentPage * 100; 
            pageContainer.style.transform = `translateX(${offset}%)`;
            updateControls();
        }

        /**
         * 自動翻頁邏輯:循環到下一頁
         */
        function nextSlide() {
            const nextIndex = (currentPage + 1) % totalPages; 
            goToPage(nextIndex);
        }

        /**
         * 啟動自動翻頁
         */
        function startAutoSlide() {
            if (totalPages > 1) {
                if (autoSlideTimer) {
                    clearInterval(autoSlideTimer);
                }
                // 設定新的計時器
                autoSlideTimer = setInterval(nextSlide, AUTO_SLIDE_INTERVAL);
            }
        }
        
        /**
         * 停止自動翻頁
         */
        function stopAutoSlide() {
            if (autoSlideTimer) {
                clearInterval(autoSlideTimer);
                autoSlideTimer = null;
            }
        }

        /**
         * 手動翻頁 (左右箭頭)
         */
        function manualChangePage(delta) {
            stopAutoSlide(); 
            goToPage(currentPage + delta);
        }

        // 將手動翻頁函數暴露給 HTML 元素的 onclick 屬性
        window.manualChangePage = manualChangePage;


        /**
         * 主要資料讀取函數
         */
        function fetchAndRenderLinks() {
            if (APPS_SCRIPT_URL.includes('AKfycbxj8-L4N7lhnYcqbgLiEdWBbg7s_Saoxe-fZaQXpTB7_YYq3qKl_zCuFz-Lk4aBMdBvkQ/exec')) {
                // 這裡檢查是否為預設的 URL,提醒用戶替換
                loadingMessage.textContent = '請在程式碼中替換 Apps Script 的 URL。';
                return;
            }

            fetch(APPS_SCRIPT_URL)
                .then(response => {
                    if (!response.ok) {
                        throw new Error('網路回應有問題');
                    }
                    return response.json();
                })
                .then(data => {
                    loadingMessage.style.display = 'none'; 
                    
                    // 過濾掉無效資料,只保留有相簿名稱和網址的項目
                    allData = data.filter(row => row[1] && row[2]);
                    
                    if (allData.length === 0) {
                        loadingMessage.style.display = 'block';
                        loadingMessage.textContent = '試算表中沒有資料。';
                        return;
                    }

                    // 計算總頁數
                    totalPages = Math.ceil(allData.length / LINKS_PER_PAGE);

                    // 循環資料並生成所有頁面
                    for (let i = 0; i < totalPages; i++) {
                        renderPage(i);
                    }
                    
                    // 顯示第一頁並更新控制項
                    goToPage(0);
                    
                    // 開始自動翻頁
                    startAutoSlide();

                })
                .catch(error => {
                    loadingMessage.style.display = 'block';
                    loadingMessage.textContent = '讀取資料時發生錯誤:' + error.message;
                    console.error('Fetch Error:', error);
                });
        }
        
        fetchAndRenderLinks();
    </script>
    
    <a href="https://docs.google.com/spreadsheets/d/15zqf581Gj_hEy9Kr3Tlgcsw-_C8jIBnaOf2OSBJzD2s/edit?usp=sharing" target="_blank" style="display:block; text-align:center; margin-top: 20px; font-size: 1.1em; color: #5a5a5a; text-decoration: none;">編輯活動相簿資料</a>
    
</body>
</html>

步驟四:運行與測試

  1. 開啟網頁: 雙擊您電腦上的 photo.html 檔案。它會使用您的預設瀏覽器開啟。

  2. 確認資料: 網頁應會顯示「讀取中...」後,載入您試算表中的前 10 個連結,並以您提供的圖片佈局呈現。

  3. 測試即時性:

    • 開啟您的 Google 試算表。

    • 新增一筆資料或修改現有連結的「相簿名稱」或「相簿網址」。

    • 回到 photo.html 頁面,重新整理瀏覽器。

    • 您會發現網頁上的連結已經同步更新!

恭喜您!您已成功建立一個由 Google 試算表驅動,具備 RWD 和自動播放功能的連結清單系統。這套系統大大降低了日常維護的複雜度,讓您專注於內容管理。

沒有留言:

張貼留言

【手把手教學】打造即時更新的 Google 試算表連結系統(Apps Script + HTML 應用)

  【手把手教學】打造即時更新的 Google 試算表連結系統(Apps Script + HTML 應用) 您是否厭倦了每次網站連結有變動,都必須手動修改網頁程式碼?本教學將教您如何利用 Google 試算表作為後端資料庫,結合 Google Apps Script 和前端 H...