[分享] 一个简易的单文件本地web图库预览程序

希米  (UID: 5950) [复制链接]
帖子链接已复制到剪贴板
帖子已经有人评论啦,不支持删除!

59 1
简介
 
本地web图库,支持目录树浏览,原生体验,Telegraph风格,macOS化UI,支持移动端浏览。
 
上手简单,就一个html文件,无需配置服务器,放置图片存储的上级目录即可,支持多级目录预览,比系统自带预览更方便一点

使用方法
 
 选择图片目录: 点击左侧边栏的 更新图库配置,选中网页当前目录内存储图片的文件夹。
 
 保存配置文件: 浏览器会自动扫描目录内(包含子目录)所有图片并下载 setting.js 数据文件。
 
 保存或替换并刷新: 将该配置移动到本网页所在目录,按 F5 或 ⌘ R 刷新即可。

 
预览
 
 临时预览站点:   https://app.hhqq.net/img
 
59uIteST3Jy647cj3gUerO3zJM3D6vlN.webp
eveT03JtoJRO6RbSdMwU4sChW4kY1643.webp
J7tOXGLpzUhdpx4CIbNduG8p9mUVlNyJ.webp
 
源码下载
 
 支持目录树浏览,原生体验,Telegraph风格,macOS化UI,支持移动端浏览。
 
 下载附件或是复制下方代码另存为html即可运行
 
 
注:*本站开启了防盗链功能,禁用了迅雷等第三方工具下载,浏览器本地下载不受影响!*
 
index.html
<!-- 
		发布地址:https://www.ximi.me/post-6044.html
		版本:v1.01 
        作者:希米
        说明:本地图库,支持目录树浏览,原生体验,Telegraph风格,macOS化UI,支持移动端浏览。
		更新时间:2036-06-07
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ximi Gallery Pro</title>

<script src="setting.js"></script>

<style>
/* ================== Base & Typography ================== */
* { box-sizing: border-box; }

body {
    margin: 0;
    background: #fff;
    color: #212121;
    display: flex;
    height: 100vh;
    overflow: hidden;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    font-size: 18px;
    line-height: 1.58;
    -webkit-font-smoothing: antialiased;
}

header { margin-bottom: 35px; }

/* ================== Floating Toggle Button ================== */
.toggle-btn {
    position: fixed;
    bottom: 30px;
    left: 30px;
    width: 48px;
    height: 48px;
    border-radius: 50%;
    background: #ffffff;
    border: 1px solid #e0e0e0;
    box-shadow: 0 4px 16px rgba(0,0,0,0.08);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1001;
    color: #333;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.toggle-btn:hover {
    background: #f8f9fa;
    transform: scale(1.05);
    box-shadow: 0 6px 20px rgba(0,0,0,0.12);
}

/* ================== Sidebar & Overlay ================== */
.sidebar-overlay {
    position: fixed; top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.2);
    backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
    z-index: 999; opacity: 0; pointer-events: none; transition: opacity 0.3s ease;
}

.sidebar-overlay.show { opacity: 1; pointer-events: auto; }

.sidebar {
    width: 250px; /* Mac 侧栏通常偏窄 */
    background-color: rgba(235, 235, 239, 0.85); /* macOS 侧边栏经典底色 */
    backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
    border-right: 1px solid rgba(0,0,0,0.06);
    display: flex; flex-direction: column; overflow-y: auto;
    transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
    z-index: 1000; flex-shrink: 0;
}

.sidebar-header { padding: 20px 16px 12px 16px; }

/* 重新设计的配置按钮:Mac 辅助按钮风格 */
.btn-update {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    width: 100%; padding: 7px;
    background: #ffffff; color: #1d1d1f;
    border: 1px solid rgba(0,0,0,0.1); border-radius: 6px;
    cursor: pointer; font-size: 13px; font-weight: 500;
    transition: all 0.2s ease;
    box-shadow: 0 1px 2px rgba(0,0,0,0.03);
}

.btn-update:hover { 
    background: #fbfbfb; 
    border-color: rgba(0,0,0,0.15); 
    box-shadow: 0 1px 4px rgba(0,0,0,0.06); 
}
.btn-update input { display: none; }

