• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

zzaz

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

全栈实战:复刻4K高清壁纸图库(完整代码方案)

一、项目背景与站点需求分析

目标站点:https://bbab.net/images/gallery/,是一套每日更新4K高清电脑桌面壁纸系统,核心特征:

  1. 内容分层:风景、建筑、动物、城市多分类壁纸;
  2. 产品定位:CC0 无版权商用高清图库,海量 4K/8K 电脑壁纸,每日自动同步上新素材;
  3. 页面能力:响应式瀑布流、图片懒加载、时间线归档、分页、缩略图/原图 CDN 分离、图片预加载优化;
  4. 技术痛点:海量图片加载性能、海量素材分页查询、静态 CDN 分发、SEO 日期归档页面。

本文提供Node.js Express 后端 + 原生 HTML/CSS/JS 前端 + SQLite 轻量数据库完整可运行代码,从零搭建同款壁纸画廊。

th

二、整体技术架构

技术栈选型

分层 技术 作用
后端服务 Node.js + Express 提供壁纸分页、日期归档、分类筛选 API
数据库 sqlite3 存储壁纸元数据(日期、分类、缩略图、原图地址)
前端页面 HTML5 + CSS3 Grid/Flex + 原生JS 画廊瀑布流、时间归档侧边栏、懒加载
静态资源 CDN 分离 缩略图小图预加载,原图高清分发
优化方案 IntersectionObserver 懒加载、WebP 格式、分页 LIMIT 偏移查询 解决大图加载卡顿

目录结构

bbab-gallery/
├── server/
│   ├── db.js        # 数据库初始化
│   ├── index.js     # Express 服务入口
│   ├── routes.js    # 画廊接口
│   └── seed.js      # 测试壁纸数据填充
├── public/
│   ├── index.html   # 画廊主页面 /images/gallery
│   ├── css/style.css
│   └── js/gallery.js
├── gallery.db       # SQLite 数据库文件
└── package.json

三、后端完整代码实现

1. package.json 依赖配置

{
  "name": "bbab-wallpaper-gallery",
  "version": "1.0.0",
  "description": "复刻https://bbab.net/images/gallery/壁纸图库",
  "main": "server/index.js",
  "scripts": {
    "dev": "nodemon server/index.js",
    "start": "node server/index.js",
    "seed": "node server/seed.js"
  },
  "dependencies": {
    "express": "^4.19.2",
    "sqlite3": "^5.1.7",
    "cors": "^2.8.5"
  },
  "devDependencies": {
    "nodemon": "^3.1.4"
  }
}

2. 数据库初始化 server/db.js

const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('./gallery.db');

// 创建壁纸表:包含日期、分类、CDN地址、宽高
db.serialize(() => {
  db.run(`CREATE TABLE IF NOT EXISTS wallpapers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    category TEXT NOT NULL, -- 风景/建筑/动物/城市
    upload_date TEXT NOT NULL, -- YYYY-MM-DD 归档日期
    thumb_url TEXT NOT NULL, -- CDN缩略图
    full_url TEXT NOT NULL, -- 高清原图
    width INTEGER,
    height INTEGER,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  )`);

  // 创建日期归档视图(用于侧边栏时间列表)
  db.run(`CREATE VIEW IF NOT EXISTS date_archive AS
    SELECT DISTINCT upload_date FROM wallpapers
    ORDER BY upload_date DESC`);
});

module.exports = db;

3. 测试数据填充 server/seed.js

模拟站点 2026 每日更新壁纸数据:

const db = require('./db');
const sampleData = [
  {
    title: "山间云海自然风光",
    category: "风景",
    upload_date: "2026-08-10",
    thumb_url: "/thumbs/0810/001.webp",
    full_url: "/orig/0810/001.webp",
    width: 3840, height: 2160
  },
  {
    title: "城市摩天大楼夜景",
    category: "城市",
    upload_date: "2026-08-10",
    thumb_url: "/thumbs/0810/002.webp",
    full_url: "/orig/0810/002.webp",
    width: 3840, height: 2160
  },
  {
    title: "森林小鹿野生动物",
    category: "动物",
    upload_date: "2026-08-09",
    thumb_url: "/thumbs/0809/001.webp",
    full_url: "/orig/0809/001.webp",
    width: 3840, height: 2160
  }
];

const stmt = db.prepare(`
  INSERT INTO wallpapers (title,category,upload_date,thumb_url,full_url,width,height)
  VALUES (?,?,?,?,?,?,?)
`);

sampleData.forEach(item => {
  stmt.run(item.title, item.category, item.upload_date, item.thumb_url, item.full_url, item.width, item.height);
});
stmt.finalize(() => {
  console.log("测试数据插入完成");
  db.close();
});

4. 接口路由 server/routes.js

提供三大核心接口:分页壁纸列表、日期归档列表、按日期筛选壁纸

