月曆生成是 PHP 處理日期時間、表格渲染、表單交互的綜合練習。本文提供一個功能完整、代碼清晰的月曆實現,涵蓋核心算法、用戶輸入驗證、樣式美化、邊界處理,並延伸至日曆組件化、多語言、事件標記等進階主題。
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>PHP 動態月曆</title>
<style>
body { font-family: 'Microsoft JhengHei', sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
.calendar { border-collapse: collapse; width: 100%; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.calendar caption { background: #2c3e50; color: white; padding: 1rem; font-size: 1.5rem; font-weight: bold; }
.calendar th, .calendar td { border: 1px solid #e0e0e0; height: 60px; text-align: center; vertical-align: top; padding: 0.5rem; }
.calendar th { background: #f5f5f5; font-weight: 600; }
.calendar td.today { background: #e3f2fd; font-weight: bold; color: #1976d2; }
.calendar td.other-month { color: #9e9e9e; background: #fafafa; }
.calendar .day-num { font-size: 1.2rem; margin-bottom: 0.5rem; }
.calendar .events { font-size: 0.75rem; line-height: 1.3; }
.calendar .event { background: #fff3e0; color: #e65100; padding: 1px 4px; border-radius: 3px; margin: 1px 0; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.form-group { display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; align-items: end; }
.form-group label { font-weight: 500; }
.form-group input { padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; }
.form-group button { padding: 0.5rem 1.5rem; background: #2c3e50; color: white; border: none; border-radius: 4px; cursor: pointer; }
.error { color: #c62828; margin-bottom: 1rem; }
</style>
</head>
<body>
<h1>📅 PHP 動態月曆</h1>
<form method="post" class="form-group">
<div>
<label>年份 (1970-2038)</label>
<input type="number" name="year" value="= htmlspecialchars($_POST['year'] ?? date('Y')) ?>" min="1970" max="2038" required>
</div>
<div>
<label>月份 (1-12)</label>
<input type="number" name="month" value="= htmlspecialchars($_POST['month'] ?? date('n')) ?>" min="1" max="12" required>
</div>
<button type="submit">顯示月曆</button>
</form>
<?php
// 輸入驗證
$errors = [];
$year = isset($_POST['year']) ? (int)$_POST['year'] : null;
$month = isset($_POST['month']) ? (int)$_POST['month'] : null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($year === null || $year < 1970 || $year > 2038) $errors[] = '年份必須在 1970-2038 之間';
if ($month === null || $month < 1 || $month > 12) $errors[] = '月份必須在 1-12 之間';
}
if ($errors) {
foreach ($errors as $e) echo "<div class='error'>⚠ $e</div>";
} elseif ($year !== null && $month !== null) {
renderCalendar($year, $month);
}
function renderCalendar($year, $month) {
// 設置時區(關鍵:避免 strtotime 受服務器時區影響)
date_default_timezone_set('Asia/Taipei');
// 月份名稱(支援中英對照)
$months = [
1 => ['zh' => '一月', 'en' => 'January'], 2 => ['zh' => '二月', 'en' => 'February'],
3 => ['zh' => '三月', 'en' => 'March'], 4 => ['zh' => '四月', 'en' => 'April'],
5 => ['zh' => '五月', 'en' => 'May'], 6 => ['zh' => '六月', 'en' => 'June'],
7 => ['zh' => '七月', 'en' => 'July'], 8 => ['zh' => '八月', 'en' => 'August'],
9 => ['zh' => '九月', 'en' => 'September'], 10 => ['zh' => '十月', 'en' => 'October'],
11 => ['zh' => '十一月', 'en' => 'November'], 12 => ['zh' => '十二月', 'en' => 'December'],
];
$weekDays = ['日', '一', '二', '三', '四', '五', '六'];
// 計算當月第一天是星期幾 (0=周日 ... 6=周六)
$firstDayTimestamp = strtotime("$year-$month-01");
$firstDayOfWeek = (int)date('w', $firstDayTimestamp);
// 當月總天數
$daysInMonth = (int)date('t', $firstDayTimestamp);
// 今天日期(用於高亮)
$todayStr = date('Y-n-j');
$currentMonthStr = "$year-$month";
// 示例事件數據(實際可從數據庫讀取)
$events = [
"$year-$month-01" => ['元旦/月度會議'],
"$year-$month-15" => ['專案截止', '團建活動'],
"$year-$month-25" => ['薪資發放'],
];
// 開始輸出表格
echo "<table class='calendar'>";
echo "<caption>{$months[$month]['zh']} {$year}年 ({$months[$month]['en']})</caption>";
// 表頭:星期
echo "<thead><tr>";
foreach ($weekDays as $wd) echo "<th>$wd</th>";
echo "</tr></thead>";
echo "<tbody><tr>";
// 1. 輸出上月尾部佔位單元格
for ($i = 0; $i < $firstDayOfWeek; $i++) {
$prevMonthDay = date('j', strtotime("-" . ($firstDayOfWeek - $i) . " days", $firstDayTimestamp));
echo "<td class='other-month'><div class='day-num'>$prevMonthDay</div></td>";
}
// 2. 輸出當月日期
for ($day = 1; $day <= $daysInMonth; $day++) {
$weekDay = ($firstDayOfWeek + $day - 1) % 7;
// 每行開頭(周日)換行
if ($weekDay === 0 && $day > 1) echo "</tr><tr>";
$dateKey = "$year-$month-$day";
$isToday = ($dateKey === $todayStr);
$tdClass = $isToday ? "today" : "";
echo "<td class='$tdClass'>";
echo "<div class='day-num'>$day</div>";
// 事件顯示
if (isset($events[$dateKey])) {
echo "<div class='events'>";
foreach ($events[$dateKey] as $ev) echo "<span class='event'>$ev</span>";
echo "</div>";
}
echo "</td>";
}
// 3. 輸出下月開頭佔位單元格(補滿最後一行)
$lastWeekDay = ($firstDayOfWeek + $daysInMonth - 1) % 7;
for ($i = $lastWeekDay + 1; $i < 7; $i++) {
$nextMonthDay = $i - $lastWeekDay;
echo "<td class='other-month'><div class='day-num'>$nextMonthDay</div></td>";
}
echo "</tr></tbody></table>";
}
?>
</body>
</html>
採用「單行緩衝」策略:
此法避免了預先計算總行數、二維數組填充的複雜度,代碼線性直觀。
必須在腳本開頭設置 `date_default_timezone_set()`。否則 `strtotime()`、`date()` 使用服務器默認時區(通常為 UTC),導致月曆在時區邊界(如台灣 UTC+8)顯示錯誤日期。生產環境建議在 `php.ini` 全局設置或框架啟動時統一配置。
// 數據庫表結構建議
CREATE TABLE calendar_events (
id INT AUTO_INCREMENT PRIMARY KEY,
event_date DATE NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
color VARCHAR(7) DEFAULT '#ff9800', -- 十六進制顏色
user_id INT, -- 多用戶隔離
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_date (event_date)
);
// 讀取當月事件
function fetchMonthEvents($pdo, $year, $month, $userId = null) {
$start = "$year-$month-01";
$end = date('Y-m-t', strtotime($start));
$sql = "SELECT event_date, title, color FROM calendar_events WHERE event_date BETWEEN ? AND ?";
$params = [$start, $end];
if ($userId) { $sql .= " AND user_id = ?"; $params[] = $userId; }
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$events = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$events[$row['event_date']][] = ['title' => $row['title'], 'color' => $row['color']];
}
return $events;
}
// 使用 PHP Intl 擴展(需安裝 php-intl)
$locale = 'zh_TW'; // 或 'en_US', 'ja_JP'
$cal = IntlCalendar::createInstance('Asia/Taipei', $locale);
$cal->set($year, $month - 1, 1); // IntlCalendar 月份 0-based
$monthName = $cal->getDisplayName(IntlCalendar::FIELD_MONTH, IntlCalendar::LONG, $locale);
$weekDays = [];
for ($i = 1; $i <= 7; $i++) {
$cal->set(IntlCalendar::DAY_OF_WEEK, $i);
$weekDays[] = $cal->getDisplayName(IntlCalendar::FIELD_DAY_OF_WEEK, IntlCalendar::SHORT, $locale);
}
// 自動支援繁體中文「週日」、英文「Sun」、日文「日」等
總結: PHP 生成月曆的核心在於「日期運算準確」「表格結構正確」「輸出安全轉義」。從單文件腳本起步,逐步剝離業務邏輯(日期計算)、數據層(事件讀取)、視圖層(HTML 渲染),引入 Intl 本地化、AJAX 交互、響應式設計,即可演進為生產級日曆組件。建議以此為基礎,嘗試實現「農曆顯示」「節假日標記」「拖拽建立事件」等進階功能。