Spring-_-Bear 的 CSDN 博客导航

精品网址导航

一个高颜值、轻量级的精品网址导航工具,基于 HTML + Tailwind CSS + JavaScript 开发,支持暗黑模式、分类浏览、关键词搜索、访问次数统计等功能,适配移动端与桌面端,纯前端运行无需后端部署。

在线访问:https://static.springbear.cn/sites-nav/

功能特点

  • 🎨 暗黑模式:支持亮色/暗色主题切换,自动记忆用户偏好,适配系统主题
  • 🔍 智能搜索:支持网站名称/描述/URL 模糊搜索,快捷键 Ctrl+K 快速唤起搜索框
  • 📁 分类浏览:按分类标签筛选网站,每个分类显示网站数量,切换便捷
  • 📊 访问统计:本地记录网站访问次数,按访问次数排序展示常用网站
  • 📱 响应式布局:完美适配手机、平板、桌面等不同尺寸设备
  • ⚡ 骨架屏加载:数据加载过程中显示骨架屏,提升视觉体验
  • ✨ 交互优化:卡片 hover 动效、模态框淡入缩放、平滑过渡动画
  • 💾 本地存储:主题偏好、访问次数等数据保存在浏览器本地存储
  • 🖼️ 自动图标:自动获取网站 favicon,加载失败时显示随机占位图

使用方法

  1. 直接打开 HTML 文件即可使用(无需部署,纯前端运行)
  2. 分类浏览:点击顶部分类标签,切换不同类别的网站列表
  3. 搜索网站:
    • 点击顶部「搜索」按钮唤起搜索框
    • 或使用快捷键 Ctrl+K 快速打开搜索
    • 输入关键词模糊匹配网站,点击结果跳转
  4. 主题切换:点击顶部月亮/太阳图标,切换亮色/暗色模式
  5. 访问统计:点击网站卡片跳转后,自动记录访问次数,列表按访问次数排序

注意事项

  • 数据存储依赖浏览器 localStorage,清空浏览器缓存会丢失主题偏好和访问次数数据
  • 网站 favicon 加载依赖第三方服务,部分网站可能无法获取图标,会自动显示占位图
  • 使用时需联网,以加载 Tailwind CSSFont Awesome 等外部资源
  • 需确保 sites-nav.json 数据文件与 HTML 文件同目录,否则无法加载网站数据
  • 快捷键 Ctrl+K 在部分浏览器/系统中可能被占用,可改用手动点击搜索按钮
  • 暗黑模式适配基于 CSS 变量,部分老旧浏览器可能显示异常

成品展示

在这里插入图片描述

项目源码

https://github.com/springbear2020/tiny-toys/tree/main/sites-nav

<!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 href="https://cdn.bootcdn.net/ajax/libs/font-awesome/7.0.1/css/all.min.css" rel="stylesheet">
    <link rel="icon" href="./sites-nav.png">
    <!-- Tailwind自定义配置 -->
    <script>
        tailwind.config = {
            darkMode: 'class',
            theme: {
                extend: {
                    colors: {
                        primary: '#165DFF',
                        secondary: '#6B7280',
                        dark: '#0F172A',
                        light: '#F8FAFC'
                    },
                    fontFamily: {
                        sans: ['Inter', 'system-ui', 'sans-serif']
                    },
                    animation: {
                        'fade-in': 'fadeIn 0.3s ease-in-out',
                        'scale-up': 'scaleUp 0.2s ease-out'
                    },
                    keyframes: {
                        fadeIn: {'0%': {opacity: '0'}, '100%': {opacity: '1'}},
                        scaleUp: {'0%': {transform: 'scale(0.95)'}, '100%': {transform: 'scale(1)'}}
                    }
                }
            }
        }
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }

            .card-hover {
                @apply transition-all duration-300 hover:scale-[1.02] hover:shadow-lg hover:border-primary/20;
            }

            .tab-active {
                @apply border-b-2 border-primary text-primary font-medium;
            }

            .scrollbar-hide {
                -ms-overflow-style: none;
                scrollbar-width: none;
            }

            .scrollbar-hide::-webkit-scrollbar {
                display: none;
            }

            /* 新增:主题图标过渡 */
            .theme-icon {
                @apply transition-all duration-300 ease-in-out;
            }
        }
    </style>