/* 目录树 macOS 化:更小的字号,紧凑的行高 */
.tree-menu { 
    padding: 0 0 80px 0; margin: 0; list-style: none; 
    font-size: 13px; 
    font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif; 
}
.tree-menu ul { list-style: none; padding-left: 20px; margin: 0; display: none; }
.tree-menu li { margin: 2px 0; }

.tree-item {
    display: flex; align-items: center;
    padding: 6px 10px; margin: 0 12px;
    border-radius: 6px; cursor: pointer; user-select: none;
    color: #1d1d1f; transition: background 0.1s, color 0.1s;
}

.tree-item:hover { background-color: rgba(0,0,0,0.06); }

/* Mac 经典的蓝底白字选中态 */
.tree-item.active { 
    background-color: rgba(0, 0, 0, 0.08); /* 极淡的黑色覆盖,体现毛玻璃质感 */
    color: #000;                           /* 纯黑色字体,提升清晰度 */
    font-weight: 600;                      /* 稍微加粗,无需花哨颜色 */
}

.folder-icon { 
    display: inline-block; width: 14px; margin-right: 6px; 
    font-size: 10px; color: #86868b; 
    transition: transform 0.2s; text-align: center; 
}
.tree-item.active .folder-icon { color: rgba(255,255,255,0.8); }
.folder-open > .tree-item > .folder-icon { transform: rotate(90deg); }
.folder-open > ul { display: block; }

/* 隐藏生硬的滚动条,鼠标悬浮时才显示细条 */
.sidebar::-webkit-scrollbar { width: 0px; background: transparent; }
.sidebar:hover::-webkit-scrollbar { width: 6px; }
.sidebar::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 3px; }

/* ================== Responsive Logic ================== */
/* PC Desktop: 默认展开 */
@media (min-width: 769px) {
    .sidebar.collapsed {
        width: 0;
        border-right: none;
        overflow: hidden;
    }
}

/* Mobile: 默认隐藏并在左侧固定 */
@media (max-width: 768px) {
    .sidebar {
        position: fixed;
        height: 100vh;
        left: 0;
        top: 0;
        width: 280px;
        transform: translateX(-100%);
        box-shadow: 4px 0 24px rgba(0,0,0,0.15);
    }
    .sidebar.open { transform: translateX(0); }
    .toggle-btn { bottom: 20px; left: 20px; }
}

/* ================== Main Content (Telegraph Style) ================== */
.main-content {
    flex: 1;
    overflow-y: auto;
    background: #fff;
    scroll-behavior: smooth;
}

.tl-article { max-width: 660px; margin: 0 auto; padding: 60px 20px 100px 20px; }
.tl-content { font-family: Georgia, serif; }

figure { margin: 0 0 32px 0; padding: 0; display: block; }
figure img {
    width: 100%; height: auto; display: block; border: 0;
    background-color: #f4f5f6; transition: opacity 0.3s ease; cursor: pointer;
    border-radius: 4px;
}

h1 {
    font-family: Georgia, serif; font-size: 32px; font-weight: 700;
    line-height: 1.15; color: #000000; margin: 0 0 12px 0; letter-spacing: -0.01em;
}

