使用者期望點擊「姓名」、「價格」、「日期」等表頭即可切換升/降序,且不重新整理頁面。後端需支援多欄位、多方向、型別感知排序。
<?php
// sort_service.php
class TableSorter {
private array $data;
private array $allowedColumns;
private array $numericColumns;
private string $defaultColumn;
private string $defaultOrder;
public function __construct(array $data, array $config = []) {
$this->data = $data;
$this->allowedColumns = $config['allowed'] ?? array_keys($data[0] ?? []);
$this->numericColumns = $config['numeric'] ?? [];
$this->defaultColumn = $config['default_col'] ?? ($this->allowedColumns[0] ?? 'id');
$this->defaultOrder = $config['default_order'] ?? 'asc';
}
public function sort(string $col = '', string $order = ''): array {
$col = $this->allowedColumns[0] ?? '';
if ($col && in_array($col, $this->allowedColumns, true)) {
$this->defaultColumn = $col;
}
$order = strtolower($order) === 'desc' ? 'desc' : 'asc';
$this->defaultOrder = $order;
if (empty($this->data)) return [];
// 建立平行陣列
$sortKeys = array_column($this->data, $this->defaultColumn);
$flags = in_array($this->defaultColumn, $this->numericColumns, true)
? SORT_NUMERIC : (SORT_NATURAL | SORT_FLAG_CASE);
$sortOrder = $order === 'desc' ? SORT_DESC : SORT_ASC;
array_multisort($sortKeys, $sortOrder, $flags, $this->data);
return $this->data;
}
public function getState(): array {
return ['col' => $this->defaultColumn, 'order' => $this->defaultOrder];
}
}
// API 端點
header('Content-Type: application/json; charset=utf-8');
require_once 'db.php'; // 取得 $pdo
$stmt = $pdo->query("SELECT id, product, price, stock, created_at FROM products");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$sorter = new TableSorter($rows, [
'allowed' => ['id', 'product', 'price', 'stock', 'created_at'],
'numeric' => ['id', 'price', 'stock'],
'default_col' => 'id',
'default_order' => 'desc',
]);
$col = $_GET['sort'] ?? '';
$order = $_GET['order'] ?? '';
$data = $sorter->sort($col, $order);
echo json_encode([
'success' => true,
'data' => $data,
'state' => $sorter->getState(),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
<!-- index.html -->
<div x-data="tableApp()" x-init="load()">
<table class="sortable">
<thead>
<template x-for="col in columns" :key="col.key">
<th @click="sort(col.key)" :class="{sorted: state.col === col.key}">
{{ col.label }}
<span x-show="state.col === col.key">
{{ state.order === 'asc' ? '▲' : '▼' }}
</span>
</th>
</template>
</thead>
<tbody>
<template x-for="row in rows" :key="row.id">
<tr><td>{{ row.id }}</td><td>{{ row.product }}</td><td>{{ row.price }}</td><td>{{ row.stock }}</td><td>{{ row.created_at }}</td></tr>
</template>
</tbody>
</table>
</div>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script>
function tableApp() {
return {
columns: [
{key: 'id', label: 'ID'},
{key: 'product', label: '商品'},
{key: 'price', label: '價格'},
{key: 'stock', label: '庫存'},
{key: 'created_at', label: '建立時間'},
],
rows: [],
state: {col: 'id', order: 'desc'},
async load() {
const url = new URL('api.php', window.location.origin);
url.searchParams.set('sort', this.state.col);
url.searchParams.set('order', this.state.order);
const res = await fetch(url);
const {data, state} = await res.json();
this.rows = data;
this.state = state;
},
sort(col) {
if (this.state.col === col) {
this.state.order = this.state.order === 'asc' ? 'desc' : 'asc';
} else {
this.state.col = col;
this.state.order = 'asc';
}
this.load();
}
};
}
</script>
// 在 sort() 中加入:
const url = new URL(window.location);
url.searchParams.set('sort', this.state.col);
url.searchParams.set('order', this.state.order);
window.history.pushState({}, '', url); // 不重載頁面更新網址
// x-init="load()" 改為:
x-init="load(new URLSearchParams(window.location.search))"
// load(params) 內:
const sort = params.get('sort') || this.state.col;
const order = params.get('order') || this.state.order;
this.state = {col: sort, order: order};
await this.load();
現在使用者可將排序後的網址分享給同事、重整頁面保持狀態。
若資料超過 5,000 筆,前端分頁 + 後端 SQL 排序更佳:
$allowed = ['id','product','price','stock','created_at'];
$col = in_array($_GET['sort'] ?? '', $allowed, true) ? $_GET['sort'] : 'id';
$order = ($_GET['order'] ?? 'desc') === 'asc' ? 'ASC' : 'DESC';
$sql = "SELECT * FROM products ORDER BY {$col} {$order} LIMIT 50 OFFSET ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([($_GET['page'] ?? 1 - 1) * 50]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
原則: 能在 SQL ORDER BY 解決的,不要搬到 PHP 記憶體排序。
TableSorter 統一驗證欄位、型別旗標、預設值。