不使用資料庫、純以檔案系統管理圖片時,opendir() + readdir() 掃描目錄、過濾副檔名、輸出 HTML 是最經典的做法。本文解析一個極簡畫廊實作,採用 CSS 背景圖 background:url() 置中裁切 顯示縮圖,避免 <img> 變形,適合快速部署相簿、作品集或素材庫。
<?php
$directory = 'gallery'; // 相對路徑
$allowed = ['jpg','jpeg','gif','png'];
$i = 0;
$dh = @opendir($directory) or die("圖片目錄讀取失敗!");
while ($file = readdir($dh)) {
if ($file==='.' || $file==='..') continue;
$parts = explode('.', $file);
$ext = strtolower(array_pop($parts));
$title = htmlspecialchars(implode('.', $parts)); // 防 XSS
if (!in_array($ext, $allowed)) continue;
// 每 4 欄清除右邊距 (配合 CSS float)
$nomargin = (($i+1) % 4 === 0) ? 'nomargin' : '';
echo "<div class='pic $nomargin' style='background:url($directory/$file) no-repeat 50% 50%;'>\n";
echo " <a href='$directory/$file' title='$title' target='_blank'>$title</a>\n";
echo "</div>\n";
$i++;
}
closedir($dh);
<style>
.pic {
float: left;
width: 200px; height: 200px;
margin: 0 20px 20px 0;
border: 1px solid #ddd;
background-size: cover; /* 關鍵:等比填滿並置中裁切 */
background-position: center;
background-repeat: no-repeat;
position: relative;
}
.pic.nomargin { margin-right: 0; }
.pic a {
display: block; width: 100%; height: 100%;
color: #fff; text-decoration: none;
background: rgba(0,0,0,0.5); /* 半透明黑底 */
opacity: 0; transition: opacity .3s;
display: flex; align-items: center; justify-content: center;
font-size: 14px; word-break: break-all; padding: 10px;
}
.pic:hover a { opacity: 1; }
.clear { clear: both; }
</style>
<div class="gallery">
<?php ...掃描輸出... ?>
<div class="clear"></div>
</div>
| 技術 | 說明 |
|---|---|
opendir/readdir/closedir | 目錄流操作,記憶體效率優於 scandir() (大目錄) |
| 副檔名不區分大小寫 | strtolower(array_pop(explode('.',$file))) |
| XSS 防護 | htmlspecialchars() 處理檔名作為 title/連結文字 |
| CSS 背景圖置中裁切 | background-size:cover; background-position:center; 完美適配各種比例 |
| 懸浮顯示標題 | opacity 轉場,滑鼠移入顯示檔名 |
| 響應式改良 | 改用 display:grid; grid-template-columns: repeat(auto-fill, minmax(200px,1fr)); 取代 float |
array_slice($files, $offset, $limit),輸出頁碼連結。thumbs/ 縮圖 (300×300),後續直接讀取快取,避免大圖下載拖慢頁面。exif_read_data($file)['DateTimeOriginal'] 取拍攝時間排序。lightbox2 或 fslightbox,data-fslightbox="gallery" 即可啟用。IntersectionObserver 載入下一頁 JSON,後端回傳檔名陣列。$directory = realpath($directory) 確保在允許根目錄下,防止路徑遍歷。htmlspecialchars()。.php, .phtml, .htaccess 等可執行副檔名。0755,檔案 0644,避免 0777。// API Controller
public function index() {
$dir = storage_path('app/public/gallery');
$files = collect(glob("$dir/*.{jpg,jpeg,gif,png}", GLOB_BRACE))
->map(fn($p) => [
'url' => asset('storage/gallery/'.basename($p)),
'thumb' => asset('storage/gallery/thumbs/'.basename($p)),
'name' => basename($p),
'time' => filemtime($p),
])->sortByDesc('time')->values()->paginate(20);
return response()->json($files);
}
// Vue 3 + Vite 前端
<script setup>
import { ref, onMounted } from 'vue'
const images = ref([])
async function load(page=1) {
const res = await fetch(`/api/gallery?page=${page}`)
images.value.push(...(await res.json()).data)
}
onMounted(() => load())
</script>
<template>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div v-for="img in images" :key="img.url" class="relative aspect-square overflow-hidden rounded">
<img :src="img.thumb" :alt="img.name" class="w-full h-full object-cover hover:scale-105 transition" loading="lazy">
<div class="absolute inset-0 bg-black/50 text-white flex items-center justify-center opacity-0 hover:opacity-100 transition">{{ img.name }}</div>
</div>
</div>
<button @click="load(page+1)" v-if="hasMore">載入更多</button>
</template>
這段極簡程式碼展示了 opendir 掃描 → 副檔名過濾 → CSS 背景圖置中裁切 → 浮層顯示標題 的經典畫廊實作。零依賴、單一檔案、即刻可用。現代專案建議採用 前後端分離 + 物件儲存 + CDN 架構,後端只提供 API,前端用 Vue/React + 虛擬滾動實作高效能畫廊。