2025年12月6日 星期六

「Google Apps Script」校慶運動會獎狀列印 + 公告標題貼紙

🎯特別感謝:頂番國小 李清展 老師 讓線上列印夢想成真
💡 簡單的比喻
想像你在玩樂高積木
1. 匯入與合併:就像是你叫大家把各自的樂高桶子(資料)倒在你桌上,然後機器人幫你把所有相同顏色的積木挑出來,組合成一個超大的城堡(總表)。
2. 生成獎狀:就像是有一個印章(範本),機器人拿著名單,自動幫你在每一張紙上蓋章,並在旁邊寫上不同人的名字。
3. 標籤列印:就像是你要把不同形狀的積木塞進一個有很多小格子的收納盒(標籤紙)。機器人會先拿尺把每個格子量得精準無比,然後把積木一個一個塞進去,完全不會凸出來。
這段程式碼就是把這些繁瑣的「手工藝」變成了「自動化工廠」!
這段程式碼就像是一位**「全能型的學校行政總管」**。
試想一下,如果你是學校的行政工讀生,老師丟給你三個超煩人的任務:
1. 把十個班級繳交的 Excel 檔全部複製貼上變成一張大總表。
2. 依照名單,幫 500 個學生做獎狀。
3. 要寄信給家長,需要印 500 張地址標籤貼紙,而且格式要對得很齊。
這段程式碼就是幫你把這三件「苦力活」全部自動化。只要點幾個按鈕,它就會幫你在幾秒鐘內做完。以下我們把它拆解成四個主要部門來介紹:
1. 指揮中心(自訂選單與設定)
這位總管很貼心,它不會讓你去寫程式碼。
專屬按鈕:當你打開這個 Google 試算表時,它會自動在上面的選單列增加一個新選項叫「🎯資料匯整
功能列表:按下去後,你會看到「匯入資料」、「合併資料」、「生成獎狀」、「產生標籤」等功能,就像是用手機 App 一樣簡單
規則書 (Setting):它運作時會去查閱一個叫做 "Setting" 的分頁。這裡面寫著重要的資訊(例如:其他檔案的 ID 編號、獎狀範本長怎樣、標籤貼紙的長寬尺寸)
2. 資料收集部(匯入與合併)
這是幫你處理「複製貼上」地獄的部門。
匯入資料:它會根據你在 Setting 頁面填寫的檔案 ID,自動跑到別的 Google 試算表檔案裡,把指定的工作表(分頁)全部複製過來這裡
合併總表
    ◦ 它會把所有分頁的資料「疊」在一起,變成一張新的總表(檔名會加上今天的日期)
    ◦ 超級聰明:它知道第一列通常是標題(如:姓名、學號),所以在合併第二張表以後的資料時,它會自動把標題切掉,只保留內容,這樣你的總表就不會中間一直出現重複的標題
    ◦ 自動清理:它還會順便幫你檢查,如果是完全空白的列,它就直接丟掉不合併,保持資料乾淨
3. 獎狀印刷部(生成 Google 簡報)
這個部門負責把名單變成漂亮的獎狀。
找模具:它會先去讀取你指定的 Google 簡報範本(模具)
填空題:它會看著名單,一列一列讀取資料。如果名單上有 100 個人,它就在簡報裡複製 100 張投影片
自動填寫:接著,它會把表格裡的資料(第一格、第二格...),依序填進獎狀上的文字框裡。就像是它幫你拿著筆,一張一張把名字寫上去
4. 標籤排版部(精準列印)
這是最需要數學頭腦的部門,專門用來印貼紙(例如地址標籤)。
精密計算:它會讀取 Setting 頁面裡你設定的毫米 (mm) 數值(邊界、貼紙寬度、間距)
自動排版
    ◦ 它會把試算表的格子寬度和高度,調整到跟你設定的一模一樣,確保印出來不會跑版
    ◦ 它會幫你把資料組合好(例如:姓名 + 成績 + 名次),然後整齊地排列在格子裡
    ◦ 如果一張紙印滿了(例如一頁只能印 10 張),它還會自動幫你加上分隔線或空白行,準備印下一頁
🎯範本下載:



/**
 * ============================================================
 * Google Sheets 學校行政自動化整合系統 (最終完整版)
 * 功能:匯入資料、合併工作表、產生日期、製作獎狀、標籤排版
 * ============================================================
 */

