iT邦幫忙

0

從零打造輔大課表神器:Chrome Extension 開發實戰 30 天 - Day 25

  • 分享至 

  • xImage
  •  

Day 25:課表渲染引擎與資料綁定

🎯 系列目標:用 30 天時間,從零開始打造一個專屬輔大學生的課表生成 Chrome 擴充功能

💻 作者:輔大智慧資安 412580084

📅 Day 25:課表渲染引擎與資料綁定

🔗 課程系列回顧

昨天 Day 24 我們建立了課表的 HTML 模板與基礎樣式,今天我們要實現課表渲染引擎的核心功能,將 Chrome Storage 中儲存的課表資料動態渲染到頁面上。

📋 學習目標

今天我們要完成:

  1. 🎨 設計課表渲染引擎架構
  2. 📖 實作資料讀取與解析
  3. 📊 建立動態課表渲染機制
  4. 🌈 實作基礎主題系統

🎨 課表渲染引擎架構設計

1.1 核心渲染類別

ScheduleRenderer 類別封裝了所有渲染相關的功能,通過 Chrome Storage API 讀取儲存的課表資料,解析後動態生成課表內容。

// 課表渲染引擎核心類別
class ScheduleRenderer {
  constructor() {
    this.storageKey = 'fjuScheduleData';
    this.currentTheme = 'default';
    this.log('🎨 課表渲染引擎初始化完成');
  }

  // 讀取儲存的課表資料
  async loadScheduleData() {
    try {
      this.log('📖 開始讀取儲存的課表資料');
      
      const result = await new Promise((resolve, reject) => {
        chrome.storage.local.get([this.storageKey], (items) => {
          if (chrome.runtime.lastError) {
            reject(new Error(chrome.runtime.lastError.message));
          } else {
            resolve(items);
          }
        });
      });
      
      const scheduleData = result[this.storageKey];
      
      // 調試輸出
      this.log('🔍 儲存資料結構:');
      this.log(JSON.stringify(scheduleData, null, 2));
      
      if (!scheduleData) {
        throw new Error('未找到儲存的課表資料');
      }
      
      this.log('✅ 成功讀取課表資料');
      return scheduleData;
      
    } catch (error) {
      this.log(`❌ 讀取課表資料失敗: ${error.message}`);
      throw error;
    }
  }

  // 日誌輸出
  log(message) {
    console.log(`[ScheduleRenderer] ${message}`);
  }
}

1.2 輔大課程時間段定義

定義輔大課程時間段對照表,確保時間顯示的準確性:

// 輔大課程時間段定義
const TIME_PERIODS = {
  '1': { time: '08:10-09:00', display: '第一節' },
  '2': { time: '09:10-10:00', display: '第二節' },
  '3': { time: '10:10-11:00', display: '第三節' },
  '4': { time: '11:10-12:00', display: '第四節' },
  'n': { time: '12:10-13:00', display: 'DN' }, // DN 時段
  '5': { time: '13:40-14:30', display: '第五節' },
  '6': { time: '14:40-15:30', display: '第六節' },
  '7': { time: '15:40-16:30', display: '第七節' },
  '8': { time: '16:40-17:30', display: '第八節' },
  'E0': { time: '17:40-18:30', display: 'E0' }
};

// 正確的時間段順序
const PERIOD_ORDER = ['1', '2', '3', '4', 'n', '5', '6', '7', '8', 'E0'];

// 星期對照表
const DAY_MAP = {
  '一': 1,
  '二': 2,
  '三': 3,
  '四': 4,
  '五': 5,
  '六': 6,
  '日': 0
};

// 反向星期對照表
const DAY_NAMES = ['日', '一', '二', '三', '四', '五', '六'];

📖 資料讀取與學生資訊渲染

2.1 學生資訊渲染功能

實作學生資訊的動態渲染功能:

通過查詢 HTML 中對應的元素 ID,將從 Chrome Storage 讀取的學生資訊動態填入頁面中。採用安全的元素查詢方式,避免因元素不存在而導致錯誤。

// 渲染學生資訊
renderStudentInfo(scheduleData) {
  this.log('👤 開始渲染學生資訊');
  
  try {
    // 更新學生資訊顯示
    document.getElementById('semesterInfo').textContent = scheduleData.學期 || '未知學期';
    document.getElementById('departmentInfo').textContent = scheduleData.學生資訊?.系級 || '未知系級';
    document.getElementById('studentIdInfo').textContent = scheduleData.學生資訊?.學號 || '未知學號';
    document.getElementById('nameInfo').textContent = scheduleData.學生資訊?.姓名 || '未知姓名';
    document.getElementById('creditsInfo').textContent = scheduleData.學生資訊?.總學分 || '未知學分';
    
    this.log('✅ 學生資訊渲染完成');
  } catch (error) {
    this.log(`❌ 學生資訊渲染失敗: ${error.message}`);
  }
}

