方案一:簡易類別式(適合中小專案)
<?php
class SimplePaginator {
private PDO $pdo;
private string $baseSql;
private int $perPage;
private int $currentPage;
private int $totalRows;
private int $totalPages;
public function __construct(PDO $pdo, string $baseSql, int $perPage = 15) {
$this->pdo = $pdo;
$this->baseSql = $baseSql;
$this->perPage = $perPage;
$this->currentPage = max(1, (int)($_GET['page'] ?? 1));
$this->calculateTotals();
}
private function calculateTotals(): void {
$countSql = "SELECT COUNT(*) FROM ({$this->baseSql}) AS t";
$this->totalRows = (int)$this->pdo->query($countSql)->fetchColumn();
$this->totalPages = (int)ceil($this->totalRows / $this->perPage);
$this->currentPage = min($this->currentPage, max(1, $this->totalPages));
}
public function fetch(): array {
$offset = ($this->currentPage - 1) * $this->perPage;
$sql = "{$this->baseSql} LIMIT {$offset}, {$this->perPage}";
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
public function render(string $cssClass = 'pagination'): string {
if ($this->totalPages <= 1) return '';
$html = "<nav><ul class=\"{$cssClass}\">";
$range = $this->getPageRange(9);
if ($this->currentPage > 1) {
$html .= $this->link(1, '首頁');
$html .= $this->link($this->currentPage - 1, '上一頁');
}
foreach ($range as $page) {
$html .= $page === $this->currentPage
? "<li class=\"active\"><span>{$page}</span></li>"
: $this->link($page, (string)$page);
}
if ($this->currentPage < $this->totalPages) {
$html .= $this->link($this->currentPage + 1, '下一頁');
$html .= $this->link($this->totalPages, '尾頁');
}
$html .= '</ul></nav>';
return $html;
}
private function getPageRange(int $display): array {
$half = (int)floor($display / 2);
$start = max(1, $this->currentPage - $half);
$end = min($this->totalPages, $start + $display - 1);
$start = max(1, $end - $display + 1);
return range($start, $end);
}
private function link(int $page, string $text): string {
$url = $this->buildUrl($page);
return "<li><a href=\"{$url}\">{$text}</a></li>";
}
private function buildUrl(int $page): string {
$params = $_GET;
$params['page'] = $page;
return '?' . http_build_query($params);
}
public function getInfo(): array {
return [
'current' => $this->currentPage,
'per_page' => $this->perPage,
'total_pages' => $this->totalPages,
'total_rows' => $this->totalRows,
];
}
}
// 使用範例
$pdo = new PDO($dsn, $user, $pass);
$pager = new SimplePaginator($pdo, "SELECT * FROM articles WHERE status=1 ORDER BY id DESC", 10);
$articles = $pager->fetch();
echo $pager->render();
方案二:群組分頁類(適合大量頁碼、類似論壇翻頁)
<?php
class GroupPaginator {
public int $perPage = 20;
public int $groupSize = 10; // 每群顯示頁數
public int $currentPage = 1;
public int $currentGroup = 1;
public int $totalRows = 0;
public string $url = '';
public function init(int $totalRows, string $url, int $page = 1, int $group = 1): void {
$this->totalRows = $totalRows;
$this->url = $url;
$this->currentPage = max(1, $page);
$this->currentGroup = max(1, $group);
}
public function getTotalPages(): int {
return max(1, (int)ceil($this->totalRows / $this->perPage));
}
public function getLimit(): string {
$offset = ($this->currentPage - 1) * $this->perPage;
return "LIMIT {$offset}, {$this->perPage}";
}
public function render(): string {
$totalPages = $this->getTotalPages();
$totalGroups = (int)ceil($totalPages / $this->groupSize);
$this->currentGroup = min($this->currentGroup, $totalGroups);
$startPage = ($this->currentGroup - 1) * $this->groupSize + 1;
$endPage = min($startPage + $this->groupSize - 1, $totalPages);
$html = '<div class="group-pager">';
// 上一群
if ($this->currentGroup > 1) {
$prevGroup = $this->currentGroup - 1;
$prevPage = ($prevGroup - 1) * $this->groupSize + 1;
$html .= $this->link($prevPage, $prevGroup, '« 前群');
}
// 頁碼
for ($p = $startPage; $p <= $endPage; $p++) {
$html .= $p === $this->currentPage
? "<span class=\"current\">{$p}</span>"
: $this->link($p, $this->currentGroup, (string)$p);
}
// 下一群
if ($this->currentGroup < $totalGroups) {
$nextGroup = $this->currentGroup + 1;
$nextPage = ($nextGroup - 1) * $this->groupSize + 1;
$html .= $this->link($nextPage, $nextGroup, '後群 »');
}
$html .= '</div>';
return $html;
}
private function link(int $page, int $group, string $text): string {
return "<a href=\"{$this->url}?page={$page}&group={$group}\">{$text}</a>";
}
}
方案三:函數式輕量分頁(零依賴、複製即用)
<?php
/**
* 輕量分頁函數
* @param int $totalRows 總筆數
* @param int $perPage 每頁筆數
* @param string $baseUrl 基礎 URL(含 ? 及其他參數,不含 page)
* @return array ['html' => 分頁 HTML, 'offset' => OFFSET, 'limit' => LIMIT 字串]
*/
function paginate(int $totalRows, int $perPage = 20, string $baseUrl = ''): array {
$totalPages = max(1, (int)ceil($totalRows / $perPage));
$currentPage = max(1, min((int)($_GET['page'] ?? 1), $totalPages));
$offset = ($currentPage - 1) * $perPage;
// 建構 URL
$params = $_GET;
unset($params['page']);
$query = $params ? '?' . http_build_query($params) . '&' : '?';
$url = $baseUrl ?: ($_SERVER['SCRIPT_NAME'] . $query);
$html = '';
if ($totalPages > 1) {
$html = '<nav><ul class="pagination">';
$display = 9;
$half = (int)floor($display / 2);
$start = max(1, $currentPage - $half);
$end = min($totalPages, $start + $display - 1);
$start = max(1, $end - $display + 1);
if ($currentPage > 1) {
$html .= '<li><a href="' . $url . 'page=1">首頁</a></li>';
$html .= '<li><a href="' . $url . 'page=' . ($currentPage - 1) . '">上一頁</a></li>';
}
for ($i = $start; $i <= $end; $i++) {
$html .= $i === $currentPage
? "<li class=\"active\"><span>{$i}</span></li>"
: '<li><a href="' . $url . "page={$i}\">{$i}</a></li>";
}
if ($currentPage < $totalPages) {
$html .= '<li><a href="' . $url . 'page=' . ($currentPage + 1) . '">下一頁</a></li>';
$html .= '<li><a href="' . $url . "page={$totalPages}\">尾頁</a></li>";
}
$html .= '</ul></nav>';
}
return [
'html' => $html,
'offset' => $offset,
'limit' => "LIMIT {$offset}, {$perPage}",
'current_page' => $currentPage,
'total_pages' => $totalPages,
'total_rows' => $totalRows,
];
}
// 使用範例
$total = $pdo->query("SELECT COUNT(*) FROM logs")->fetchColumn();
$page = paginate($total, 50);
$stmt = $pdo->query("SELECT * FROM logs ORDER BY id DESC " . $page['limit']);
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo $page['html'];
三種方案比較選型
| 特性 | SimplePaginator | GroupPaginator | paginate() 函數 |
| 程式碼量 | 中 | 中 | 極少 |
| 物件導向 | 是 | 是 | 否 |
| 適合頁數 | < 1000 | > 1000 | < 500 |
| 自訂樣式 | 繼承覆寫 render() | 繼承覆寫 render() | 修改函數/傳入模板 |
| API 模式支援 | getInfo() 回傳陣列 | 需自行擴充 | 回傳陣列含完整資訊 |
| 依賴 | PDO | 無 | 無 |
進階:Ajax 無刷新分頁
<script>
// 通用 Ajax 分頁處理
document.querySelectorAll('.pagination a').forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
const url = a.href;
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(r => r.json())
.then(data => {
document.getElementById('list-container').innerHTML = data.html;
document.getElementById('pager-container').innerHTML = data.pager;
history.pushState(null, '', url);
});
});
});
</script>
<?php
// 後端判斷 Ajax 請求
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH'])) {
echo json_encode([
'html' => renderList($items),
'pager' => $pager->render(),
]);
exit;
}
// 否則正常輸出完整頁面
?>