function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('🎯資料匯整')
    .addItem('🚀匯入資料 (讀取 Setting)', 'importSheetsFromMultipleSources')
    .addItem('🚀合併所有工作表資料', 'MergeSheets')
    .addSeparator()
    .addItem('📆產生日期 (於 F2)', 'createDate')
    .addItem('🥇生成獎狀 (當前工作表)', 'createSlidesFromSheet')
    .addItem('📂開啟獎狀連結', 'openSlides')
    .addSeparator()
    .addItem('🖨️ 產生標籤 (排除空白列)', 'tag_AddressPrint')
    .addToUi();
}

// ==========================================
// 1. 日期與基礎工具
// ==========================================
function createDate() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getActiveSheet();
 
  // 防呆:禁止在 Setting 或 標籤列印頁面執行
  var sheetName = sheet.getName();
  if (sheetName === "Setting" || sheetName === "標籤列印") {
    SpreadsheetApp.getUi().alert('警告', '請勿在「' + sheetName + '」頁面執行此功能,請切換到資料工作表。', SpreadsheetApp.getUi().ButtonSet.OK);
    return;
  }
  sheet.getRange("F2").setValue(getTimemark());
}

function getTimemark() {
  var dateString = Utilities.formatDate(new Date(), "GMT+8", "yyyy-MM-dd");
  var parts = dateString.split('-');
  var year = parseInt(parts[0]);
  var month = parseInt(parts[1]);
  var day = parseInt(parts[2]);
  return "中華民國 " + (year - 1911) + " 年 " + month + " 月 " + day + " 日";
}

// ==========================================
// 2. 匯入資料功能
// ==========================================
function importSheetsFromMultipleSources() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var settingSheet = ss.getSheetByName("Setting");

  if (!settingSheet) {
    SpreadsheetApp.getUi().alert('錯誤', '找不到名為 "Setting" 的工作表。', SpreadsheetApp.getUi().ButtonSet.OK);
    return;
  }

  // 1. 讀取目標試算表 ID
  var destinationSpreadsheetId = settingSheet.getRange("B2").getValue().toString().trim();
  // 如果 B2 空白,預設為當前檔案
  var destinationSpreadsheet = (destinationSpreadsheetId === "") ? ss : SpreadsheetApp.openById(destinationSpreadsheetId);

  // 2. 讀取來源試算表 IDs (從 B3 開始)
  var lastRow = settingSheet.getLastRow();
  var sourceSpreadsheetIds = [];
 
  if (lastRow >= 3) {
    var rangeData = settingSheet.getRange(3, 2, lastRow - 2, 1).getValues();
    for (var i = 0; i < rangeData.length; i++) {
      var id = rangeData[i][0].toString().trim();
      if (id !== "") sourceSpreadsheetIds.push(id);
    }
  }

  if (sourceSpreadsheetIds.length === 0) {
    SpreadsheetApp.getUi().alert('提示', 'Setting B3 之後沒有來源 ID。', SpreadsheetApp.getUi().ButtonSet.OK);
    return;
  }

  // 3. 執行匯入
  var successCount = 0;
  for (var s = 0; s < sourceSpreadsheetIds.length; s++) {
    var sourceId = sourceSpreadsheetIds[s];
    try {
      var sourceSpreadsheet = SpreadsheetApp.openById(sourceId);
      var sheets = sourceSpreadsheet.getSheets();
     
      for (var i = 0; i < sheets.length; i++) {
        var sheet = sheets[i];
        var sheetName = sheet.getName();

        // 如果目標不存在該工作表才匯入
        if (!destinationSpreadsheet.getSheetByName(sheetName)) {
          sheet.copyTo(destinationSpreadsheet);
          Logger.log("從來源 " + sourceId + " 匯入: " + sheetName);
          successCount++;
        }
      }
    } catch (e) {
      Logger.log("無法開啟來源 ID: " + sourceId + " - " + e.toString());
    }
  }
  SpreadsheetApp.getUi().alert('完成', '匯入作業結束。\n共匯入 ' + successCount + ' 個新工作表。', SpreadsheetApp.getUi().ButtonSet.OK);
}