.tl-meta { font-size: 15px; color: #79828b; margin: 0; }
.tl-meta .author { color: #212121d6; text-decoration: none; }
.tl-meta .divider { padding: 0 4px; color: #b0b7bd; }

</style>
</head>

<body>

<button class="toggle-btn" id="sidebarToggle" aria-label="Toggle Sidebar">
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <line x1="3" y1="12" x2="21" y2="12"></line>
        <line x1="3" y1="6" x2="21" y2="6"></line>
        <line x1="3" y1="18" x2="21" y2="18"></line>
    </svg>
</button>

<div class="sidebar-overlay" id="sidebarOverlay"></div>

<aside class="sidebar" id="sidebar">
    <div class="sidebar-header">
        <label class="btn-update">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-lucide="refresh-cw" aria-hidden="true" class="lucide lucide-refresh-cw w-4 h-4 text-gray-500"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path><path d="M8 16H3v5"></path></svg>
            更新图库配置
            <input type="file" webkitdirectory multiple onchange="handleFolderSelect(event)">
        </label>
    </div>

    <ul class="tree-menu" id="sidebarMenu"></ul>
</aside>

<!-- main -->
<main class="main-content" style="background-color: #f5f5f7; display: flex; align-items: flex-start; justify-content: center; ">
       <div class="tl-article">
        <header>
            <h1 id="galleryTitle" style="display: flex; align-items: center; gap: 10px;">
                <span style="font-size: 32px;"></span> 
            </h1>
            <div class="tl-meta" id="galleryMeta">
                <span class="author"></span>
                <span class="divider">·</span>
                <time></time>
            </div>
        </header>

        <div class="tl-content" id="galleryContent"> 
    <!-- macOS 窗口主容器 -->
    <div style="width: 100%; max-width: 540px; background: rgba(255, 255, 255, 0.75); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-radius: 14px; box-shadow: 0 10px 30px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.04); border: 1px solid rgba(0,0,0,0.08); overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;">

        <!-- 顶部 Title Bar (红黄绿按钮) -->
        <div style="height: 40px; background: rgba(246, 246, 246, 0.5); border-bottom: 1px solid rgba(0,0,0,0.06); display: flex; align-items: center; padding: 0 16px; position: relative;">
            <div style="display: flex; gap: 8px;">
                <div style="width: 12px; height: 12px; border-radius: 50%; background: #FF5F56; border: 1px solid #E0443E;"></div>
                <div style="width: 12px; height: 12px; border-radius: 50%; background: #FFBD2E; border: 1px solid #DEA123;"></div>
                <div style="width: 12px; height: 12px; border-radius: 50%; background: #27C93F; border: 1px solid #1AAB29;"></div>
            </div>
            <div style="position: absolute; left: 50%; transform: translateX(-50%); font-size: 13px; font-weight: 600; color: #4a4a4a; user-select: none;">
                设置助理
            </div>
        </div>

        <!-- 内容区域 -->
        <div style="padding: 36px 40px 44px 40px;">
            <h1 style="font-size: 26px; font-weight: 700; margin: 0 0 6px 0; color: #1d1d1f; letter-spacing: -0.4px;">欢迎使用本地图库</h1>
            <p style="font-size: 14px; color: #86868b; margin: 0 0 32px 0; line-height: 1.5;">只需完成基础配置,即可在本地以原生体验浏览你的相册。</p>

            <!-- 步骤列表 -->
            <div style="display: flex; flex-direction: column; gap: 24px;">

                <!-- Step 1 -->
                <div style="display: flex; gap: 16px;">
                    <div style="width: 28px; height: 28px; border-radius: 50%; background: #e8f2ff; color: #007aff; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; flex-shrink: 0;">1</div>
                    <div>
                        <div style="font-size: 15px; font-weight: 600; color: #1d1d1f; margin-bottom: 2px;">选择图片目录</div>
                        <div style="font-size: 13px; color: #515154; line-height: 1.5;">
                            点击左侧边栏的 <span style="display: inline-block; background: #ffffff; border: 1px solid rgba(0,0,0,0.1); border-radius: 5px; padding: 1px 6px; font-size: 11px; font-weight: 500; color: #1d1d1f; box-shadow: 0 1px 1px rgba(0,0,0,0.03); margin: 0 2px;">更新图库配置</span>,选中网页当前目录内存储图片的文件夹。
                        </div>
                    </div>
                </div>

                <!-- Step 2 -->
                <div style="display: flex; gap: 16px;">
                    <div style="width: 28px; height: 28px; border-radius: 50%; background: #e8f2ff; color: #007aff; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; flex-shrink: 0;">2</div>
                    <div>
                        <div style="font-size: 15px; font-weight: 600; color: #1d1d1f; margin-bottom: 2px;">保存配置文件</div>
                        <div style="font-size: 13px; color: #515154; line-height: 1.5;">
                            浏览器会自动扫描目录内(包含子目录)所有图片并下载 <code style="font-family: ui-monospace, SFMono-Regular, Consolas, monospace; background: rgba(0,0,0,0.05); padding: 1px 5px; border-radius: 4px; font-size: 12px; color: #d03050;">setting.js</code> 数据文件。
                        </div>
                    </div>
                </div>

                <!-- Step 3 -->
                <div style="display: flex; gap: 16px;">
                    <div style="width: 28px; height: 28px; border-radius: 50%; background: #e8f2ff; color: #007aff; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; flex-shrink: 0;">3</div>
                    <div>
                        <div style="font-size: 15px; font-weight: 600; color: #1d1d1f; margin-bottom: 2px;">保存或替换并刷新</div>
                        <div style="font-size: 13px; color: #515154; line-height: 1.5;">
                            将该配置移动到本网页所在目录,按 <span style="display: inline-block; background: #ffffff; border: 1px solid rgba(0,0,0,0.15); border-radius: 4px; padding: 1px 5px; font-size: 11px; font-weight: 600; color: #1d1d1f; box-shadow: 0 1px 0px rgba(0,0,0,0.05); border-bottom-width: 2px;">F5</span> 或 <span style="display: inline-block; background: #ffffff; border: 1px solid rgba(0,0,0,0.15); border-radius: 4px; padding: 1px 5px; font-size: 11px; font-weight: 600; color: #1d1d1f; box-shadow: 0 1px 0px rgba(0,0,0,0.05); border-bottom-width: 2px;">⌘ R</span> 刷新即可。
                        </div>
                    </div>
                </div>

            </div>

            <!-- 底部提示信息 -->
            <div style="margin-top: 36px; padding-top: 16px; border-top: 1px solid rgba(0,0,0,0.06); display: flex; align-items: flex-start; gap: 8px; color: #86868b; font-size: 12px;">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: #007aff; margin-top: 2px; flex-shrink: 0;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
                <span style="line-height: 1.5;">在左侧选择相册后,此向导会自动关闭。配置好后,未来双击打开网页即可直接看图,无需重复操作。</span>
            </div>
        </div>
    </div>
 </div>
    </div>
</main>

<script>
window.galleryData = window.galleryData || {};

/* ===================== UI 交互控制逻辑 ===================== */
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.getElementById('sidebarToggle');
const overlay = document.getElementById('sidebarOverlay');

function toggleSidebar() {
    if (window.innerWidth <= 768) {
        sidebar.classList.toggle('open');
        overlay.classList.toggle('show');
    } else {
        sidebar.classList.toggle('collapsed');
    }
}

// 绑定折叠按钮与遮罩层点击事件
toggleBtn.addEventListener('click', toggleSidebar);
overlay.addEventListener('click', () => {
    sidebar.classList.remove('open');
    overlay.classList.remove('show');
});


/* ===================== 初始化 ===================== */
function initializeApp() {
    const menu = document.getElementById('sidebarMenu');
    menu.innerHTML = '<li style="padding:20px;color:#999;">正在加载 setting.js...</li>';

    if (window.gallerySettings) {
        window.galleryData = window.gallerySettings;
    }

// 如果没有数据,直接 return,保留页面上默认显示的“使用说明”
    if (!window.galleryData || Object.keys(window.galleryData).length === 0) {
        return; 
    }

    renderSidebarTree();
}

initializeApp();

/* ===================== 侧栏渲染逻辑 ===================== */
function renderSidebarTree() {
    const menuContainer = document.getElementById('sidebarMenu');
    menuContainer.innerHTML = '';

    if (Object.keys(window.galleryData).length === 0) {
        menuContainer.innerHTML = '<li style="padding:20px;color:#999;">无可用图库</li>';
        return;
    }

    for (const [dbName, dbData] of Object.entries(window.galleryData)) {
        menuContainer.appendChild(buildTreeNodes(dbName, dbData));
    }
}

function buildTreeNodes(name, nodeData) {
    const li = document.createElement('li');
    const itemDiv = document.createElement('div');
    itemDiv.className = 'tree-item';

    const hasSubDirs = Object.keys(nodeData).some(k => k !== '_images');
    const hasImages = nodeData._images && nodeData._images.length > 0;

    // 使用更干净的符号替代 Emoji
    itemDiv.innerHTML = `<span class="folder-icon">${hasSubDirs ? '▶' : '•'}</span> <span style="word-break: break-all;">${name}</span>`;
    li.appendChild(itemDiv);

    itemDiv.onclick = (e) => {
        e.stopPropagation();

        if (hasSubDirs) li.classList.toggle('folder-open');

        document.querySelectorAll('.tree-item').forEach(el => el.classList.remove('active'));
        itemDiv.classList.add('active');

        if (hasImages) {
            renderGallery(name, nodeData._images);
            // 【手机端交互优化】:点击渲染图库后,自动收起侧边栏
            if (window.innerWidth <= 768) {
                sidebar.classList.remove('open');
                overlay.classList.remove('show');
            }
        }
    };

    if (hasSubDirs) {
        const ul = document.createElement('ul');
        for (const [subName, subData] of Object.entries(nodeData)) {
            if (subName === '_images') continue;
            ul.appendChild(buildTreeNodes(subName, subData));
        }
        li.appendChild(ul);
    }

    return li;
}

/* ===================== 图片渲染 ===================== */
function renderGallery(title, imagesArray) {
    document.getElementById('galleryTitle').textContent = title;
    
    // 获取当天的优雅日期格式,如 "July 05, 2026"
    const dateStr = new Date().toLocaleDateString('en-US', { month: 'long', day: '2-digit', year: 'numeric' });
    
    document.getElementById('galleryMeta').innerHTML = `
        <div class="tl-meta">
            <span class="author">Anonymous</span>
            <span class="divider">·</span>
            <time>${dateStr}</time>
        </div>`;

    const content = document.getElementById('galleryContent');
    content.innerHTML = '';

    imagesArray.forEach(img => {
        const name = img.split('/').pop().split('\\').pop().replace(/\.[^/.]+$/, "");
        content.innerHTML += `
            <figure>
                <img src="${img}" title="${name}" loading="lazy">
            </figure>
        `;
    });
}

 
/* ===================== 生成 setting.js (已优化排序) ===================== */
function handleFolderSelect(event) {
    const files = event.target.files;
    if (!files.length) return;

    let tree = {};
    let root = "";

    for (let file of files) {
        const ext = file.name.split('.').pop().toLowerCase();
        if (!['jpg','jpeg','png','gif','webp'].includes(ext)) continue;

        const parts = file.webkitRelativePath.split('/');

        if (!root) root = parts[0];

        let cur = tree;

        for (let i = 1; i < parts.length - 1; i++) {
            const dir = parts[i];
            if (!cur[dir]) cur[dir] = {};
            cur = cur[dir];
        }

        if (!cur._images) cur._images = [];
        cur._images.push(file.webkitRelativePath);
    }

    // --- 新增:递归排序函数 ---
    function sortTree(node) {
        for (let key in node) {
            if (key === '_images') {
                // 对图片路径按文件名进行自然排序
                node[key].sort((a, b) => {
                    const nameA = a.split('/').pop().toLowerCase();
                    const nameB = b.split('/').pop().toLowerCase();
                    return nameA.localeCompare(nameB, undefined, {numeric: true, sensitivity: 'base'});
                });
            } else if (typeof node[key] === 'object') {
                sortTree(node[key]);
            }
        }
    }

    sortTree(tree);
    // -----------------------

    const js = `window.gallerySettings = ${JSON.stringify(tree, null, 4)};`;
    downloadFile(js, "setting.js");
    alert("setting.js 已生成(图片已按名称排序),请替换后刷新页面");
}

/* ===================== download ===================== */
function downloadFile(content, name) {
    const blob = new Blob([content], { type: "text/javascript" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = name;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
}

/* ===================== 控制台致敬信息 ===================== */
window.addEventListener('load', () => {
    const consoleInfo = `
发布地址:https://www.ximi.me/post-6044.html
版本:v1.01 
作者:希米
说明:本地图库,支持目录树浏览,原生体验,Telegraph风格,macOS化UI,支持移动端浏览。
更新时间:2036-06-07`;
    
    console.log("%c " + consoleInfo, "color: #555; font-size: 13px; line-height: 1.6; padding: 10px; background: #f0f0f0; border-radius: 5px;");
});
</script>
</body>
</html>

关于
 
 1.作者:希米
 
 
 3.最后更新:2026-07-05
博客:www.ximi.me
已有评论 ( 1 )
提示:您必须 登录 才能查看此内容。
域名市场
   域名载入中...
创建新帖
自助推广 (点击空位或 这里 添加)
确认删除
确定要删除这篇帖子吗?删除后将无法恢复。
删除成功
帖子已成功删除,页面将自动刷新。
删除失败
删除帖子时发生错误,请稍后再试。