const express = require('express');
const router = express.Router();
const db = require('./db');
const PAGE_SIZE = 12;

// 1. 获取全部日期归档(侧边栏时间列表)
router.get("/archive-dates", (req, res) => {
  db.all("SELECT * FROM date_archive", (err, rows) => {
    if(err) return res.status(500).json({code:500,msg:"数据库错误",err});
    res.json({code:200,data:rows});
  });
});

// 2. 分页获取壁纸,支持日期筛选 /api/wallpapers?page=1&date=2026-08-10
router.get("/wallpapers", (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const filterDate = req.query.date || "";
  const offset = (page - 1) * PAGE_SIZE;
  let sql = "SELECT * FROM wallpapers";
  let params = [];
  if(filterDate){
    sql += " WHERE upload_date = ?";
    params.push(filterDate);
  }
  sql += " ORDER BY upload_date DESC LIMIT ?,?";
  params.push(offset, PAGE_SIZE);

  // 查询当前页数据
  db.all(sql, params, (err, list) => {
    if(err) return res.status(500).json({code:500,msg:"查询失败"});
    // 查询总数计算分页
    let countSql = "SELECT COUNT(id) total FROM wallpapers";
    let countParams = [];
    if(filterDate){
      countSql += " WHERE upload_date = ?";
      countParams.push(filterDate);
    }
    db.get(countSql, countParams, (_, row) => {
      const total = row.total;
      const totalPage = Math.ceil(total / PAGE_SIZE);
      res.json({
        code:200,
        data:{
          list, page, PAGE_SIZE, total, totalPage
        }
      });
    });
  });
});

module.exports = router;

5. 服务入口 server/index.js

const express = require('express');
const cors = require('cors');
const routes = require('./routes');
const app = express();
const PORT = 3000;

// 中间件
app.use(cors());
app.use(express.static("public")); // 托管前端静态页面
app.use("/api", routes);

// 画廊页面路由,映射 /images/gallery
app.get("/images/gallery", (req, res) => {
  res.sendFile(__dirname + "/../public/index.html");
});

app.listen(PORT, () => {
  console.log(`服务运行 http://localhost:${PORT}/images/gallery`);
});

四、前端页面完整代码(复刻画廊 UI)

1. public/index.html 主页面

实现:顶部分类导航、左侧日期归档侧边栏、中间瀑布流画廊、分页控件、图片懒加载

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>每日高清壁纸 | bbab Gallery</title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <header class="site-header">
    <h1>发现每日精彩壁纸</h1>
    <nav class="category-nav">
      <span class="cat-item active" data-cat="all">全部</span>
      <span class="cat-item" data-cat="风景">风景</span>
      <span class="cat-item" data-cat="建筑">建筑</span>
      <span class="cat-item" data-cat="动物">动物</span>
      <span class="cat-item" data-cat="城市">城市</span>
    </nav>
  </header>

  <div class="container">
    <!-- 左侧日期归档 -->
    <aside class="archive-sidebar">
      <h3>更新归档</h3>
      <div id="dateList" class="date-list"></div>
    </aside>
    <!-- 壁纸画廊主区域 -->
    <main class="gallery-main">
      <div id="wallGrid" class="wall-grid"></div>
      <div id="pageBox" class="page-box"></div>
    </main>
  </div>

  <script src="/js/gallery.js"></script>
</body>
</html>

2. public/css/style.css 样式(还原原图站布局)

