纯HTML打造跨端图片墙浏览神器

一个纯HTML编写的图片浏览静态页面,支持PC和H5端适配。页面提供网格/列表两种视图模式,包含搜索、排序、图片信息展示和下载功能。

目录

1、图片墙

2、PC端预览

3、H5端预览

4、HTML代码

5、附录


1、图片墙

        工作中经常需要分享图片,特制作一个图片墙浏览页面。以草图和大纲形式,喂给AI辅助完成页面框架搭建,经过不断调试和优化,最终效果还算过得去。纯前端,供有需要的同学参考。

2、PC端预览

  • 网格视图、列表视图

3、H5端预览

  • 列表视图、网格视图

4、HTML代码


<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片浏览器</title>
    <!-- 引入 Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- 引入 Font Awesome -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        /* 自定义滚动条 */
        ::-webkit-scrollbar {
            width: 8px;
        }
        ::-webkit-scrollbar-track {
            background: #f1f1f1; 
        }
        ::-webkit-scrollbar-thumb {
            background: #888; 
            border-radius: 4px;
        }
        ::-webkit-scrollbar-thumb:hover {
            background: #555; 
        }
        
        /* 图片卡片悬停效果 */
        .image-card {
            transition: all 0.3s ease;
        }
        .image-card:hover {
            transform: translateY(-5px);
            box-shadow: 0 10px 20px rgba(0,0,0,0.15);
        }

        /* 列表视图样式调整 */
        .list-view .image-card {
            flex-direction: row;
            align-items: center;
            height: auto;
        }
        .list-view .image-card img {
            width: 100px;
            height: 100px;
            object-fit: cover;
            margin-right: 20px;
        }
        .list-view .card-content {
            flex: 1;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        /* 加载动画 */
        .loader {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3b82f6;
            border-radius: 50%;
            width: 30px;
            height: 30px;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body class="bg-gray-50 text-gray-800 font-sans min-h-screen flex flex-col">

    <!-- 头部导航 -->
    <header class="bg-white shadow-sm sticky top-0 z-50">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
            <div class="flex items-center gap-1">
                <i class="fa-solid fa-images text-blue-600 text-xl"></i>
                <h1 class="text-xl font-bold text-gray-900">图库</h1>
            </div>
            
            <div class="flex items-center gap-4">
                <!-- 搜索框 hidden -->
                <div class="relative md:block">
                    <input type="text" id="searchInput" placeholder="搜索图片..." class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-40 transition-all">
                    <i class="fa-solid fa-search absolute left-3 top-3 text-gray-400"></i>
                </div>

                <!-- 视图切换按钮 -->
                <button id="toggleViewBtn" class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                    <i id="viewIcon" class="fa-solid fa-list"></i>
                    <span id="viewText">列表</span>
                </button>
            </div>
        </div>
    </header>

    <!-- 主内容区域 -->
    <main class="flex-grow max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-3 w-full">
        <!-- 统计信息 -->
        <div class="mb-3 flex justify-between items-center text-sm text-gray-500">
            <span id="imageCount">共 0 张图片</span>
            <div class="flex gap-2">
                <button onclick="sortfileList('date')" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-sort-amount-down"></i> 按日期</button>
                <button onclick="sortfileList('name')" class="hover:text-blue-600 transition-colors"><i class="fa-solid fa-sort-alpha-down"></i> 按名称</button>
            </div>
        </div>

        <!-- 图片容器 -->
        <div id="galleryContainer" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 gap-2 transition-all duration-300">
            <!-- 图片将通过 JS 动态插入这里 -->
        </div>

        <!-- 空状态提示 -->
        <div id="emptyState" class="hidden flex-col items-center justify-center py-20 text-gray-400">
            <i class="fa-regular fa-image text-6xl mb-4"></i>
            <p>没有找到相关图片</p>
        </div>
    </main>

    <!-- 页脚 -->
    <footer class="bg-white border-t mt-auto py-6">
        <div class="max-w-7xl mx-auto px-4 text-center text-gray-500 text-sm">Copyright &copy; 2026.</div>
        <div class="max-w-7xl mx-auto px-4 text-center text-gray-500 text-sm">狂龙骄子 All Rights Reserved.</div>
    </footer>

    <!-- Toast 通知容器 -->
    <div id="toastContainer" class="fixed bottom-5 right-5 z-50 flex flex-col gap-2"></div>

    <script>
        // 模拟图片数据
        const initialfileList = [
        "https://assets.699pic.com/public/web/images/500/709/879.jpg!detail.v1",
        "https://assets.699pic.com/public/web/images/500/534/517.jpg!detail.v1",
        "https://assets.699pic.com/public/web/images/701/264/819.jpg!detail.v1",
        "https://assets.699pic.com/public/web/images/701/400/464.jpg!detail.v1",
        "https://assets.699pic.com/public/web/images/701/226/206.jpg!detail.v1",
        ];

        let fileList = [...initialfileList];
        let fileInfoArray = [];
        let isGridView = true; // 默认网格视图
        let titleSortType = 'ASC';
        let dateSortType = 'ASC';

        // DOM 元素
        const galleryContainer = document.getElementById('galleryContainer');
        const toggleViewBtn = document.getElementById('toggleViewBtn');
        const viewIcon = document.getElementById('viewIcon');
        const viewText = document.getElementById('viewText');
        const searchInput = document.getElementById('searchInput');
        const imageCount = document.getElementById('imageCount');
        const emptyState = document.getElementById('emptyState');
        
        
        // 初始化
        document.addEventListener('DOMContentLoaded', async () => {
            // 并行获取所有图片的详细信息
            fileInfoArray = await Promise.all(
                initialfileList.map(file => getImageDetails(file).catch(() => null))
            );
            let imageList = [];
            for(let i=0; i<initialfileList.length; i++) {
                const url = initialfileList[i];
                const imageInfo = fileInfoArray[i];
                
                initialfileList[i] = {
                    title: getFilenameFromUrl(url),
                    url: url,
                    lastModified: imageInfo ? formatLastModified(imageInfo.lastModified) : null,
                    size: imageInfo ? imageInfo.sizeKB : null,
                    width: imageInfo ? imageInfo.width : null,
                    height: imageInfo ? imageInfo.height : null,
                };
            }
            fileList = [...initialfileList];

            renderfileList();
            
            // 自动触发一次视图切换以初始化状态(可选,这里直接渲染默认状态)
            // toggleViewBtn.click(); 
            
            // 绑定事件
            toggleViewBtn.addEventListener('click', switchView);
            searchInput.addEventListener('input', handleSearch);
        });

        // 渲染图片列表
        function renderfileList() {
            galleryContainer.innerHTML = '';
            
            if (fileList.length === 0) {
                emptyState.classList.remove('hidden');
                emptyState.classList.add('flex');
                imageCount.textContent = `共 0 张图片`;
                return;
            } else {
                emptyState.classList.add('hidden');
                emptyState.classList.remove('flex');
            }

            imageCount.textContent = `共 ${fileList.length} 张图片`;
            
            fileList.forEach((img, index) => {
                const card = document.createElement('div');
                card.className = `image-card bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 flex flex-col ${!isGridView ? 'list-view-item' : ''}`;
                
                // 构建卡片 HTML
                card.innerHTML = `
                    <div class="relative group overflow-hidden ${isGridView ? 'h-48' : 'w-24 h-24 flex-shrink-0'}">
                        <img src="${img.url}" alt="${img.title}" class="w-full h-full object-scale transition-transform duration-300 group-hover:scale-110" loading="lazy">
                        <div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-10 transition-all duration-300"></div>
                    </div>
                    
                    <div class="p-2 flex flex-col flex-grow ${!isGridView ? 'justify-center' : ''} overflow-hidden">
                        <div class="flex justify-between items-start mb-2">
                            <h3 class="font-semibold text-gray-800 truncate pr-2" title="${img.title}">${img.title}</h3>
                        </div>
                        
                        <div class="flex items-center justify-between items-end">
                            <div class="text-xs text-gray-500 flex items-center gap-3">
                                <span><i class="fa-regular fa-calendar"></i> ${img.lastModified}</span>
                                ${isGridView ? '<span><i class="fa-solid fa-database"></i>' + img.size + ' KB</span>' : ''}
                                <span><i class="fa-solid fa-ruler-combined"></i> ${img.width}x${img.height}</span>
                            </div>
                            <button onclick="downloadImage('${img.url}', '${img.title}')" 
                                    class="download-btn flex items-center gap-2 px-3 py-1.5 bg-green-50 text-green-600 rounded-lg hover:bg-green-100 hover:text-green-700 transition-colors text-sm font-medium group/btn">
                                下载
                            </button>
                        </div>
                    </div>
                `;
                
                galleryContainer.appendChild(card);
            });
            
            updateViewClass();
        }

        // 切换视图模式
        function switchView() {
            isGridView = !isGridView;
            
            if (isGridView) {
                viewIcon.className = 'fa-solid fa-list';
                viewText.textContent = '列表';
                galleryContainer.classList.remove('grid-cols-1');
                galleryContainer.classList.add('sm:grid-cols-2', 'md:grid-cols-3', 'lg:grid-cols-3');
            } else {
                viewIcon.className = 'fa-solid fa-border-all';
                viewText.textContent = '网格';
                galleryContainer.classList.remove('sm:grid-cols-2', 'md:grid-cols-3', 'lg:grid-cols-3');
                galleryContainer.classList.add('grid-cols-1');
            }
            
            renderfileList();
            showToast(isGridView ? '已切换到网格视图' : '已切换到列表视图', 'info');
        }

        // 更新视图类名以适配 CSS
        function updateViewClass() {
            if (!isGridView) {
                galleryContainer.classList.add('list-view');
            } else {
                galleryContainer.classList.remove('list-view');
            }
        }

        // 搜索功能
        function handleSearch(e) {
            const term = e.target.value.toLowerCase();
            fileList = initialfileList.filter(img => img.title.toLowerCase().includes(term));
            renderfileList();
        }

        // 排序功能
        function sortfileList(type) {
            if (type === 'date') {
                if (dateSortType === 'ASC') {
                    fileList.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified));
                    dateSortType = 'DSC';
                } else {
                    fileList.sort((a, b) => new Date(a.lastModified) - new Date(b.lastModified));
                    dateSortType = 'ASC';
                }
            } else if (type === 'name') {
                if (titleSortType === 'ASC') {
                    fileList.sort((a, b) => a.title.localeCompare(b.title));
                    titleSortType = 'DSC';
                } else {
                    fileList.sort((a, b) => b.title.localeCompare(a.title));
                    titleSortType = 'ASC';
                }
            }
            renderfileList();
            showToast(`已按${type === 'date' ? '日期' : '名称'}排序`, 'success');
        }

        // 核心功能:下载图片
        async function downloadImage(url, filename) {
            const btn = event.currentTarget;
            const originalContent = btn.innerHTML;
            
            // 显示加载状态
            btn.disabled = true;
            btn.innerHTML = `<div class="loader w-4 h-4 border-2"></div> 下载中...`;
            btn.classList.add('opacity-75', 'cursor-not-allowed');

            const baseUrl = window.location.href; // 当前页面完整 URL
            const relativePath = './mch-banner/banner1.png';
            const finalUrl = new URL(url, baseUrl).href;

            try {
                // 使用 fetch 获取图片 blob,解决跨域和直接打开新窗口的问题
                const response = await fetch(finalUrl); 
                if (!response.ok) throw new Error('网络响应异常');
                
                const blob = await response.blob();
                const blobUrl = window.URL.createObjectURL(blob);
                
                // 创建临时链接触发下载
                const link = document.createElement('a');
                link.href = blobUrl;
                link.download = filename;
                document.body.appendChild(link);
                link.click();
                
                // 清理
                document.body.removeChild(link);
                window.URL.revokeObjectURL(blobUrl);
                
                showToast('下载成功!', 'success');
            } catch (error) {
                console.error('Download failed:', error);
                showToast('下载失败,请重试', 'error');
            } finally {
                // 恢复按钮状态
                btn.disabled = false;
                btn.innerHTML = originalContent;
                btn.classList.remove('opacity-75', 'cursor-not-allowed');
            }
        }

        // Toast 提示系统
        function showToast(message, type = 'info') {
            const toast = document.createElement('div');
            const colors = {
                success: 'bg-green-500',
                error: 'bg-red-500',
                info: 'bg-blue-500'
            };
            const icons = {
                success: 'fa-check-circle',
                error: 'fa-exclamation-circle',
                info: 'fa-info-circle'
            };

            toast.className = `${colors[type]} text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 transform transition-all duration-300 translate-y-10 opacity-0 min-w-[200px]`;
            toast.innerHTML = `
                <i class="fa-solid ${icons[type]}"></i>
                <span class="font-medium text-sm">${message}</span>
            `;

            document.getElementById('toastContainer').appendChild(toast);

            // 动画进入
            requestAnimationFrame(() => {
                toast.classList.remove('translate-y-10', 'opacity-0');
            });

            // 自动移除
            setTimeout(() => {
                toast.classList.add('translate-y-10', 'opacity-0');
                setTimeout(() => {
                    toast.remove();
                }, 300);
            }, 3000);
        }
        
        // 下载图片并计算 Blob 大小。同时获取像素宽高。
        async function getImageDetails(url) {
            try {
                const response = await fetch(url);
                if (!response.ok) throw new Error('图片加载失败');

                // 1. 将响应转换为 Blob
                const blob = await response.blob();
                
                // 2. 计算文件大小 (Blob.size 单位是字节)
                const sizeInKB = (blob.size / 1024).toFixed(2);

                // 3. 获取最后修改日期 (通常远程 Blob 没有此属性,除非从 Header 解析)
                // 注意:Blob 对象本身不包含 lastModified,除非你是从 input 文件来的
                // 这里我们只能依赖 Header 中的 Last-Modified (如果有的话)
                const lastModified = response.headers.get('Last-Modified') || '未知';

                // 4. 获取图片宽高 (像素)
                return new Promise((resolve) => {
                    const img = new Image();
                    img.src = URL.createObjectURL(blob);
                    img.onload = () => {
                        resolve({
                            width: img.naturalWidth,
                            height: img.naturalHeight,
                            sizeKB: sizeInKB,
                            lastModified: lastModified
                        });
                        URL.revokeObjectURL(img.src);
                    };
                });

            } catch (error) {
                console.error('处理图片失败:', error);
                return null;
            }
        }

        // 时间戳格式化为 yyyy-MM-dd hh:mm:ss 格式
        function formatLastModified(timestamp) {
            if (!timestamp) return '未知时间';
            const date = new Date(timestamp);
            const year = date.getFullYear();
            const month = String(date.getMonth() + 1).padStart(2, '0');
            const day = String(date.getDate()).padStart(2, '0');
            const hours = String(date.getHours()).padStart(2, '0');
            const minutes = String(date.getMinutes()).padStart(2, '0');
            const seconds = String(date.getSeconds()).padStart(2, '0');
            return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
        }
        
        // 从 URL 路径中提取文件名
        function getFilenameFromUrl(url) {
            // 判断是否为相对路径(不以 http://、https://、// 开头)
            if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('//')) {
                // 确保相对路径以 / 开头
                if (!url.startsWith('/')) {
                    url = '/' + url;
                }
                // 拼接当前页面的基础 URL
                url = window.location.origin + url;
            }
            try {
                const urlObj = new URL(url);
                const pathname = urlObj.pathname;
                const filename = pathname.substring(pathname.lastIndexOf('/') + 1);
                return decodeURIComponent(filename) || 'image.jpg';
            } catch (e) {
                // URL 解析失败,尝试直接从字符串中截取
                const parts = url.split('/');
                const lastPart = parts[parts.length - 1];
                // 去除可能的查询参数
                const filename = lastPart.split('?');
                return decodeURIComponent(filename) || 'image.jpg';
            }
        }
    </script>
</body>
</html>

5、附录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狂龙骄子

独码乐,不如众码乐,乐享其中

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值