</head>
<body class="bg-light dark:bg-dark text-gray-800 dark:text-gray-200 min-h-screen flex flex-col transition-colors duration-300">
<!-- 顶部导航栏 -->
<header class="sticky top-0 z-50 bg-white/90 dark:bg-dark/90 backdrop-blur-sm border-b border-gray-200 dark:border-gray-700">
    <div class="container mx-auto px-4 py-4 flex items-center justify-between">
        <!-- Logo区域 -->
        <div class="flex items-center gap-2">
            <i class="fa fa-compass text-primary text-2xl"></i>
            <h1 class="text-xl font-bold tracking-tight">精品网址导航</h1>
        </div>

        <!-- 功能按钮区 -->
        <div class="flex items-center gap-3">
            <button id="searchBtn"
                    class="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
                <i class="fa fa-search text-gray-500 dark:text-gray-400"></i>
                <span class="hidden sm:inline text-sm">搜索</span>
                <kbd class="hidden md:inline px-2 py-0.5 text-xs bg-gray-200 dark:bg-gray-700 rounded">Ctrl+K</kbd>
            </button>
            <button id="themeToggle"
                    class="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
                <i class="fa-solid fa-moon theme-icon dark:hidden text-gray-500"></i>
                <i class="fa-solid fa-sun theme-icon hidden dark:inline text-yellow-400"></i>
            </button>
        </div>
    </div>
</header>

<!-- 搜索模态框 -->
<div id="searchModal" class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm hidden animate-fade-in">
    <div class="container mx-auto px-4 pt-20 max-w-2xl">
        <div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl overflow-hidden animate-scale-up">
            <!-- 搜索输入框 -->
            <div class="p-4 border-b border-gray-200 dark:border-gray-700">
                <div class="relative">
                    <i class="fa fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
                    <input
                            id="searchInput"
                            type="text"
                            placeholder="搜索网站名称、描述..."
                            class="w-full pl-10 pr-4 py-3 bg-gray-100 dark:bg-gray-700 rounded-lg outline-none border-0 focus:ring-2 focus:ring-primary"
                    >
                    <button id="closeSearch"
                            class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
                        <i class="fa fa-times"></i>
                    </button>
                </div>
            </div>
            <!-- 搜索结果列表 -->
            <div id="searchResults" class="max-h-[60vh] overflow-y-auto p-2 space-y-1">
                <div class="text-center text-gray-400 py-10">输入关键词开始搜索</div>
            </div>
        </div>
    </div>
</div>

<!-- 主体内容 -->
<main class="flex-1 container mx-auto px-4 py-8">
    <!-- 骨架屏加载 -->
    <div id="skeleton" class="space-y-6">
        <div class="h-10 bg-gray-200 dark:bg-gray-700 rounded w-1/3 animate-pulse"></div>
        <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
            <div class="h-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
            <div class="h-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
            <div class="h-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
            <div class="h-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
        </div>
    </div>

    <!-- 分类Tab栏 -->
    <div id="categoryTabs" class="hidden mb-6 overflow-x-auto scrollbar-hide">
        <div class="flex gap-1 sm:gap-2 min-w-full">
            <!-- 分类标签将通过JS动态生成 -->
        </div>
    </div>

    <!-- 网站卡片网格 -->
    <div id="siteGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-5 hidden">
        <!-- 网站卡片将通过JS动态生成 -->
    </div>

    <!-- 无数据提示 -->
    <div id="emptyState" class="hidden py-16 text-center">
        <i class="fa fa-folder-open-o text-4xl text-gray-300 dark:text-gray-600 mb-3"></i>
        <p class="text-gray-500 dark:text-gray-400">暂无相关网站数据</p>
    </div>
</main>

<!-- 页脚 -->
<footer class="mt-auto py-6 border-t border-gray-200 dark:border-gray-700">
    <div class="container mx-auto px-4 text-center text-sm text-gray-500 dark:text-gray-400">
        <p>© 2026 Spring-_-Bear</p>
    </div>
</footer>

