🎯 系列目標:用 30 天時間,從零開始打造一個專屬輔大學生的課表生成 Chrome 擴充功能
💻 作者:輔大智慧資安 412580084
📅 Day 25:課表渲染引擎與資料綁定
昨天 Day 24 我們建立了課表的 HTML 模板與基礎樣式,今天我們要實現課表渲染引擎的核心功能,將 Chrome Storage 中儲存的課表資料動態渲染到頁面上。
今天我們要完成:
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}`);
}
}
定義輔大課程時間段對照表,確保時間顯示的準確性:
// 輔大課程時間段定義
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 = ['日', '一', '二', '三', '四', '五', '六'];
實作學生資訊的動態渲染功能:
通過查詢 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}`);
}
}
處理複雜的課程時間段格式:
// 解析課程時間段
parseCoursePeriods(periodString) {
if (!periodString) return [];
// 處理多個時段(如 "1,2,3")
return periodString.split(',').map(p => p.trim());
}
動態建立課表網格結構:
// 建立課表網格結構
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('✅ 課表網格結構建立完成');
}
將課程資料渲染到課表網格中:
遍歷課程清單,解析每門課程的時間資訊,根據星期和節次定位到對應的課表格子,創建課程卡片並添加到格子中。通過顏色輪換機制為不同課程添加視覺區分。
// 渲染課程到課表
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('✅ 課程渲染完成');
}
實現基礎主題切換功能:
通過監聽主題選擇器的變化事件,動態切換 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);
});
}
}
整合所有功能實現完整渲染流程:
// 渲染完整課表
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;
}
}
在頁面載入完成後初始化渲染引擎:
// 初始化渲染引擎
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);
}
});
📋 使用步驟:
// 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>"]
}
]
}
確保資料已儲存
runIntegratedTest()
確保課表資料已儲存到 Chrome Storage開啟課表頁面按鈕
我的課表
可以看到存在 Extension Storage
的課程清單能在頁面上呈現
切換主題
今天我們完成了課表渲染引擎的核心功能實作,明天我們要進一步完善畫面顯示,實現響應式設計