// ==========================================
// 3. 合併所有工作表資料 (強力優化版:自動過濾空白)
// ==========================================
function MergeSheets() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var today = Utilities.formatDate(new Date(), "GMT+8", "yyyyMMdd");
  var newSheetName = today + "_合併資料";

  ss.toast("正在準備合併...", "系統提示");

  // 1. 刪除舊的合併表 (如果存在)
  var existingSheet = ss.getSheetByName(newSheetName);
  if (existingSheet) ss.deleteSheet(existingSheet);
 
  // 建立新表
  var newSheet = ss.insertSheet(newSheetName);
  var sheets = ss.getSheets();
  var allData = [];
  var hasHeader = false;
  var headerRow = [];

  // 2. 循環讀取所有工作表
  for (var i = 0; i < sheets.length; i++) {
    var sheet = sheets[i];
    var sheetName = sheet.getName();

    // 排除名單
    if (sheetName.includes("合併資料") || sheetName === "All" || sheetName === "Setting" || sheetName === "標籤列印") {
      continue;
    }

    // 取得資料範圍
    var lastRow = sheet.getLastRow();
    if (lastRow < 1) continue; // 全空工作表跳過
   
    // 讀取該表所有資料
    var values = sheet.getRange(1, 1, lastRow, sheet.getLastColumn()).getValues();

    // ★★★ 關鍵優化:在記憶體中就先過濾掉空白列 ★★★
    // 這裡我們把「完全空白」的列篩選掉
    var cleanValues = values.filter(function(row) {
      return row.join("").trim() !== "";
    });

    if (cleanValues.length === 0) continue;

    // 3. 處理標題與內容
    if (!hasHeader) {
      // 第一個被讀到的表,取第一列當標題
      headerRow = cleanValues[0];
      allData = allData.concat(cleanValues); // 包含標題的所有資料放入
      hasHeader = true;
    } else {
      // 後續的表
      if (cleanValues.length > 1) {
        // 檢查該表第一列是否為標題 (通常是),如果是就去掉
        // 簡單判斷:如果第一列內容跟目前的 headerRow 很像,就當作標題去掉
        // 這裡直接假設第一列就是標題,予以移除 (slice(1))
        allData = allData.concat(cleanValues.slice(1));
      } else {
        // 如果該表只有一行且不是標題,則加入;如果是標題則忽略
        // 這裡保守處理:若長度為1且 header已存在,假設它是資料(除非完全等於標題)
        if (cleanValues[0].join("") !== headerRow.join("")) {
           allData = allData.concat(cleanValues);
        }
      }
    }
  }

  // 4. 一次性寫入資料
  if (allData.length > 0) {
    // 補齊欄位長度 (避免不同工作表欄數不一致導致報錯)
    var maxColumns = 0;
    for (var r = 0; r < allData.length; r++) {
      if (allData[r].length > maxColumns) maxColumns = allData[r].length;
    }
    // 補空字串
    for (var r = 0; r < allData.length; r++) {
      while (allData[r].length < maxColumns) allData[r].push("");
    }

    // 寫入
    newSheet.getRange(1, 1, allData.length, maxColumns).setValues(allData);
    newSheet.activate();
   
    // 顯示成功視窗 (這會強制結束「執行中」的轉圈圈)
    SpreadsheetApp.getUi().alert('成功', '合併完成!\n已自動過濾空白列,共匯整 ' + allData.length + ' 列資料。', SpreadsheetApp.getUi().ButtonSet.OK);
  } else {
    SpreadsheetApp.getUi().alert('提示', '沒有找到任何有效資料可以合併。', SpreadsheetApp.getUi().ButtonSet.OK);
  }
}

// ==========================================
// 4. 生成獎狀 (Google Slides)
// ==========================================
function getPresentationId() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Setting");
  if (!sheet) throw new Error("找不到 'Setting' 工作表");
  var id = sheet.getRange("B1").getValue();
  if (!id) throw new Error("請在 Setting B1 填入簡報 ID");
  return id.toString().trim();
}

function createSlidesFromSheet() {
  var PRESENTATION_ID;
  try { PRESENTATION_ID = getPresentationId(); } catch (e) { SpreadsheetApp.getUi().alert(e.message); return; }
 
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
 
  // 防呆檢查
  if (sheet.getName() === "Setting" || sheet.getName() === "標籤列印") {
    SpreadsheetApp.getUi().alert('操作錯誤', '請切換到含有名單的資料工作表,再執行獎狀生成。', SpreadsheetApp.getUi().ButtonSet.OK);
    return;
  }
 
  var lastRow = sheet.getLastRow();
  if (lastRow < 2) {
    SpreadsheetApp.getUi().alert('錯誤', '目前工作表沒有足夠的資料。', SpreadsheetApp.getUi().ButtonSet.OK);
    return;
  }

  var data = sheet.getRange(2, 1, lastRow - 1, sheet.getLastColumn()).getValues();
  var presentation = SlidesApp.openById(PRESENTATION_ID);
  var slides = presentation.getSlides();
  var templateSlide = slides[0];
  var templateTextBoxes = getTextBoxes(templateSlide);
 
  // 移除舊的投影片 (保留模板)
  for (var i = slides.length - 1; i > 0; i--) slides[i].remove();

  var generatedCount = 0;
  for (var i = 0; i < data.length; i++) {
    var rowData = data[i];
    // 檢查該列是否有內容
    var hasContent = rowData.some(function(cell) { return cell && cell.toString().trim() !== ''; });
   
    if (hasContent) {
      var slide = presentation.appendSlide(templateSlide.getLayout());
      var slideTextBoxes = getTextBoxes(slide);
      var fillCount = Math.min(rowData.length, slideTextBoxes.length);
     
      for (var j = 0; j < fillCount; j++) {
        var textContent = (rowData[j] !== undefined) ? rowData[j].toString() : '';
        if (slideTextBoxes[j]) slideTextBoxes[j].setText(textContent);
      }
      generatedCount++;
    }
  }
  templateSlide.remove(); // 移除模板
  SpreadsheetApp.getUi().alert('完成', '已生成 ' + generatedCount + ' 張獎狀。', SpreadsheetApp.getUi().ButtonSet.OK);
}