<script>
    // 全局数据存储
    let siteData = {categories: [], sites: []};
    let currentCategory = 'all';

    // DOM元素获取
    const elements = {
        skeleton: document.getElementById('skeleton'),
        categoryTabs: document.getElementById('categoryTabs'),
        siteGrid: document.getElementById('siteGrid'),
        emptyState: document.getElementById('emptyState'),
        searchModal: document.getElementById('searchModal'),
        searchInput: document.getElementById('searchInput'),
        searchResults: document.getElementById('searchResults'),
        searchBtn: document.getElementById('searchBtn'),
        closeSearch: document.getElementById('closeSearch'),
        themeToggle: document.getElementById('themeToggle')
    };

    // 初始化主题
    (function initTheme() {
        const isDark = localStorage.getItem('darkMode') === 'true' ||
            (!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches);
        if (isDark) {
            document.documentElement.classList.add('dark');
        }
        // 同步切换按钮的图标显示状态
        updateThemeIcon(isDark);
    })();

    // 切换主题(修复:同步更新图标)
    elements.themeToggle.addEventListener('click', () => {
        const isDark = document.documentElement.classList.toggle('dark');
        localStorage.setItem('darkMode', isDark);
        // 更新按钮图标
        updateThemeIcon(isDark);
    });

    // 新增:统一更新主题图标状态的函数
    function updateThemeIcon(isDark) {
        const moonIcon = elements.themeToggle.querySelector('.fa-moon');
        const sunIcon = elements.themeToggle.querySelector('.fa-sun');

        if (isDark) {
            moonIcon.classList.add('hidden');
            sunIcon.classList.remove('hidden');
        } else {
            moonIcon.classList.remove('hidden');
            sunIcon.classList.add('hidden');
        }
    }

    // 搜索模态框控制
    function openSearch() {
        elements.searchModal.classList.remove('hidden');
        elements.searchInput.focus();
    }

    function closeSearch() {
        elements.searchModal.classList.add('hidden');
        elements.searchInput.value = '';
        elements.searchResults.innerHTML = '<div class="text-center text-gray-400 py-10">输入关键词开始搜索</div>';
    }

    elements.searchBtn.addEventListener('click', openSearch);
    elements.closeSearch.addEventListener('click', closeSearch);
    elements.searchModal.addEventListener('click', (e) => e.target === elements.searchModal && closeSearch());

    // 全局快捷键绑定
    document.addEventListener('keydown', (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
            e.preventDefault();
            openSearch();
        }
        if (e.key === 'Escape') closeSearch();
    });

    // 加载JSON数据
    async function fetchSiteData() {
        try {
            const response = await fetch('./sites-nav.json');
            if (!response.ok) throw new Error('数据加载失败');
            siteData = await response.json();
            renderCategories();
            renderSites(currentCategory);
            // 隐藏骨架屏,显示内容
            elements.skeleton.classList.add('hidden');
            elements.categoryTabs.classList.remove('hidden');
            elements.siteGrid.classList.remove('hidden');
        } catch (error) {
            elements.skeleton.innerHTML = `<div class="text-center py-16 text-red-500">数据加载失败:${error.message}</div>`;
            console.error(error);
        }
    }

    // 渲染分类标签
    function renderCategories() {
        const tabContainer = elements.categoryTabs.querySelector('div');
        // 全部标签
        const allCount = siteData.sites.length;
        tabContainer.innerHTML = `
        <button data-category="all" class="tab-active px-4 py-2 whitespace-nowrap rounded-lg transition-colors">
          全部 <span class="ml-1 px-1.5 text-xs bg-primary/10 text-primary rounded-full">${allCount}</span>
        </button>
      `;
        // 动态生成分类
        siteData.categories.forEach(category => {
            const count = siteData.sites.filter(s => s.category === category.id).length;
            const tab = document.createElement('button');
            tab.dataset.category = category.id;
            tab.className = 'px-4 py-2 whitespace-nowrap rounded-lg transition-colors hover:bg-gray-100 dark:hover:bg-gray-800';
            tab.innerHTML = `
          <i class="fa ${category.icon} mr-1"></i> ${category.name}
          <span class="ml-1 px-1.5 text-xs bg-gray-200 dark:bg-gray-700 rounded-full">${count}</span>
        `;
            tabContainer.appendChild(tab);
        });
        // 绑定分类切换事件
        tabContainer.querySelectorAll('button').forEach(tab => {
            tab.addEventListener('click', () => {
                currentCategory = tab.dataset.category;
                // 切换激活样式
                tabContainer.querySelectorAll('button').forEach(t => t.classList.remove('tab-active'));
                tab.classList.add('tab-active');
                renderSites(currentCategory);
            });
        });
    }

    // 更新网站访问次数到 localStorage
    function updateVisitCount(siteId) {
        const visitCounts = JSON.parse(localStorage.getItem('siteVisitCounts')) || {};
        visitCounts[siteId] = (visitCounts[siteId] || 0) + 1;
        localStorage.setItem('siteVisitCounts', JSON.stringify(visitCounts));
    }

    // 渲染网站卡片 ✅ 修复排序问题
    function renderSites(categoryId) {
        const grid = elements.siteGrid;
        grid.innerHTML = '';
        // 1. 过滤数据
        let filteredSites = categoryId === 'all'
            ? siteData.sites
            : siteData.sites.filter(site => site.category === categoryId);

        if (filteredSites.length === 0) {
            elements.emptyState.classList.remove('hidden');
            grid.classList.add('hidden');
            return;
        }
        elements.emptyState.classList.add('hidden');
        grid.classList.remove('hidden');

        // ====================== 核心修复:按访问次数 & 分类配置顺序排序 ======================
        // 创建分类顺序映射表(严格对应你categories数组的顺序)
        const categorySortMap = {};
        siteData.categories.forEach((category, index) => {
            categorySortMap[category.id] = index;
        });

        // 获取本地访问计数
        const visitCounts = JSON.parse(localStorage.getItem('siteVisitCounts')) || {};
        // 按访问次数逆序,次数相同按分类顺序
        filteredSites.sort((siteA, siteB) => {
            const countA = visitCounts[siteA.id] || 0;
            const countB = visitCounts[siteB.id] || 0;
            // 优先按访问次数逆序
            if (countB !== countA) {
                return countB - countA;
            }
            // 次数相同时按分类顺序
            const indexA = categorySortMap[siteA.category] ?? 999;
            const indexB = categorySortMap[siteB.category] ?? 999;
            return indexA - indexB;
        });
        // ========================================================================

        // 生成卡片
        filteredSites.forEach(site => {
            const card = document.createElement('a');
            card.href = site.url;
            card.target = '_blank';
            card.rel = 'noopener noreferrer';
            card.className = 'relative bg-white dark:bg-gray-800 rounded-xl p-4 border border-gray-200 dark:border-gray-700 card-hover flex flex-col gap-3';
            card.innerHTML = `
          <span class="absolute top-2 right-2 px-1.5 py-0.5 text-xs bg-primary/10 text-primary rounded-full">${visitCounts[site.id] || 0}</span>
          <div class="flex items-center gap-3">
            <img
              src="${site.icon || `https://favicon.im/${new URL(site.url).host}`}"
              alt="${site.name}"
              class="w-10 h-10 rounded-lg object-cover"
              onerror="this.src='https://picsum.photos/40/40?random=${site.name}'"
            >
            <div class="flex-1 min-w-0">
              <h3 class="font-medium truncate">${site.name}</h3>
              <p class="text-xs text-gray-500 dark:text-gray-400 truncate">${site.description || site.title}</p>
            </div>
          </div>
        `;
            card.addEventListener('click', () => {
                updateVisitCount(site.id);
            });
            grid.appendChild(card);
        });
    }

    // 搜索功能
    elements.searchInput.addEventListener('input', (e) => {
        const keyword = e.target.value.trim().toLowerCase();
        if (!keyword) {
            elements.searchResults.innerHTML = '<div class="text-center text-gray-400 py-10">输入关键词开始搜索</div>';
            return;
        }
        // 模糊搜索
        const results = siteData.sites.filter(site =>
            site.name.toLowerCase().includes(keyword) ||
            (site.title && site.title.toLowerCase().includes(keyword)) ||
            (site.description && site.description.toLowerCase().includes(keyword) ||
                (site.url && site.url.toLowerCase().includes(keyword)))
        );
        // 渲染结果
        if (results.length === 0) {
            elements.searchResults.innerHTML = '<div class="text-center text-gray-400 py-6">未找到相关网站</div>';
            return;
        }
        elements.searchResults.innerHTML = results.map(item => `
        <a href="${item.url}" target="_blank" rel="noopener noreferrer"
          onclick="updateVisitCount(${item.id})"
          class="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
          <img src="${item.icon || `https://favicon.im/${new URL(item.url).host}`}"
               class="w-8 h-8 rounded"
               onerror="this.src='https://picsum.photos/32/32?random=${item.name}'">
          <div class="flex-1 min-w-0">
            <p class="font-medium truncate">${item.name}</p>
            <p class="text-xs text-gray-500 dark:text-gray-400 truncate">${item.title || item.description}</p>
          </div>
        </a>
      `).join('');
    });

    // 初始化加载数据
    window.addEventListener('DOMContentLoaded', fetchSiteData);
</script>
</body>
</html>
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