2.2 課程時間段解析

處理複雜的課程時間段格式:

// 解析課程時間段
parseCoursePeriods(periodString) {
  if (!periodString) return [];
  
  // 處理多個時段(如 "1,2,3")
  return periodString.split(',').map(p => p.trim());
}

📊 動態課表渲染機制

3.1 課表網格結構建立

動態建立課表網格結構:

// 建立課表網格結構
createScheduleGrid() {
  this.log('📊 建立課表網格結構');
  
  const scheduleBody = document.getElementById('scheduleBody');
  if (!scheduleBody) {
    throw new Error('找不到課表主體元素');
  }
  
  // 清空現有內容
  scheduleBody.innerHTML = '';
  
  // 按照正確的時間順序建立時間段行
  PERIOD_ORDER.forEach(periodKey => {
    const periodInfo = TIME_PERIODS[periodKey];
    if (!periodInfo) return;
    
    const row = document.createElement('tr');
    
    // 時間欄位
    const timeCell = document.createElement('td');
    timeCell.innerHTML = `
      <div class="time-period">${periodInfo.display}</div>
      <div class="time-range">${periodInfo.time}</div>
    `;
    row.appendChild(timeCell);
    
    // 星期欄位(週一到週五)
    for (let day = 1; day <= 5; day++) {
      const dayCell = document.createElement('td');
      dayCell.setAttribute('data-period', periodKey);
      dayCell.setAttribute('data-day', day);
      dayCell.className = 'schedule-cell';
      row.appendChild(dayCell);
    }
    
    scheduleBody.appendChild(row);
  });
  
  this.log('✅ 課表網格結構建立完成');
}

3.2 課程渲染功能

將課程資料渲染到課表網格中:

遍歷課程清單,解析每門課程的時間資訊,根據星期和節次定位到對應的課表格子,創建課程卡片並添加到格子中。通過顏色輪換機制為不同課程添加視覺區分。

// 渲染課程到課表
renderCourses(courses) {
  this.log(`📚 開始渲染 ${courses.length} 門課程`);
  
  // 清除所有現有的課程卡片
  document.querySelectorAll('.course-card').forEach(card => card.remove());
  
  courses.forEach((course, index) => {
    try {
      if (!course.上課時間 || !Array.isArray(course.上課時間)) {
        this.log(`⚠️ 課程 ${course.課程名稱} 缺少時間資訊,跳過渲染`);
        return;
      }
      
      course.上課時間.forEach((timeSlot, slotIndex) => {
        const day = DAY_MAP[timeSlot.星期];
        const periods = this.parseCoursePeriods(timeSlot.節次);
        
        // 檢查是否為有效星期(週一到週五)
        if (day === undefined || day < 1 || day > 5) {
          this.log(`⚠️ 課程 ${course.課程名稱} 星期資訊無效: ${timeSlot.星期}`);
          return;
        }
        
        // 為每個時段創建課程卡片
        periods.forEach(period => {
          const cell = document.querySelector(`td[data-period="${period}"][data-day="${day}"]`);
          
          if (cell) {
            const courseCard = document.createElement('div');
            courseCard.className = 'course-card';
            courseCard.innerHTML = `
              <div class="course-name">${course.課程名稱}</div>
              <div class="course-room">${timeSlot.教室 || '未指定教室'}</div>
            `;
            
            // 添加一些樣式變化以區分不同課程
            const colors = ['#4a90e2', '#66bb6a', '#9575cd', '#ffb74d', '#9fa8da', '#006064'];
            const color = colors[index % colors.length];
            courseCard.style.borderLeft = `4px solid ${color}`;
            
            cell.appendChild(courseCard);
          } else {
            this.log(`⚠️ 找不到對應的課表格子: 星期${timeSlot.星期} 第${period}節`);
          }
        });
      });
    } catch (error) {
      this.log(`❌ 渲染課程 ${course.課程名稱} 時發生錯誤: ${error.message}`);
    }
  });
  
  this.log('✅ 課程渲染完成');
}

🌈 基礎主題系統

4.1 主題系統實作

實現基礎主題切換功能:

通過監聽主題選擇器的變化事件,動態切換 CSS 中定義的主題變數。使用 localStorage 儲存使用者的主題選擇,確保下次訪問時保持相同的設定。

// 應用主題
applyTheme(themeName) {
  this.log(`🎨 應用主題: ${themeName}`);
  
  document.documentElement.setAttribute('data-theme', themeName);
  this.currentTheme = themeName;
  
  // 儲存主題選擇
  localStorage.setItem('fjuScheduleTheme', themeName);
  
  // 更新主題選擇器顯示
  const themeSelector = document.getElementById('themeSelector');
  if (themeSelector) {
    themeSelector.value = themeName;
  }
}

// 初始化主題選擇器
initThemeSelector() {
  const themeSelector = document.getElementById('themeSelector');
  if (themeSelector) {
    // 恢復上次選擇的主題
    const savedTheme = localStorage.getItem('fjuScheduleTheme') || 'default';
    themeSelector.value = savedTheme;
    this.applyTheme(savedTheme);
    
    // 綁定主題切換事件
    themeSelector.addEventListener('change', (event) => {
      this.applyTheme(event.target.value);
    });
  }
}

4.2 完整渲染流程

整合所有功能實現完整渲染流程:

// 渲染完整課表
async renderSchedule() {
  this.log('🚀 開始渲染完整課表');
  
  try {
    // 讀取儲存的課表資料
    const scheduleData = await this.loadScheduleData();
    
    // 渲染學生資訊
    this.renderStudentInfo(scheduleData);
    
    // 建立課表網格
    this.createScheduleGrid();
    
    // 渲染課程
    this.renderCourses(scheduleData.課程清單);
    
    this.log('🎉 課表渲染完成');
    return true;
    
  } catch (error) {
    this.log(`❌ 課表渲染失敗: ${error.message}`);
    this.showNotification('課表渲染失敗: ' + error.message, 'error');
    return false;
  }
}

🧪 測試與使用指南

5.1 初始化渲染引擎

在頁面載入完成後初始化渲染引擎:

// 初始化渲染引擎
document.addEventListener('DOMContentLoaded', async () => {
  console.log('🎯 開始初始化課表渲染引擎');
  
  try {
    const renderer = new ScheduleRenderer();
    
    // 初始化主題選擇器
    renderer.initThemeSelector();
    
    // 初始化控制按鈕
    renderer.initControls();
    
    // 渲染課表
    await renderer.renderSchedule();
    
    console.log('✅ 課表渲染引擎初始化完成');
    
    // 添加測試函數供調試使用
    window.testScheduleRenderer = renderer;
    
  } catch (error) {
    console.error('❌ 課表渲染引擎初始化失敗:', error);
    
    // 顯示錯誤通知
    const notification = document.createElement('div');
    notification.className = 'schedule-notification notification-error';
    notification.textContent = '課表載入失敗: ' + error.message;
    notification.style.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      padding: 12px 20px;
      border-radius: 5px;
      background-color: #dc3545;
      color: white;
      font-weight: bold;
      z-index: 10000;
    `;
    document.body.appendChild(notification);
  }
});

5.2 使用步驟

📋 使用步驟

  1. 請確保以下檔案存在且配置正確:
// 1. 確保 schedule.html 包含以下元素:
// - 學生資訊顯示區域(id: semesterInfo, departmentInfo, studentIdInfo, nameInfo, creditsInfo)
// - 課表主體(id: scheduleBody)
// - 主題選擇器(id: themeSelector)

// 2. 確保 schedule-styles.css 包含必要的樣式:
// - 課表網格樣式
// - 課程卡片樣式
// - 主題變數定義

// 3. 確保 manifest.json 包含:
{
  "permissions": ["storage"],
  "web_accessible_resources": [
    {
      "resources": ["schedule.html", "schedule-styles.css", "schedule-renderer.js"],
      "matches": ["<all_urls>"]
    }
  ]
}
  1. 確保資料已儲存

    • 在選課頁面執行 runIntegratedTest() 確保課表資料已儲存到 Chrome Storage
  2. 開啟課表頁面按鈕

    • 在 Chrome 擴充功能中點擊 popup,選擇「開始生成課表」
    • 在輔大學生入口網中點擊 我的課表
  3. 可以看到存在 Extension Storage 的課程清單能在頁面上呈現
    0

  4. 切換主題
    1


🎯 明日預告

今天我們完成了課表渲染引擎的核心功能實作,明天我們要進一步完善畫面顯示,實現響應式設計


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言