日曆元件是後台管理系統、預約平台、行事曆應用的基礎組件。純 PHP 實作日曆邏輯(無需 JavaScript)具有首屏渲染快、SEO 友善、伺服器端可控制權限等優勢。本文提供兩種風格的完整 PHP 日曆類別:
兩者均採用物件導向設計,易於擴展(如加入農曆、節假日、事件標記)。
<?php
/**
* 經典月曆表格類別
* 功能:產生 HTML 表格、上下月導航、今日高亮、可自訂樣式類別
*/
class CalendarClassic
{
private int $year;
private int $month;
private array $weekLabels = ['日', '一', '二', '三', '四', '五', '六'];
private string $tableClass = 'calendar-table';
private string $todayClass = 'today';
private string $otherMonthClass = 'other-month';
private string $navUrl; // 導航連結基礎 URL
public function __construct(?int $year = null, ?int $month = null, string $navUrl = '')
{
$this->year = $year ?? (int)date('Y');
$this->month = $month ?? (int)date('m');
$this->navUrl = $navUrl ?: $_SERVER['PHP_SELF'];
}
public function setWeekLabels(array $labels): self
{
$this->weekLabels = $labels;
return $this;
}
public function setTableClass(string $class): self { $this->tableClass = $class; return $this; }
public function setTodayClass(string $class): self { $this->todayClass = $class; return $this; }
public function setOtherMonthClass(string $class): self { $this->otherMonthClass = $class; return $this; }
/** 取得該月天數 */
private function getDaysInMonth(): int
{
return (int)date('t', mktime(0, 0, 0, $this->month, 1, $this->year));
}
/** 取得該月 1 號是星期幾 (0=日 ... 6=六) */
private function getFirstWeekday(): int
{
return (int)date('w', mktime(0, 0, 0, $this->month, 1, $this->year));
}
/** 產生上一月/下一月導航連結 */
private function getNavLinks(): string
{
$prevMonth = $this->month - 1;
$prevYear = $this->year;
if ($prevMonth < 1) { $prevMonth = 12; $prevYear--; }
$nextMonth = $this->month + 1;
$nextYear = $this->year;
if ($nextMonth > 12) { $nextMonth = 1; $nextYear++; }
$prevUrl = $this->navUrl . "?year=$prevYear&month=$prevMonth";
$nextUrl = $this->navUrl . "?year=$nextYear&month=$nextMonth";
return "<td colspan='2'><a href='$prevUrl'><<</a></td>"
. "<td colspan='3' class='title'>{$this->year} 年 {$this->month} 月</td>"
. "<td colspan='2'><a href='$nextUrl'>>></a></td>";
}
/** 渲染完整 HTML 表格 */
public function render(): string
{
$daysInMonth = $this->getDaysInMonth();
$firstWeekday = $this->getFirstWeekday();
$today = (int)date('j');
$isCurrentMonth = (date('Y') == $this->year && date('m') == $this->month);
$html = "<table class='{$this->tableClass}'>\n";
$html .= " <tr class='nav-row'>" . $this->getNavLinks() . "</tr>\n";
$html .= " <tr class='week-header'>";
foreach ($this->weekLabels as $label) {
$html .= "<th>$label</th>";
}
$html .= "</tr>\n";
$day = 1;
for ($week = 0; $week < 6; $week++) {
$html .= " <tr>";
for ($wd = 0; $wd < 7; $wd++) {
$cellClass = [];
$content = '';
if ($week === 0 && $wd < $firstWeekday) {
// 上月尾巴
$prevMonthDays = (int)date('t', mktime(0,0,0,$this->month,0,$this->year));
$content = $prevMonthDays - $firstWeekday + $wd + 1;
$cellClass[] = $this->otherMonthClass;
} elseif ($day > $daysInMonth) {
// 下月開頭
$content = $day - $daysInMonth;
$cellClass[] = $this->otherMonthClass;
$day++;
} else {
// 本月日期
$content = $day;
if ($isCurrentMonth && $day === $today) {
$cellClass[] = $this->todayClass;
}
$day++;
}
$classAttr = $cellClass ? " class='" . implode(' ', $cellClass) . "'" : '';
$html .= "<td$classAttr><div class='day-num'>$content</div></td>";
}
$html .= "</tr>\n";
if ($day > $daysInMonth && $week > 3) break; // 已經顯示完本月且至少 4 週
}
$html .= "</table>";
return $html;
}
}
// ---------- 使用範例 ----------
require_once 'CalendarClassic.php';
$cal = new CalendarClassic(
year: isset($_GET['year']) ? (int)$_GET['year'] : null,
month: isset($_GET['month']) ? (int)$_GET['month'] : null,
navUrl: $_SERVER['PHP_SELF']
);
$cal->setWeekLabels(['Sun','Mon','Tue','Wed','Thu','Fri','Sat'])
->setTableClass('cal')
->setTodayClass('today');
echo $cal->render();
<style>
.cal {border-collapse: collapse; width: 100%; max-width: 400px; font-family: sans-serif;}
.cal th, .cal td {border: 1px solid #ddd; padding: 8px; text-align: center; height: 40px;}
.cal .nav-row td {background: #f0f4f8; font-weight: bold;}
.cal .nav-row a {color: #3366cc; text-decoration: none;}
.cal .week-header th {background: #3366cc; color: white;}
.cal .today {background: #ffe0b2; font-weight: bold;}
.cal .other-month {color: #ccc; background: #fafafa;}
.cal .day-num {font-size: 1.1em;}
</style>
適用於需要使用者選擇日期後提交表單、AJAX 載入該日行程、快速跳轉年月的場景。
<?php
/**
* 增強互動式日曆
* 功能:日期連結帶參數、年月下拉選單快速跳轉、今日高亮、可擴展事件標記
*/
class CalendarEnhanced
{
private int $year;
private int $month;
private int $selectedDay; // 被點選/高亮的日期
private array $events = []; // ['YYYY-MM-DD' => ['title'=>..., 'color'=>...]]
private string $baseUrl;
public function __construct(?int $year = null, ?int $month = null, ?int $day = null, string $baseUrl = '')
{
$this->year = $year ?? (int)date('Y');
$this->month = $month ?? (int)date('m');
$this->selectedDay = $day ?? (int)date('d');
$this->baseUrl = $baseUrl ?: $_SERVER['PHP_SELF'];
}
public function addEvent(string $date, string $title, string $color = '#e74c3c'): self
{
$this->events[$date] = ['title' => $title, 'color' => $color];
return $this;
}
public function setEvents(array $events): self { $this->events = $events; return $this; }
private function getDaysInMonth(): int { return (int)date('t', mktime(0,0,0,$this->month,1,$this->year)); }
private function getFirstWeekday(): int { return (int)date('w', mktime(0,0,0,$this->month,1,$this->year)); }
private function buildUrl(array $params): string
{
return $this->baseUrl . '?' . http_build_query(array_merge(
['year' => $this->year, 'month' => $this->month], $params
));
}
public function render(): string
{
$daysInMonth = $this->getDaysInMonth();
$firstWd = $this->getFirstWeekday();
$weekLabels = ['日','一','二','三','四','五','六'];
$html = "<div class='cal-enhanced'>\n";
$html .= " <table>\n";
// 標題列 + 年月下拉選單
$html .= " <tr><td colspan='7' class='header'>";
$html .= "<a href='{$this->buildUrl(['year'=>$this->year-1])}' title='上一年'><<</a> ";
$html .= "<a href='{$this->buildUrl(['month'=>$this->month-1])}' title='上一月'><</a> ";
// 年份下拉
$html .= "<select name='year' onchange='this.form.submit()'>";
for ($y = 1970; $y <= 2039; $y++) {
$sel = $y == $this->year ? ' selected' : '';
$html .= "<option value='$y'$sel>$y</option>";
}
$html .= "</select> 年 ";
// 月份下拉
$html .= "<select name='month' onchange='this.form.submit()'>";
for ($m = 1; $m <= 12; $m++) {
$sel = $m == $this->month ? ' selected' : '';
$html .= "<option value='$m'$sel>$m</option>";
}
$html .= "</select> 月 ";
$html .= "<a href='{$this->buildUrl(['month'=>$this->month+1])}' title='下一月'>>></a> ";
$html .= "<a href='{$this->buildUrl(['year'=>$this->year+1])}' title='下一年'>>></a>";
$html .= "</td></tr>\n";
// 星期標題
$html .= " <tr class='week'>";
foreach ($weekLabels as $w) $html .= "<th>$w</th>";
$html .= "</tr>\n";
// 日期格
$day = 1;
for ($week = 0; $week < 6; $week++) {
$html .= " <tr>";
for ($wd = 0; $wd < 7; $wd++) {
$classes = [];
$content = '';
$attrs = '';
if ($week === 0 && $wd < $firstWd) {
$prevLastDay = (int)date('t', mktime(0,0,0,$this->month,0,$this->year));
$content = $prevLastDay - $firstWd + $wd + 1;
$classes[] = 'other';
} elseif ($day > $daysInMonth) {
$content = $day - $daysInMonth;
$classes[] = 'other';
$day++;
} else {
$content = $day;
$dateKey = sprintf('%04d-%02d-%02d', $this->year, $this->month, $day);
// 日期連結:點擊帶入 day 參數
$url = $this->buildUrl(['day' => $day]);
$attrs = "href='$url'";
if ($day === $this->selectedDay) $classes[] = 'selected';
if (date('Y-m-d') === $dateKey) $classes[] = 'today';
if (isset($this->events[$dateKey])) {
$classes[] = 'has-event';
$attrs .= " title='" . htmlspecialchars($this->events[$dateKey]['title']) . "'";
}
$day++;
}
$classAttr = $classes ? " class='" . implode(' ', $classes) . "'" : '';
$html .= "<td$classAttr>";
if ($attrs) $html .= "<a $attrs>$content</a>";
else $html .= "<span>$content</span>";
// 事件指示點
if (isset($dateKey) && isset($this->events[$dateKey])) {
$color = $this->events[$dateKey]['color'];
$html .= "<span class='event-dot' style='background:$color'></span>";
}
$html .= "</td>";
}
$html .= "</tr>\n";
if ($day > $daysInMonth && $week > 3) break;
}
$html .= " </table>\n";
$html .= "</div>";
return $html;
}
}
// ---------- 使用範例 ----------
require_once 'CalendarEnhanced.php';
$cal = new CalendarEnhanced(
year: $_GET['year'] ?? null,
month: $_GET['month'] ?? null,
day: $_GET['day'] ?? null
);
// 模擬事件資料(實際可從資料庫讀取)
$cal->addEvent('2024-01-15', '團隊會議', '#3498db')
->addEvent('2024-01-20', '專案截止', '#e74c3c')
->addEvent('2024-01-28', '客戶拜訪', '#2ecc71');
echo $cal->render();
<style>
.cal-enhanced {font-family: sans-serif; max-width: 500px;}
.cal-enhanced table {border-collapse: collapse; width: 100%;}
.cal-enhanced th, .cal-enhanced td {border: 1px solid #eee; padding: 6px; text-align: center; position: relative; height: 38px;}
.cal-enhanced .header {background: #f8f9fa; padding: 10px; text-align: center;}
.cal-enhanced .header select {padding: 4px; margin: 0 4px;}
.cal-enhanced .week th {background: #34495e; color: white; font-weight: normal;}
.cal-enhanced td.other {color: #bbb; background: #fdfdfd;}
.cal-enhanced td.today {background: #fff3cd;}
.cal-enhanced td.selected {background: #3498db; color: white;}
.cal-enhanced td.has-event a {font-weight: bold;}
.cal-enhanced .event-dot {display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-top: 2px;}
.cal-enhanced a {text-decoration: none; color: inherit; display: block;}
.cal-enhanced a:hover {background: #e8f4fd;}
</style>
| 功能 | 實作提示 |
|---|---|
| 農曆/節氣顯示 | 引入 overtrue/laravel-chinese-calendar 或自行實作農曆算法 |
| 法定假日標記 | 建立假日資料表,addEvent() 批次載入 |
| 多月聯動顯示 | 實例化多個 Calendar 並並排渲染(CSS Flex/Grid) |
| AJAX 載入行程 | 日期 <a> 改為 data-date 屬性,JS 監聽 click 發送 fetch |
| 日期範圍選擇 | 雙擊/拖曳選取,前端維護 startDate/endDate 狀態 |
| 國際化 (i18n) | 週標題、月份名稱、年份格式抽離為語言包 |
純 PHP 實作日曆類別的優勢在於邏輯集中於伺服器端、首屏無 JS 閃動、易於整合權限控管(如特定角色不可選特定日期)。本文提供的兩種風格分別適合「純顯示導航」與「互動選擇回傳」場景,開發者可直接複製使用,或依專案需求擴展農曆、假日、事件、AJAX 等進階功能。關鍵在於將日期計算邏輯封裝於類別內部,對外僅暴露 render(),保持模板乾淨、易維護。