" + OWTR_escapeHtml_(title) + "
", '' + OWTR_escapeHtml_(summary) + "
", '- ' + links.join("") + "
/** * 一週一報三端分流 GAS MVP * * 版本:MVP 0.4 * 使用方式: * 1. 建立 Google Sheet * 2. 擴充功能 → Apps Script * 3. 貼上本檔 * 4. 儲存並重新整理試算表 * 5. 使用「一週一報三端分流」選單操作 * * 核心功能: * - 初始化週報平台工作表 * - 建立本週週報資料列 * - 建立本週照片資料夾 * - 產生三端文案:班網完整版、LINE 短提醒版、FB 故事版 * - 產生 Google Docs 週報 * - 提供 LINE / FB 文案複製側邊欄 * - 產生班網發布素材與 HTML 貼上版 * - 使用 Google Chat Webhook 發布週報摘要到協作平台 * - 提供本週發布面板 Sidebar * - 標記發布狀態 * - 產生精簡歷週索引 * * 命名說明: * - onOpen 是 Google Apps Script 指定的簡單觸發器名稱。 * - 其他自訂函式皆使用 OWTR_ 前綴,降低與其他 GAS 程式碼命名衝突的機率。 */ const OWTR_CONFIG = { TIMEZONE: "Asia/Taipei", SHEETS: { DASHBOARD: "00_週報中控台", WEEKLY_DATA: "01_週報資料", COPY_OUTPUT: "02_三端文案", PUBLISH_LOG: "03_發布紀錄", SETTINGS: "04_設定", INDEX: "05_歷週索引", WEBSITE_MATERIALS: "06_班網發布素材", }, DEFAULT_SETTINGS: [ ["設定項目", "設定值", "說明"], ["班級名稱", "六年甲班", "顯示於週報標題"], ["導師名稱", "王老師", "顯示於週報署名"], ["學校名稱", "○○國小", "顯示於週報內容"], ["班級網站網址", "", "Google Sites 或班級網站網址"], ["FB粉專網址", "", "班級粉專網址,可空白"], ["Drive根資料夾ID", "", "可空白;空白時會自動建立一個根資料夾"], ["Google Chat Webhook URL", "", "協作平台 Google Chat 空間的 Webhook URL"], ["是否啟用協作平台發布", "否", "填「是」才允許一鍵發布到協作平台"], ["協作平台名稱", "Google Chat", "顯示於中控台與發布紀錄"], ["是否啟用AI", "否", "MVP 版暫不啟用 AI"], ], PLACEHOLDERS: { learningFocus: "國語:\n數學:\n自然/社會:\n其他:", activities: "本週班級活動或學校活動:", highlights: "學生亮點或值得鼓勵的表現:", reminders: "下週考試、繳交、活動、服裝或物品提醒:", parentTasks: "需要家長協助、簽名、填表或準備事項:", }, STATUS: { NOT_CREATED: "⚪ 尚未建立", DRAFT: "🟡 草稿中", COPY_READY: "🟢 已產生文案", DOC_READY: "🔵 已產生 Docs", COLLAB_READY: "🟣 已發布協作平台", COMPLETED: "✅ 已完成三端發布", }, }; /** * 試算表開啟時建立選單 */ function onOpen() { OWTR_onOpen(); } function OWTR_onOpen() { SpreadsheetApp.getUi() .createMenu("一週一報三端分流") .addItem("1. 初始化平台", "OWTR_initPlatform") .addItem("重新整理中控台", "OWTR_refreshDashboardView") .addSeparator() .addItem("2. 建立本週週報範本", "OWTR_createCurrentWeekReport") .addItem("建立下一週週報範本", "OWTR_createNextWeekReport") .addItem("3. 建立本週照片資料夾", "OWTR_createPhotoFolderForActiveWeek") .addSeparator() .addItem("開啟本週發布面板", "OWTR_showWeeklyPublishPanel") .addSeparator() .addItem("4. 產生三端文案", "OWTR_generateThreeChannelCopies") .addItem("5. 產生 Google Docs 週報", "OWTR_generateGoogleDocForActiveWeek") .addItem("產生班網發布素材", "OWTR_generateWebsitePublishMaterials") .addItem("一鍵發布到協作平台", "OWTR_publishToCollaborationPlatform") .addSeparator() .addItem("複製 LINE 文案", "OWTR_showLineCopySidebar") .addItem("複製 FB 文案", "OWTR_showFbCopySidebar") .addSeparator() .addItem("6. 標記班網已發布", "OWTR_markWebsitePublished") .addItem("7. 標記 LINE 已發布", "OWTR_markLinePublished") .addItem("8. 標記 FB 已發布", "OWTR_markFbPublished") .addSeparator() .addItem("9. 更新歷週索引", "OWTR_updateWeeklyIndex") .addToUi(); } /** * ========================= * 初始化平台 * ========================= */ function OWTR_initPlatform() { OWTR_runAction_("初始化平台", function () { const ss = SpreadsheetApp.getActiveSpreadsheet(); OWTR_setupDashboardSheet_(ss); OWTR_setupWeeklyDataSheet_(ss); OWTR_setupCopyOutputSheet_(ss); OWTR_setupPublishLogSheet_(ss); OWTR_setupSettingsSheet_(ss); OWTR_setupIndexSheet_(ss); OWTR_setupWebsiteMaterialsSheet_(ss); SpreadsheetApp.getUi().alert( "初始化完成。請先到「04_設定」確認班級資料,再執行「建立本週週報範本」。", ); }); } function OWTR_refreshDashboardView() { OWTR_runAction_("重新整理中控台", function () { const ss = SpreadsheetApp.getActiveSpreadsheet(); const activeWeekId = OWTR_getActiveWeekId_() || OWTR_getLatestWeekId_(); OWTR_ensureMvp03Structure_(ss); OWTR_setupDashboardSheet_(ss); OWTR_setupIndexSheet_(ss); if (activeWeekId) { OWTR_refreshDashboardForWeek_(activeWeekId); OWTR_updateWeeklyIndex_(); } OWTR_alert_("中控台已重新整理,不會清空週報資料與三端文案。"); }); } function OWTR_setupDashboardSheet_(ss) { const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.DASHBOARD); sheet.clear(); const values = [ ["一週一報三端分流|班級週報中控台", "", "", "", "", ""], [ "操作提示", "建立本週週報 → 填寫週報資料 → 產生三端文案 → 產生 Docs / 班網素材 → 手動發布三端或一鍵發布協作平台", "", "", "", "", ], ["", "", "", "", "", ""], ["目前週次ID", "", "", "總狀態", OWTR_CONFIG.STATUS.NOT_CREATED, ""], ["日期區間", "", "", "Google Docs", "尚未建立", ""], ["班級網站", "尚未設定", "", "照片資料夾", "尚未建立", ""], ["", "", "", "", "", ""], ["流程狀態", "", "", "", "", ""], ["草稿", "⚪ 尚未建立", "三端文案", "⚪ 尚未產生", "Google Docs", "⚪ 尚未產生"], ["三端發布", "⚪ 尚未完成", "協作平台", "⚪ 尚未發布", "", ""], ["", "", "", "", "", ""], ["發布狀態", "", "", "", "", ""], ["班級網站", "尚未發布", "", "", "", ""], ["LINE提醒", "尚未發布", "", "", "", ""], ["FB粉專", "尚未發布", "", "", "", ""], ["協作平台", "尚未發布", "", "", "", ""], ["", "", "", "", "", ""], ["本週流程步驟", "", "", "", "", ""], ["1", "建立週報範本", "⚪ 尚未完成", "下一步", "建立或確認本週週報資料列", ""], ["2", "填寫週報資料", "⚪ 尚未完成", "", "到 01_週報資料補齊內容", ""], ["3", "產生三端文案", "⚪ 尚未完成", "", "在發布面板或選單產生文案", ""], ["4", "建立 Docs / 照片資料夾", "⚪ 尚未完成", "", "在發布面板一鍵建立缺少項目", ""], ["5", "複製並發布三端", "⚪ 尚未完成", "", "從發布面板複製班網 / LINE / FB 文案", ""], ["6", "發布協作平台", "⚪ 尚未完成", "", "送出 Google Chat 協作通知", ""], ]; sheet.getRange(1, 1, values.length, 6).setValues(values); sheet.getRange("A1:F1").merge(); sheet.getRange("B2:F2").merge(); sheet.getRange("A8:F8").merge(); sheet.getRange("A12:F12").merge(); sheet.getRange("A18:F18").merge(); sheet.getRange("A1") .setFontSize(18) .setFontWeight("bold") .setFontColor("#ffffff") .setHorizontalAlignment("center") .setBackground("#1f4e79"); sheet.getRange("A2:F2").setBackground("#eaf3f8"); sheet.getRange("A4:A6").setFontWeight("bold"); sheet.getRange("D4:D6").setFontWeight("bold"); sheet.getRange("A8:F8").setFontWeight("bold").setBackground("#d9eaf7"); sheet.getRange("A12:F12").setFontWeight("bold").setBackground("#d9eaf7"); sheet.getRange("A18:F18").setFontWeight("bold").setBackground("#d9eaf7"); sheet.getRange("A9:F10").setBackground("#f8fafc"); sheet.getRange("A13:B16").setBackground("#f8fafc"); sheet.getRange("A19:F24").setBackground("#f8fafc"); sheet.getRange("E4").setFontWeight("bold"); sheet.setColumnWidth(1, 150); sheet.setColumnWidth(2, 240); sheet.setColumnWidth(3, 140); sheet.setColumnWidth(4, 150); sheet.setColumnWidth(5, 240); sheet.setColumnWidth(6, 40); sheet.setFrozenRows(1); } function OWTR_setupWeeklyDataSheet_(ss) { const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.WEEKLY_DATA); sheet.clear(); const headers = [ "週次ID", "日期區間", "本週學習重點", "本週班級活動", "學生亮點", "下週重要提醒", "家長配合事項", "照片資料夾URL", "建立時間", "最後更新時間", ]; sheet.getRange(1, 1, 1, headers.length).setValues([headers]); OWTR_styleHeader_(sheet, headers.length); sheet.setFrozenRows(1); sheet.setColumnWidths(1, headers.length, 180); sheet.setColumnWidth(3, 260); sheet.setColumnWidth(4, 260); sheet.setColumnWidth(5, 260); sheet.setColumnWidth(6, 260); sheet.setColumnWidth(7, 260); } function OWTR_setupCopyOutputSheet_(ss) { const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.COPY_OUTPUT); sheet.clear(); const headers = [ "週次ID", "班網完整版", "LINE短提醒版", "FB故事版", "產生時間", "是否確認", ]; sheet.getRange(1, 1, 1, headers.length).setValues([headers]); OWTR_styleHeader_(sheet, headers.length); sheet.setFrozenRows(1); sheet.setColumnWidth(1, 150); sheet.setColumnWidth(2, 420); sheet.setColumnWidth(3, 360); sheet.setColumnWidth(4, 420); } function OWTR_setupPublishLogSheet_(ss) { const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.PUBLISH_LOG); sheet.clear(); const headers = [ "週次ID", "班級網站", "LINE", "FB", "協作平台", "Google Docs URL", "發布備註", "最後更新時間", ]; sheet.getRange(1, 1, 1, headers.length).setValues([headers]); OWTR_styleHeader_(sheet, headers.length); sheet.setFrozenRows(1); sheet.setColumnWidths(1, headers.length, 180); sheet.setColumnWidth(6, 360); } function OWTR_setupSettingsSheet_(ss) { const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.SETTINGS); sheet.clear(); sheet .getRange(1, 1, OWTR_CONFIG.DEFAULT_SETTINGS.length, 3) .setValues(OWTR_CONFIG.DEFAULT_SETTINGS); OWTR_styleHeader_(sheet, 3); sheet.setFrozenRows(1); sheet.setColumnWidth(1, 180); sheet.setColumnWidth(2, 360); sheet.setColumnWidth(3, 420); } function OWTR_setupIndexSheet_(ss) { const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.INDEX); sheet.clear(); const headers = [ "週次ID", "日期區間", "摘要", "Google Docs", "照片資料夾", "班級網站", "發布狀態", "最後更新時間", ]; sheet.getRange(1, 1, 1, headers.length).setValues([headers]); OWTR_styleHeader_(sheet, headers.length); sheet.setFrozenRows(1); sheet.setColumnWidths(1, headers.length, 180); sheet.setColumnWidth(3, 420); sheet.setColumnWidth(4, 180); sheet.setColumnWidth(5, 180); sheet.setColumnWidth(6, 180); sheet.setColumnWidth(7, 240); sheet.setColumnWidth(8, 180); } function OWTR_setupWebsiteMaterialsSheet_(ss) { const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.WEBSITE_MATERIALS); sheet.clear(); const headers = [ "週次ID", "班網標題", "摘要", "純文字版", "HTML貼上版", "Google Docs連結", "照片資料夾連結", "產生時間", ]; sheet.getRange(1, 1, 1, headers.length).setValues([headers]); OWTR_styleHeader_(sheet, headers.length); sheet.setFrozenRows(1); sheet.setColumnWidth(1, 150); sheet.setColumnWidth(2, 260); sheet.setColumnWidth(3, 360); sheet.setColumnWidth(4, 520); sheet.setColumnWidth(5, 520); sheet.setColumnWidth(6, 240); sheet.setColumnWidth(7, 240); sheet.setColumnWidth(8, 180); } function OWTR_ensureMvp03Structure_(ss) { OWTR_ensureSettingsRows_(ss); OWTR_ensurePublishLogColumns_(ss); OWTR_ensureWebsiteMaterialsSheet_(ss); } function OWTR_ensureSettingsRows_(ss) { const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.SETTINGS); if (!sheet) return; const lastRow = sheet.getLastRow(); const existingKeys = lastRow >= 2 ? sheet.getRange(2, 1, lastRow - 1, 1).getValues().map(function (row) { return row[0]; }) : []; const missingRows = OWTR_CONFIG.DEFAULT_SETTINGS.slice(1).filter(function (row) { return existingKeys.indexOf(row[0]) === -1; }); if (missingRows.length > 0) { sheet.getRange(sheet.getLastRow() + 1, 1, missingRows.length, 3).setValues(missingRows); } } function OWTR_ensurePublishLogColumns_(ss) { const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.PUBLISH_LOG); if (!sheet) return; const requiredHeaders = [ "週次ID", "班級網站", "LINE", "FB", "協作平台", "Google Docs URL", "發布備註", "最後更新時間", ]; const lastCol = Math.max(sheet.getLastColumn(), 1); const currentHeaders = sheet.getRange(1, 1, 1, lastCol).getValues()[0]; if (currentHeaders.indexOf("協作平台") === -1) { sheet.insertColumnAfter(4); } sheet.getRange(1, 1, 1, requiredHeaders.length).setValues([requiredHeaders]); const lastRow = sheet.getLastRow(); if (lastRow >= 2) { const collaborationRange = sheet.getRange(2, 5, lastRow - 1, 1); const values = collaborationRange.getValues().map(function (row) { return [row[0] || "尚未發布"]; }); collaborationRange.setValues(values); } OWTR_styleHeader_(sheet, requiredHeaders.length); sheet.setColumnWidths(1, requiredHeaders.length, 180); sheet.setColumnWidth(6, 360); } function OWTR_ensureWebsiteMaterialsSheet_(ss) { let sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEBSITE_MATERIALS); if (!sheet) { OWTR_setupWebsiteMaterialsSheet_(ss); return; } const headers = [ "週次ID", "班網標題", "摘要", "純文字版", "HTML貼上版", "Google Docs連結", "照片資料夾連結", "產生時間", ]; if (sheet.getLastRow() === 0 || sheet.getRange(1, 1).getValue() !== "週次ID") { sheet.getRange(1, 1, 1, headers.length).setValues([headers]); } OWTR_styleHeader_(sheet, headers.length); sheet.setFrozenRows(1); } /** * ========================= * 建立週報 * ========================= */ function OWTR_createCurrentWeekReport() { OWTR_runAction_("建立本週週報範本", function () { const result = OWTR_createWeekReportByInfo_(OWTR_getCurrentWeekInfo_()); if (!result) return; OWTR_alert_( result.existed ? "本週週報已存在:" + result.weekId : "已建立本週週報範本:" + result.weekId + "\n請到「01_週報資料」填寫內容。", ); }); } function OWTR_createNextWeekReport() { OWTR_runAction_("建立下一週週報範本", function () { const result = OWTR_createWeekReportByInfo_(OWTR_getNextWeekInfo_()); if (!result) return; OWTR_alert_( result.existed ? "下一週週報已存在:" + result.weekId : "已建立下一週週報範本:" + result.weekId + "\n請到「01_週報資料」先填入可預備的提醒事項。", ); }); } function OWTR_createWeekReportByInfo_(weekInfo) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEEKLY_DATA); if (!sheet) { OWTR_alert_("請先執行「初始化平台」。"); return null; } const weekId = weekInfo.weekId; const dateRange = weekInfo.dateRange; if (OWTR_findRowByWeekId_(sheet, weekId) > 0) { OWTR_refreshDashboardForWeek_(weekId); return { weekId: weekId, dateRange: dateRange, existed: true }; } const now = new Date(); const placeholders = OWTR_CONFIG.PLACEHOLDERS; sheet.appendRow([ weekId, dateRange, placeholders.learningFocus, placeholders.activities, placeholders.highlights, placeholders.reminders, placeholders.parentTasks, "", now, now, ]); OWTR_ensurePublishLogRow_(weekId); OWTR_refreshDashboardForWeek_(weekId); OWTR_updateWeeklyIndex_(); return { weekId: weekId, dateRange: dateRange, existed: false }; } function OWTR_getCurrentWeekInfo_() { return OWTR_getWeekInfoForDate_(new Date()); } function OWTR_getNextWeekInfo_() { const nextWeek = new Date(); nextWeek.setDate(nextWeek.getDate() + 7); return OWTR_getWeekInfoForDate_(nextWeek); } function OWTR_getWeekInfoForDate_(today) { const timezone = OWTR_CONFIG.TIMEZONE; const day = today.getDay(); const diffToMonday = day === 0 ? -6 : 1 - day; const monday = new Date(today); monday.setDate(today.getDate() + diffToMonday); const friday = new Date(monday); friday.setDate(monday.getDate() + 4); const year = Utilities.formatDate(today, timezone, "yyyy"); const weekNumber = OWTR_getWeekNumber_(today); const weekId = year + "-W" + String(weekNumber).padStart(2, "0"); const dateRange = Utilities.formatDate(monday, timezone, "yyyy/MM/dd") + "–" + Utilities.formatDate(friday, timezone, "yyyy/MM/dd"); return { weekId, dateRange, }; } function OWTR_getWeekNumber_(date) { const target = new Date(date.valueOf()); const dayNumber = (date.getDay() + 6) % 7; target.setDate(target.getDate() - dayNumber + 3); const firstThursday = new Date(target.getFullYear(), 0, 4); const firstDayNumber = (firstThursday.getDay() + 6) % 7; firstThursday.setDate(firstThursday.getDate() - firstDayNumber + 3); return 1 + Math.round((target - firstThursday) / (7 * 24 * 60 * 60 * 1000)); } /** * ========================= * 照片資料夾 * ========================= */ function OWTR_createPhotoFolderForActiveWeek() { OWTR_runAction_("建立本週照片資料夾", function () { const activeWeekId = OWTR_getActiveWeekId_(); if (!activeWeekId) { OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。"); return; } const beforeData = OWTR_getWeeklyDataByWeekId_(activeWeekId); const hadFolder = beforeData && beforeData.photoFolderUrl; const folderUrl = OWTR_createPhotoFolderForWeek_(activeWeekId); if (!folderUrl) return; OWTR_refreshDashboardForWeek_(activeWeekId); OWTR_updateWeeklyIndex_(); OWTR_alert_((hadFolder ? "本週照片資料夾已存在:\n" : "已建立本週照片資料夾:\n") + folderUrl); }); } function OWTR_createPhotoFolderForWeek_(weekId) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const weeklySheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEEKLY_DATA); if (!weeklySheet) { OWTR_alert_("請先執行「初始化平台」。"); return ""; } const row = OWTR_findRowByWeekId_(weeklySheet, weekId); if (row <= 0) { OWTR_alert_("找不到週報資料:" + weekId); return ""; } const existingUrl = weeklySheet.getRange(row, 8).getValue(); if (existingUrl) { return existingUrl; } const settings = OWTR_getSettings_(); const className = settings["班級名稱"] || "班級"; const rootFolder = OWTR_getOrCreateRootFolder_(settings); const folderName = weekId + "_" + className + "_週報照片"; const folder = rootFolder.createFolder(folderName); const folderUrl = folder.getUrl(); weeklySheet.getRange(row, 8).setValue(folderUrl); weeklySheet.getRange(row, 10).setValue(new Date()); return folderUrl; } function OWTR_getOrCreateRootFolder_(settings) { const rootFolderId = settings["Drive根資料夾ID"]; if (rootFolderId) { try { return DriveApp.getFolderById(rootFolderId); } catch (e) { OWTR_alert_( "「Drive根資料夾ID」無法讀取,系統將自動建立新的根資料夾。\n\n原始錯誤:" + e.message, ); } } const ss = SpreadsheetApp.getActiveSpreadsheet(); const folderName = ss.getName() + "_一週一報資料夾"; const folder = DriveApp.createFolder(folderName); const settingsSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.SETTINGS); const row = OWTR_findSettingsRow_("Drive根資料夾ID"); if (settingsSheet && row > 0) { settingsSheet.getRange(row, 2).setValue(folder.getId()); } return folder; } /** * ========================= * 產生三端文案 * ========================= */ function OWTR_generateThreeChannelCopies() { OWTR_runAction_("產生三端文案", function () { const ss = SpreadsheetApp.getActiveSpreadsheet(); const weeklySheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEEKLY_DATA); const outputSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.COPY_OUTPUT); if (!weeklySheet || !outputSheet) { OWTR_alert_("請先執行「初始化平台」。"); return; } const weekId = OWTR_getActiveWeekId_(); if (!weekId) { OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。"); return; } const data = OWTR_getWeeklyDataByWeekId_(weekId); if (!data) { OWTR_alert_("找不到週報資料:" + weekId); return; } const settings = OWTR_getSettings_(); const warnings = OWTR_getIncompleteFieldWarnings_(data); const websiteCopy = OWTR_buildWebsiteCopy_(data, settings); const lineCopy = OWTR_buildLineCopy_(data, settings); const fbCopy = OWTR_buildFbCopy_(data, settings); const existingRow = OWTR_findRowByWeekId_(outputSheet, weekId); const now = new Date(); if (existingRow > 0) { outputSheet .getRange(existingRow, 2, 1, 5) .setValues([[websiteCopy, lineCopy, fbCopy, now, "否"]]); } else { outputSheet.appendRow([weekId, websiteCopy, lineCopy, fbCopy, now, "否"]); } OWTR_refreshDashboardForWeek_(weekId); OWTR_updateWeeklyIndex_(); let message = "已產生三端文案。請到「02_三端文案」查看。"; if (warnings.length > 0) { message += "\n\n提醒:下列欄位仍是預設提示文字或尚未填寫,建議補齊後再正式發布:\n- " + warnings.join("\n- "); } OWTR_alert_(message); }); } function OWTR_buildWebsiteCopy_(data, settings) { const className = settings["班級名稱"] || "班級"; const teacherName = settings["導師名稱"] || "導師"; const classWebsiteUrl = settings["班級網站網址"] || ""; return [ "【" + className + "|" + data.weekId + " 班級週報】", "", "日期區間:" + data.dateRange, "", "一、本週學習重點", OWTR_normalizeText_(data.learningFocus), "", "二、本週班級活動", OWTR_normalizeText_(data.activities), "", "三、學生亮點", OWTR_normalizeText_(data.highlights), "", "四、下週重要提醒", OWTR_normalizeText_(data.reminders), "", "五、家長配合事項", OWTR_normalizeText_(data.parentTasks), "", "六、照片與相關連結", data.photoFolderUrl ? "照片資料夾:" + data.photoFolderUrl : "本週尚未建立照片資料夾。", classWebsiteUrl ? "班級網站:" + classWebsiteUrl : "", "", teacherName + " 敬上", ] .filter(function (line, index, lines) { return line !== "" || lines[index - 1] !== ""; }) .join("\n") .trim(); } function OWTR_buildLineCopy_(data, settings) { const className = settings["班級名稱"] || "班級"; const websiteUrl = settings["班級網站網址"] || ""; return [ "【" + className + " 本週提醒|" + data.weekId + "】", "", "一、本週重點", OWTR_compactText_(data.learningFocus), "", "二、下週提醒", OWTR_compactText_(data.reminders), "", "三、家長配合", OWTR_compactText_(data.parentTasks), "", websiteUrl ? "完整週報請看班級網站:\n" + websiteUrl : "完整週報請見班級網站或老師後續公告。", ].join("\n"); } function OWTR_buildFbCopy_(data, settings) { const className = settings["班級名稱"] || "班級"; return [ "【" + className + " 班級故事|" + data.weekId + "】", "", "這一週,孩子們在學習與班級活動中,持續累積屬於自己的成長。", "", "本週學習重點:", OWTR_compactText_(data.learningFocus), "", "班級活動與生活觀察:", OWTR_compactText_(data.activities), "", "特別想記錄的是:", OWTR_compactText_(data.highlights), "", "學習不只是完成課本進度,也是在一次次練習、合作與表達中,慢慢長出面對事情的能力。", "", data.photoFolderUrl ? "本週照片紀錄:\n" + data.photoFolderUrl : "", ] .join("\n") .trim(); } function OWTR_getIncompleteFieldWarnings_(data) { const fields = [ ["本週學習重點", "learningFocus", data.learningFocus], ["本週班級活動", "activities", data.activities], ["學生亮點", "highlights", data.highlights], ["下週重要提醒", "reminders", data.reminders], ["家長配合事項", "parentTasks", data.parentTasks], ]; return fields .filter(function (field) { return OWTR_isPlaceholderOrBlank_(field[1], field[2]); }) .map(function (field) { return field[0]; }); } function OWTR_isPlaceholderOrBlank_(key, value) { const text = String(value || "").trim(); const placeholder = String(OWTR_CONFIG.PLACEHOLDERS[key] || "").trim(); return text === "" || text === placeholder; } /** * ========================= * 產生 Google Docs 週報 * ========================= */ function OWTR_generateGoogleDocForActiveWeek() { OWTR_runAction_("產生 Google Docs 週報", function () { const weekId = OWTR_getActiveWeekId_(); if (!weekId) { OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。"); return; } const docUrl = OWTR_createGoogleDocForWeek_(weekId); if (!docUrl) return; OWTR_refreshDashboardForWeek_(weekId); OWTR_updateWeeklyIndex_(); OWTR_alert_("已產生 Google Docs 週報:\n" + docUrl); }); } function OWTR_createGoogleDocForWeek_(weekId) { const existingDocUrl = OWTR_getPublishInfo_(weekId).docUrl; if (existingDocUrl) return existingDocUrl; const data = OWTR_getWeeklyDataByWeekId_(weekId); if (!data) { OWTR_alert_("找不到週報資料:" + weekId); return ""; } const settings = OWTR_getSettings_(); const className = settings["班級名稱"] || "班級"; const title = className + "_" + weekId + "_班級週報"; const doc = DocumentApp.create(title); const body = doc.getBody(); body.clear(); body.setMarginTop(54); body.setMarginBottom(54); body.setMarginLeft(54); body.setMarginRight(54); OWTR_buildFormalDocBody_(body, data, settings); OWTR_buildFormalDocFooter_(doc, settings); doc.saveAndClose(); const docUrl = doc.getUrl(); OWTR_updatePublishLogDocUrl_(weekId, docUrl); return docUrl; } function OWTR_buildFormalDocBody_(body, data, settings) { const className = settings["班級名稱"] || "班級"; const teacherName = settings["導師名稱"] || "導師"; const schoolName = settings["學校名稱"] || ""; const classWebsiteUrl = settings["班級網站網址"] || ""; body .appendParagraph(className + " 班級週報") .setHeading(DocumentApp.ParagraphHeading.TITLE) .setAlignment(DocumentApp.HorizontalAlignment.CENTER); const dateParagraph = body .appendParagraph(data.weekId + "|" + data.dateRange) .setAlignment(DocumentApp.HorizontalAlignment.CENTER); OWTR_setParagraphTextColor_(dateParagraph, "#666666"); if (schoolName) { const schoolParagraph = body .appendParagraph(schoolName) .setAlignment(DocumentApp.HorizontalAlignment.CENTER); OWTR_setParagraphTextColor_(schoolParagraph, "#666666"); } body.appendHorizontalRule(); OWTR_appendDocSection_(body, "一、本週學習重點", data.learningFocus); OWTR_appendDocSection_(body, "二、本週班級活動", data.activities); OWTR_appendDocSection_(body, "三、學生亮點", data.highlights); OWTR_appendDocSection_(body, "四、下週重要提醒", data.reminders); OWTR_appendDocSection_(body, "五、家長配合事項", data.parentTasks); body .appendParagraph("六、照片與相關連結") .setHeading(DocumentApp.ParagraphHeading.HEADING2); if (data.photoFolderUrl) { const photoParagraph = body.appendParagraph("照片資料夾:"); photoParagraph.appendText(data.photoFolderUrl).setLinkUrl(data.photoFolderUrl); } else { body.appendParagraph("本週尚未建立照片資料夾。"); } if (classWebsiteUrl) { const siteParagraph = body.appendParagraph("班級網站:"); siteParagraph.appendText(classWebsiteUrl).setLinkUrl(classWebsiteUrl); } body.appendParagraph(""); body.appendHorizontalRule(); body .appendParagraph(teacherName + " 敬上") .setAlignment(DocumentApp.HorizontalAlignment.RIGHT); const generatedDateParagraph = body .appendParagraph( Utilities.formatDate(new Date(), OWTR_CONFIG.TIMEZONE, "yyyy/MM/dd"), ) .setAlignment(DocumentApp.HorizontalAlignment.RIGHT); OWTR_setParagraphTextColor_(generatedDateParagraph, "#666666"); } function OWTR_appendDocSection_(body, heading, text) { body.appendParagraph(heading).setHeading(DocumentApp.ParagraphHeading.HEADING2); const lines = OWTR_normalizeText_(text) .split("\n") .map(function (line) { return line.trim(); }) .filter(function (line) { return line !== ""; }); lines.forEach(function (line) { body.appendParagraph(line).setLineSpacing(1.15); }); } function OWTR_buildFormalDocFooter_(doc, settings) { const footer = doc.addFooter(); const className = settings["班級名稱"] || "班級"; const teacherName = settings["導師名稱"] || "導師"; const footerParagraph = footer .appendParagraph(className + "|" + teacherName) .setAlignment(DocumentApp.HorizontalAlignment.CENTER); OWTR_setParagraphTextColor_(footerParagraph, "#888888"); } /** * ========================= * 班網發布素材 * ========================= */ function OWTR_generateWebsitePublishMaterials() { OWTR_runAction_("產生班網發布素材", function () { const weekId = OWTR_getActiveWeekId_(); if (!weekId) { OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。"); return; } const result = OWTR_generateWebsitePublishMaterialsForWeek_(weekId); if (!result) return; OWTR_alert_("已產生班網發布素材,請到「06_班網發布素材」查看。"); }); } function OWTR_generateWebsitePublishMaterialsForWeek_(weekId) { const ss = SpreadsheetApp.getActiveSpreadsheet(); OWTR_ensureWebsiteMaterialsSheet_(ss); const data = OWTR_getWeeklyDataByWeekId_(weekId); if (!data) { OWTR_alert_("找不到週報資料:" + weekId); return null; } const settings = OWTR_getSettings_(); const publishInfo = OWTR_getPublishInfo_(weekId); const websiteCopy = OWTR_getWebsiteCopyForWeek_(weekId) || OWTR_buildWebsiteCopy_(data, settings); const title = OWTR_buildWebsiteMaterialTitle_(data, settings); const summary = OWTR_makeWeeklySummary_(data); const html = OWTR_buildWebsiteHtml_(title, summary, websiteCopy, publishInfo.docUrl, data.photoFolderUrl); const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEBSITE_MATERIALS); const existingRow = OWTR_findRowByWeekId_(sheet, weekId); const rowValues = [ weekId, title, summary, websiteCopy, html, publishInfo.docUrl || "", data.photoFolderUrl || "", new Date(), ]; let row = existingRow; if (existingRow > 0) { sheet.getRange(existingRow, 1, 1, rowValues.length).setValues([rowValues]); } else { sheet.appendRow(rowValues); row = sheet.getLastRow(); } OWTR_setLinkCell_(sheet, row, 6, "開啟 Docs", publishInfo.docUrl, "尚未建立"); OWTR_setLinkCell_(sheet, row, 7, "開啟照片資料夾", data.photoFolderUrl, "尚未建立"); return { title: title, summary: summary, plainText: websiteCopy, html: html, docUrl: publishInfo.docUrl || "", photoFolderUrl: data.photoFolderUrl || "", }; } function OWTR_getWebsiteCopyForWeek_(weekId) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const outputSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.COPY_OUTPUT); if (!outputSheet) return ""; const row = OWTR_findRowByWeekId_(outputSheet, weekId); if (row <= 0) return ""; return outputSheet.getRange(row, 2).getValue(); } function OWTR_buildWebsiteMaterialTitle_(data, settings) { const className = settings["班級名稱"] || "班級"; return className + "|" + data.weekId + " 班級週報"; } function OWTR_buildWebsiteHtml_(title, summary, plainText, docUrl, photoFolderUrl) { const paragraphs = OWTR_escapeHtml_(plainText) .split(/\n{2,}/) .map(function (block) { return "
" + block.replace(/\n/g, "
") + "
' + OWTR_escapeHtml_(summary) + "
", '