*{margin:0;padding:0;box-sizing:border-box;font-family:system-ui}
body{background:#f5f7fa;color:#222}
.site-header{padding:20px 40px;border-bottom:1px solid #eee}
.site-header h1{font-size:22px;margin-bottom:12px}
.category-nav{display:flex;gap:16px}
.cat-item{padding:6px 14px;border-radius:99px;background:#eee;cursor:pointer}
.cat-item.active{background:#2563eb;color:#fff}

.container{display:flex;max-width:1400px;margin:0 auto;padding:24px;gap:24px}
.archive-sidebar{width:220px;flex-shrink:0}
.date-list{margin-top:12px;display:flex;flex-direction:column;gap:8px}
.date-item{padding:8px 10px;border-radius:6px;cursor:pointer}
.date-item:hover{background:#eef2ff}
.date-item.active{background:#dbeafe;color:#2563eb;font-weight:500}

.gallery-main{flex:1}
.wall-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:18px}
.wall-card{border-radius:10px;overflow:hidden;background:#fff;box-shadow:0 2px 12px #0000000d}
.wall-card img{width:100%;height:160px;object-fit:cover;display:block}
.wall-card p{padding:10px;font-size:14px;color:#444}

.page-box{margin-top:30px;display:flex;gap:10px;justify-content:center}
.page-btn{padding:8px 16px;border:1px solid #ddd;border-radius:6px;cursor:pointer}
.page-btn.active{background:#2563eb;color:#fff;border-color:#2563eb}

3. public/js/gallery.js 交互逻辑(懒加载+分页+日期筛选)

// 全局状态
const state = {
  currentPage:1,
  selectDate:"",
  dateListEl:document.getElementById("dateList"),
  gridEl:document.getElementById("wallGrid"),
  pageEl:document.getElementById("pageBox")
};

// 1. 初始化加载日期归档列表
async function loadArchiveDates(){
  const res = await fetch("/api/archive-dates");
  const {data} = await res.json();
  state.dateListEl.innerHTML = "";
  data.forEach(item=>{
    const div = document.createElement("div");
    div.className = "date-item";
    div.innerText = item.upload_date;
    div.dataset.date = item.upload_date;
    // 日期点击筛选壁纸
    div.onclick = ()=>{
      document.querySelectorAll(".date-item").forEach(d=>d.classList.remove("active"));
      div.classList.add("active");
      state.selectDate = item.upload_date;
      state.currentPage = 1;
      loadWallpapers();
    }
    state.dateListEl.appendChild(div);
  })
}

// 2. 加载壁纸列表并渲染
async function loadWallpapers(){
  const params = new URLSearchParams();
  params.append("page",state.currentPage);
  if(state.selectDate) params.append("date",state.selectDate);
  const res = await fetch(`/api/wallpapers?${params.toString()}`);
  const {data} = await res.json();
  const {list,totalPage} = data;
  state.gridEl.innerHTML = "";

  // 渲染卡片 + 图片懒加载
  list.forEach(wall=>{
    const card = document.createElement("div");
    card.className = "wall-card";
    card.innerHTML = `
      <img data-src="${wall.thumb_url}" alt="${wall.title}" class="lazy-img">
      <p>${wall.title}</p>
    `;
    state.gridEl.appendChild(card);
  })
  // 开启懒加载
  initLazyLoad();
  // 渲染分页
  renderPage(totalPage);
}

// 3. IntersectionObserver 图片懒加载
function initLazyLoad(){
  const imgs = document.querySelectorAll(".lazy-img");
  const observer = new IntersectionObserver((entries)=>{
    entries.forEach(entry=>{
      if(entry.isIntersecting){
        const img = entry.target;
        img.src = img.dataset.src;
        observer.unobserve(img);
      }
    })
  },{rootMargin:"120px"});
  imgs.forEach(img=>observer.observe(img));
}

// 4. 分页渲染
function renderPage(totalPage){
  state.pageEl.innerHTML = "";
  for(let i=1;i<=totalPage;i++){
    const btn = document.createElement("button");
    btn.className = "page-btn";
    if(i === state.currentPage) btn.classList.add("active");
    btn.innerText = i;
    btn.onclick = ()=>{
      state.currentPage = i;
      loadWallpapers();
      window.scrollTo(0,0);
    }
    state.pageEl.appendChild(btn);
  }
}

// 页面加载执行
window.onload = async ()=>{
  await loadArchiveDates();
  loadWallpapers();
}

五、部署运行完整步骤

  1. 安装依赖
npm install
  1. 初始化数据库并插入测试壁纸数据
npm run seed
  1. 启动开发服务
npm run dev
  1. 访问画廊页面:http://localhost:3000/images/gallery

六、站点核心功能优化方案(对标 bbab.net 原版)

1. 海量图片性能优化

  1. CDN 分离缩略图/原图:缩略图采用 WebP 低分辨率预加载,原图点击弹窗加载;
  2. IntersectionObserver 懒加载:仅视口内图片发起请求,减少网络消耗;
  3. 数据库分页 LIMIT 偏移查询,禁止一次性全量查询百万级壁纸;
  4. 静态资源开启 Nginx 缓存、gzip 压缩。

2. 日期归档业务逻辑

原版站点核心特色是按更新日期倒序归档,数据库使用视图快速去重日期列表,前端点击日期筛选对应当日上新壁纸,完全还原页面左侧时间线导航。

3. 扩展功能迭代(可二次开发)

  1. 分类筛选接口:增加 category 参数过滤风景/城市壁纸;
  2. 图片弹窗预览:点击卡片弹出高清原图下载按钮;
  3. 定时爬虫脚本:每日自动同步 CC0 图库素材,实现站点“每日更新”特性;
  4. 搜索接口:基于壁纸标题模糊搜索;
  5. SEO 优化:日期归档独立静态页面、图片 alt 标签、sitemap.xml。

七、总结

本文完整复现 https://bbab.net/images/gallery/ 壁纸图库的日期归档、瀑布流画廊、分页、图片懒加载核心能力,全栈代码开箱即用,轻量 SQLite 无需复杂数据库环境,适合独立开发者搭建高清壁纸、摄影素材展示类站点。

posted on 2026-08-10 13:35  独立开发者+  阅读(180)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3