function getTextBoxes(slide) {
  var textBoxes = [];
  var elements = slide.getPageElements();
  for (var i = 0; i < elements.length; i++) {
    if (elements[i].getPageElementType() == SlidesApp.PageElementType.SHAPE) {
      if (elements[i].asShape().getText()) textBoxes.push(elements[i].asShape().getText());
    }
  }
  return textBoxes;
}

function openSlides() {
  try {
    var id = getPresentationId();
    var html = HtmlService.createHtmlOutput('<script>window.open("https://docs.google.com/presentation/d/' + id + '/edit", "_blank");google.script.host.close();</script>');
    SpreadsheetApp.getUi().showModalDialog(html, '開啟中...');
  } catch (e) { SpreadsheetApp.getUi().alert(e.message); }
}

// ==========================================
// 5. 標籤排版列印 (優化版:自動過濾空白)
// ==========================================
function tag_AddressPrint() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var mainWs = ss.getActiveSheet();
 
  if (mainWs.getName() === "Setting" || mainWs.getName() === "標籤列印") {
    SpreadsheetApp.getUi().alert("⚠️ 請先切換到「資料來源」工作表(例如:合併資料),再點選執行。");
    return;
  }

  ss.toast("正在讀取並過濾資料...", "步驟 1/3");

  // 1. 取得並過濾資料
  var lastRow = mainWs.getLastRow();
  if (lastRow < 2) {
    SpreadsheetApp.getUi().alert("資料不足,無法列印。");
    return;
  }
 
  var rawData = mainWs.getRange(2, 1, lastRow - 1, mainWs.getLastColumn()).getValues();
 
  // 過濾掉全空的列
  var cleanData = rawData.filter(function(row) {
    return row.join("").trim() !== "";
  });

  if (cleanData.length === 0) {
    SpreadsheetApp.getUi().alert("沒有有效資料(全是空白列)。");
    return;
  }

  // 2. 重建「標籤列印」工作表
  var tagSheetName = "標籤列印";
  var oldSheet = ss.getSheetByName(tagSheetName);
  if (oldSheet) ss.deleteSheet(oldSheet);
  var tagSheet = ss.insertSheet(tagSheetName);

  // 3. 執行排版與填入
  tagTypesetting(tagSheet, cleanData.length);
  WRAPROWS_B(tagSheet, cleanData);
}

function tagTypesetting(tagPrint_ws, dataCount) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var tag_setting_ws = ss.getSheetByName("Setting");

  if (!tag_setting_ws) {
    SpreadsheetApp.getUi().alert("錯誤:找不到 'Setting' 工作表。");
    return;
  }

  ss.toast("正在設定標籤格式...", "步驟 2/3");

  var upper_boundary = tag_setting_ws.getRange("F3").getValue();
  var lower_boundary = tag_setting_ws.getRange("F4").getValue();
  var left_boundary = tag_setting_ws.getRange("F5").getValue();
  var right_boundary = tag_setting_ws.getRange("F6").getValue();
  var wrap_count = tag_setting_ws.getRange("E7").getValue();
  var page_rows = tag_setting_ws.getRange("F7").getValue();
  var tag_width = tag_setting_ws.getRange("F8").getValue();
  var tag_height = tag_setting_ws.getRange("F9").getValue();
  var col_spacing = tag_setting_ws.getRange("F10").getValue();
  var row_spacing = tag_setting_ws.getRange("F11").getValue();
 
  var allColNums = wrap_count + (wrap_count - 1) + 2;
  var onepage_row_numbers = page_rows + (page_rows - 1) + 2;
  var pages = Math.ceil(dataCount / (wrap_count * page_rows));
  var allRowNums = pages * onepage_row_numbers;

  // 警告:如果頁數過多
  if (allRowNums > 4000) {
    SpreadsheetApp.getUi().alert("警告:資料量過大 (" + allRowNums + "列),可能執行逾時。");
  }

  // 調整工作表大小
  var currentMaxRows = tagPrint_ws.getMaxRows();
  var currentMaxCols = tagPrint_ws.getMaxColumns();
 
  if (allRowNums > currentMaxRows) tagPrint_ws.insertRowsAfter(currentMaxRows, allRowNums - currentMaxRows);
  if (allColNums > currentMaxCols) tagPrint_ws.insertColumnsAfter(currentMaxCols, allColNums - currentMaxCols);
  if (currentMaxCols > allColNums) tagPrint_ws.deleteColumns(allColNums + 1, currentMaxCols - allColNums);

  // 設定欄寬
  for(var i = 1; i <= allColNums; i++) {
    if (i % 2 == 0) tagPrint_ws.setColumnWidth(i, tag_width);
    else tagPrint_ws.setColumnWidth(i, col_spacing);
  }
  tagPrint_ws.setColumnWidth(1, left_boundary);
  tagPrint_ws.setColumnWidth(allColNums, right_boundary);

  // 設定列高
  for (var i = 1; i <= allRowNums; i++){
    var mod = i % onepage_row_numbers;
    if (mod == 1) tagPrint_ws.setRowHeight(i, upper_boundary);
    else if (mod == 0) tagPrint_ws.setRowHeight(i, lower_boundary);
    else {
      if (mod % 2 == 0) tagPrint_ws.setRowHeight(i, tag_height);
      else tagPrint_ws.setRowHeight(i, row_spacing);
    }
  }
 
  // 設定樣式
  tagPrint_ws.getRange(1, 1, allRowNums, allColNums)
             .setFontSize(12)
             .setHorizontalAlignment('left')
             .setVerticalAlignment('middle')
             .setWrap(true);
}

function WRAPROWS_B(tagPrint_ws, cleanData) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var tag_setting_ws = ss.getSheetByName("Setting");
 
  ss.toast("正在寫入資料...", "步驟 3/3");

  var wrap_count = tag_setting_ws.getRange("E7").getValue();
  var page_rows = tag_setting_ws.getRange("F7").getValue();
  var pad_width = "";
  var insert_col = 1;

  var array_row = [];
 
  for (var i = 0; i < cleanData.length; i++) {
    var row = cleanData[i];
   
    // ============================================
    // ★ 在此定義標籤內容 (0=A欄, 1=B欄...)
    // ============================================
    var content = row[0] + " " + row[1] + "\n" +
                  "項目:" + row[2] + "\n" +
                  "成績:" + row[3] + "  名次:第" + row[4] + "名";

    array_row.push(content);
   
    for (var j = 0; j < insert_col && !(((i + 1) % wrap_count) == 0); j++) {
       array_row.push(pad_width);
    }
  }

  // 補齊最後的空位
  const insert_col_numbers = (wrap_count - 1) * insert_col + wrap_count;
  const put_more_pad = insert_col_numbers - (array_row.length % insert_col_numbers);
  if (array_row.length % insert_col_numbers > 0) {
    for (var i = 1; i <= put_more_pad; i++) array_row.push(pad_width);
  }

  var array_wrapped = [];
  const padding_row = Array(insert_col_numbers).fill(pad_width);
  const boundary_row = Array(insert_col_numbers).fill("");
  var totalDataRows = array_row.length / insert_col_numbers;

  for (var i = 0; i < totalDataRows; i++) {
    array_wrapped.push(array_row.slice(i * insert_col_numbers, (i + 1) * insert_col_numbers));
   
    var currentRowInPage = i % page_rows;
    if (currentRowInPage < page_rows - 1) {
      array_wrapped.push(padding_row);
    } else {
      if (i < totalDataRows - 1) {
         array_wrapped.push(boundary_row);
         array_wrapped.push(boundary_row);
      }
    }
  }

  if (array_wrapped.length > 0) {
    var writeRows = array_wrapped.length;
    var maxRows = tagPrint_ws.getMaxRows();
    if (writeRows > maxRows) tagPrint_ws.insertRowsAfter(maxRows, writeRows - maxRows);
    tagPrint_ws.getRange(2, 2, writeRows, array_wrapped[0].length).setValues(array_wrapped);
  }

  ss.toast("標籤產生完成!", "完成");
  ss.setActiveSheet(tagPrint_ws);
}

沒有留言:

張貼留言

歡迎大家一起留言討論!