1.部署

具体如何部署,可查看 用 Cloudflare+Pages+KV 无需服务器,轻松部署个人导航网站(CloudNav) 这篇文章,里面有详细说明。

2.修改图标API

2.1 修改链接的图标API服务

components/LinkModal.tsx 文件中找到

// Logic to fetch icon
  const fetchIconFromUrl = (targetUrl: string) => {
      if (!targetUrl) return;
      try {
        let normalizedUrl = targetUrl;
        if (!targetUrl.startsWith('http')) {
            normalizedUrl = 'https://' + targetUrl;
        }

        // Use Google's specialized favicon service which is more robust
        // t2.gstatic.com is used by Chrome internal pages
        // fallback_opts=TYPE,SIZE,URL ensures it tries multiple ways to get an icon
        const newIcon = `https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(normalizedUrl)}&size=128`;

        setIconUrl(newIcon);
      } catch (e) {
          // invalid url
      }
  };

替换成:

// Logic to fetch icon
    const fetchIconFromUrl = (targetUrl: string) => {
        if (!targetUrl) return;
        try {
            let normalizedUrl = targetUrl;
            if (!targetUrl.startsWith('http')) {
                normalizedUrl = 'https://' + targetUrl;
            }

            // 提取域名
            const urlObj = new URL(normalizedUrl);
            const domain = urlObj.hostname;

            // 使用 faviconextractor.com 获取图标
            const newIcon = `https://www.faviconextractor.com/favicon/${domain}?larger=true`;

            setIconUrl(newIcon);
        } catch (e) {
            // invalid url
            console.error('Failed to extract domain:', e);
        }
    };

2.2 修改搜索引擎的图标API服务

components/SearchSettingsModal.tsx 文件中找到

const fetchIconFromUrl = (targetUrl: string) => {
    if (!targetUrl) return;
    try {
      let normalizedUrl = targetUrl;
      if (!targetUrl.startsWith('http')) {
          normalizedUrl = 'https://' + targetUrl;
      }

      // 尝试解析域名
      const urlObj = new URL(normalizedUrl);
      const origin = urlObj.origin;

      // 使用 Google 的 favicon 服务获取图标
      const newIconUrl = `https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(origin)}&size=128`;

      setNewIcon(newIconUrl);
    } catch (e) {
        // invalid url
    }
};

替换成:

const fetchIconFromUrl = (targetUrl: string) => {
    if (!targetUrl) return;
    try {
        let normalizedUrl = targetUrl;
        if (!targetUrl.startsWith('http')) {
            normalizedUrl = 'https://' + targetUrl;
        }

        // 提取域名
        const urlObj = new URL(normalizedUrl);
        const domain = urlObj.hostname;

        // 使用 faviconextractor.com 获取图标
        const newIcon = `https://www.faviconextractor.com/favicon/${domain}?larger=true`;

        setNewIcon(newIcon);
    } catch (e) {
        // invalid url
        console.error('Failed to extract domain:', e);
    }
};

可备用图标API站点

// Favicon Extractor
https://www.faviconextractor.com/favicon/${domain}?larger=true
// icon.bqb.cool
https://icon.bqb.cool?url=https://${domain}
// favicon.im
https://favicon.im/${domain}?larger=true
// Icon Horse
https://icon.horse/icon/${domain}
// keeweb
https://services.keeweb.info/favicon/${domain}/128
// DuckDuckGo Icons
https://icons.duckduckgo.com/ip3/${domain}.ico
// Favicon.kit
https://favicon.kit/favicon/${domain}
// Google Favicon API
https://www.google.com/s2/favicons?domain=${domain}&sz=128
// Cravatar 国内速度快,缺点:不能定义图标大小
https://cn.cravatar.com/favicon/api/index.php?url=${domain}

// 自己部署1
https://keeweb-favicon.eeen.eu.cc/favicon/${domain}/256
// 自己部署2
https://favicons.eeen.eu.cc/${domain}.ico

3.修改网站前端样式

在 Github 仓库的根目录 App.tsx 中进行修改。
如需修改每行的显示个数:

App.tsx 中搜索 grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 应该能找到两处:

置顶链接区域:

<div className={`grid gap-3 ${siteSettings.cardStyle === 'simple' ? 'grid-cols-2 md:grid-cols-5 lg:grid-cols-8 xl:grid-cols-10' : 'grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8'}`}>

分类链接区域

<div className={`grid gap-3 ${siteSettings.cardStyle === 'simple' ? 'grid-cols-2 md:grid-cols-5 lg:grid-cols-8 xl:grid-cols-10' : 'grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8'}`}>

修改成

<div className="grid gap-3 grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
  • 手机:每行2个
  • 平板:每行3个
  • 小电脑:每行4个
  • 大屏幕:每行6个

把这两处都改掉,保存提交,会重新部署,成功之后就能看到效果了!

4. 左侧菜单设置按钮仅登录用户可见

找到

<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">分类目录</span>
<button 
  onClick={() => { if(!authToken) setIsAuthOpen(true); else setIsCatManagerOpen(true); }}
  className="p-1 text-slate-400 hover:text-blue-500 hover:bg-slate-100 dark:hover:bg-slate-700 rounded"
  title="管理分类"
>
  <Settings size={14} />
</button>

整段替换成

<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">分类目录</span>
{authToken && (
 <button 
    onClick={() => { if(!authToken) setIsAuthOpen(true); else setIsCatManagerOpen(true); }}
    className="p-1 text-slate-400 hover:text-blue-500 hover:bg-slate-100 dark:hover:bg-slate-700 rounded"
    title="管理分类"
 >
    <Settings size={14} />
 </button>
)}

5.进阶功能:修改分类可见条件

修改分类模块(全员可见 / 全员隐藏 / 仅管理员可见)

①、在 Github 仓库的根目录下找到(App.tsx)文件:

修改此功能,🔧 需要修改两个地方

第一处:左侧侧边栏分类列表
找到这段代码:

{categories.map(cat => {
    const isLocked = cat.password && !unlockedCategoryIds.has(cat.id);
    const isEmoji = cat.icon && cat.icon.length <= 4 && !/^[a-zA-Z]+$/.test(cat.icon);

    return (
      <button
        key={cat.id}
        onClick={() => scrollToCategory(cat.id)}
        className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all group ${
          activeCategory === cat.id 
            ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 font-medium' 
            : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700'
        }`}
      >
        <div className={`p-1.5 rounded-lg transition-colors flex items-center justify-center ${activeCategory === cat.id ? 'bg-blue-100 dark:bg-blue-800' : 'bg-slate-100 dark:bg-slate-800'}`}>
          {isLocked ? <Lock size={16} className="text-amber-500" /> : (isEmoji ? <span className="text-base leading-none">{cat.icon}</span> : <Icon name={cat.icon} size={16} />)}
        </div>
        <span className="truncate flex-1 text-left">{cat.name}</span>
        {activeCategory === cat.id && <div className="w-1.5 h-1.5 rounded-full bg-blue-500"></div>}
      </button>
    );
})}

整段替换成:

{/* 左侧先过滤出可见的分类 */}
{categories
  .filter(cat => {
    // 如果是管理员(已登录),能看到"全员可见"和"仅管理员可见"的分类
    if (authToken) {
      // 管理员也看不到"全员隐藏"的分类
      return cat.isVisible !== false;
    }
    // 如果是普通用户,只显示"全员可见"的分类
    return cat.isVisible !== false && !cat.isAdminOnly;
  })
  .map(cat => {
    const isLocked = cat.password && !unlockedCategoryIds.has(cat.id);
    const isEmoji = cat.icon && cat.icon.length <= 4 && !/^[a-zA-Z]+$/.test(cat.icon);

    return (
      <button
        key={cat.id}
        onClick={() => scrollToCategory(cat.id)}
        className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all group ${
          activeCategory === cat.id 
            ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 font-medium' 
            : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700'
        }`}
      >
        <div className={`p-1.5 rounded-lg transition-colors flex items-center justify-center ${activeCategory === cat.id ? 'bg-blue-100 dark:bg-blue-800' : 'bg-slate-100 dark:bg-slate-800'}`}>
          {isLocked ? <Lock size={16} className="text-amber-500" /> : (isEmoji ? <span className="text-base leading-none">{cat.icon}</span> : <Icon name={cat.icon} size={16} />)}
        </div>
        <span className="truncate flex-1 text-left">{cat.name}</span>
        {activeCategory === cat.id && <div className="w-1.5 h-1.5 rounded-full bg-blue-500"></div>}
      </button>
    );
})}

第二处:右侧内容区域
找到这段代码:

{categories.map(cat => {
    let catLinks = searchResults.filter(l => l.categoryId === cat.id);
    const isLocked = cat.password && !unlockedCategoryIds.has(cat.id);

    // Logic Fix: If External Search, do NOT hide categories based on links
    // Because external search doesn't filter links.
    // However, the user probably wants to see the links grid even when typing for external search
    // Current logic: if search query exists AND local search -> filter. 
    // If search query exists AND external search -> show all (searchResults returns all).

    if (searchQuery && searchMode === 'local' && catLinks.length === 0) return null;

    return (
        <section key={cat.id} id={`cat-${cat.id}`} className="scroll-mt-24">

整段替换成:

{/* 先过滤出可见的分类,然后再渲染 */}
{categories
  .filter(cat => {
    // 如果是管理员(已登录)
    if (authToken) {
      // 管理员也看不到"全员隐藏"的分类
      return cat.isVisible !== false;
    }
    // 如果是普通用户,只显示全员可见的分类
    return cat.isVisible !== false && !cat.isAdminOnly;
  })
  .map(cat => {
    let catLinks = searchResults.filter(l => l.categoryId === cat.id);
    const isLocked = cat.password && !unlockedCategoryIds.has(cat.id);

    if (searchQuery && searchMode === 'local' && catLinks.length === 0) return null;

    return (
      <section key={cat.id} id={`cat-${cat.id}`} className="scroll-mt-24">

②、在 GitHub 上打开(components/CategoryManagerModal.tsx)文件

修改此功能,🔧 需要修改两个地方

第一处:添加 isVisible?: boolean
找到这段代码:

import React, { useState } from 'react';
import { X, ArrowUp, ArrowDown, Trash2, Edit2, Plus, Check, Lock, Merge, Smile } from 'lucide-react';
import { Category, LinkItem } from '../types';
import Icon from './Icon';

整段替换成:

import React, { useState } from 'react';
import { X, ArrowUp, ArrowDown, Trash2, Edit2, Plus, Check, Lock, Merge, Smile } from 'lucide-react';
import { Category, LinkItem } from '../types';
import Icon from './Icon';

interface CategoryWithVisibility extends Category {
  isVisible?: boolean;
}

第二处:添加是否可见下拉选项框
找到这段代码:

{/* Actions */}
{editingId !== cat.id && mergingCatId !== cat.id && (
    <div className="flex items-center gap-1 self-start mt-2">
      <button onClick={() => startEdit(cat)} className="p-1.5 text-slate-400 hover:text-blue-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded" title="编辑">
          <Edit2 size={14} />
      </button>
      <button onClick={() => openMerge(cat.id)} className="p-1.5 text-slate-400 hover:text-purple-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded" title="合并到其他分类">
          <Merge size={14} />
      </button>
      <button 
      onClick={() => { if(confirm(`确定删除"${cat.name}"分类吗?该分类下的书签将移动到"常用推荐"。`)) onDeleteCategory(cat.id); }}
      className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded"
      title="删除"
      >
      <Trash2 size={14} />
      </button>
    </div>
)}

整段替换成:

{/* Actions */}
{editingId !== cat.id && mergingCatId !== cat.id && (
    <div className="flex items-center gap-1 self-start mt-2">
      {/* ===== 是否可见下拉框(直接显示在列表里) ===== */}
      <div className="flex items-center gap-2 mr-3 border-r border-slate-200 dark:border-slate-700 pr-3">
        <select
          value={
            (cat as CategoryWithVisibility).isVisible === false ? "hidden" :
            (cat as CategoryWithVisibility).isAdminOnly === true ? "admin" : "public"
          }
          onChange={(e) => {
            const value = e.target.value;
            let isVisible = true;
            let isAdminOnly = false;

            if (value === "hidden") {
              isVisible = false;
              isAdminOnly = false;
            } else if (value === "admin") {
              isVisible = true;
              isAdminOnly = true;
            } else { // public
              isVisible = true;
              isAdminOnly = false;
            }

            const updatedCategories = categories.map(c => 
              c.id === cat.id ? { ...c, isVisible, isAdminOnly } : c
            );
            onUpdateCategories(updatedCategories);
          }}
          className="text-xs p-1.5 pr-8 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none appearance-none cursor-pointer"
          style={{
            backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
            backgroundPosition: 'right 0.5rem center',
            backgroundRepeat: 'no-repeat',
            backgroundSize: '1.5em 1.5em',
            paddingRight: '2rem'
          }}
        >
          <option value="public" className="dark:bg-slate-800">👥 全员可见</option>
          <option value="admin" className="dark:bg-slate-800">👑 仅管理员可见</option>
          <option value="hidden" className="dark:bg-slate-800">🚫 全员隐藏</option>
        </select>
      </div>
      {/* ===== 下拉框结束 ===== */}

      <button onClick={() => startEdit(cat)} className="p-1.5 text-slate-400 hover:text-blue-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded" title="编辑">
          <Edit2 size={14} />
      </button>
      <button onClick={() => openMerge(cat.id)} className="p-1.5 text-slate-400 hover:text-purple-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded" title="合并到其他分类">
          <Merge size={14} />
      </button>
      <button 
      onClick={() => { if(confirm(`确定删除"${cat.name}"分类吗?该分类下的书签将移动到"常用推荐"。`)) onDeleteCategory(cat.id); }}
      className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-slate-200 dark:hover:bg-slate-600 rounded"
      title="删除"
      >
      <Trash2 size={14} />
      </button>
    </div>
)}

③、在 Github 中找到根目录下的主页面文件(types.ts)文件

找到这段代码:

export interface Category {
  id: string;
  name: string;
  icon: string; // Lucide icon name or emoji
  password?: string; // Optional password for category protection
}

整段替换成:

export interface Category {
  id: string;
  name: string;
  icon?: string;
  password?: string;
  isVisible?: boolean;      // false = 全员隐藏
  isAdminOnly?: boolean;    // true = 仅管理员可见
}

6.仅管理可见模式,增加 [管] 标识

当选择 仅管理可见 时,左侧的分类名称,和右侧的标题后面,添加一个[管]标识,需在(App.tsx)文件中做修改

第一处:左侧侧边栏分类列表
找到这段代码

<span className="truncate flex-1 text-left">{cat.name}</span>

替换成

<span className="truncate flex-1 text-left">
  {cat.name}
  {cat.isAdminOnly && authToken && (
    <span className="ml-1.5 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400 border border-purple-200 dark:border-purple-800">管</span>
  )}
</span>

第二处:右侧内容标题
找到这段代码

<h2 className="text-lg font-bold text-slate-800 dark:text-slate-200">
  {cat.name}
</h2>
{isLocked && <Lock size={16} className="text-amber-500" />}

替换成

<div className="flex items-center gap-2">
  <h2 className="text-lg font-bold text-slate-800 dark:text-slate-200">
    {cat.name}
  </h2>
  {cat.isAdminOnly && authToken && (
    <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400 border border-purple-200 dark:border-purple-800">仅管理员</span>
  )}
  {isLocked && <Lock size={16} className="text-amber-500" />}
</div>

7.给登录验证页面增加一个关闭按钮

在 GitHub 上打开(components/AuthModal.tsx)文件

把里面整个代码替换成:

import React, { useState } from 'react';
import { Lock, ArrowRight, Loader2, X } from 'lucide-react';  // 导入 X 图标

interface AuthModalProps {
  isOpen: boolean;
  onLogin: (password: string) => Promise<boolean>;
  onClose: () => void;  // 添加 onClose 属性
}

const AuthModal: React.FC<AuthModalProps> = ({ isOpen, onLogin, onClose }) => {
  const [password, setPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');

  if (!isOpen) return null;

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);
    setError('');

    const success = await onLogin(password);
    if (!success) {
      setError('密码错误或无法连接服务器');
    }
    setIsLoading(false);
  };

  return (
    <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/80 backdrop-blur-md">
      <div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden border border-slate-200 dark:border-slate-700 p-8 relative">

        {/* ===== 新增:关闭按钮 ===== */}
        <button
          onClick={onClose}
          className="absolute right-4 top-4 p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-full transition-colors"
          aria-label="关闭"
        >
          <X size={20} />
        </button>
        {/* ===== 新增结束 ===== */}

        <div className="flex flex-col items-center mb-6">
          <div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center mb-4 text-blue-600 dark:text-blue-400">
            <Lock size={32} />
          </div>
          <h2 className="text-xl font-bold dark:text-white">身份验证</h2>
          <p className="text-sm text-slate-500 dark:text-slate-400 text-center mt-2">
            请输入部署时设置的 PASSWORD 以同步数据
          </p>
        </div>

        <form onSubmit={handleSubmit} className="space-y-4">
          <div>
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              className="w-full p-3 rounded-xl border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none transition-all text-center tracking-widest"
              placeholder="访问密码"
              autoFocus
            />
          </div>

          {error && (
            <div className="text-red-500 text-sm text-center font-medium">
              {error}
            </div>
          )}

          <button
            type="submit"
            disabled={isLoading || !password}
            className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-xl transition-colors shadow-lg shadow-blue-500/30 flex items-center justify-center gap-2"
          >
            {isLoading ? <Loader2 className="animate-spin" /> : <>解锁进入 <ArrowRight size={18} /></>}
          </button>
        </form>
      </div>
    </div>
  );
};

export default AuthModal;

第二处:找到 App.tsx 里使用 AuthModal 的地方,把 onClose 传进去:

<AuthModal isOpen={isAuthOpen} onLogin={handleLogin} />

替换成

<AuthModal 
  isOpen={isAuthOpen} 
  onLogin={handleLogin}
  onClose={() => setIsAuthOpen(false)}
/>

8.左侧底部调整

  1. 让底部的(导入、备份、设置)按钮只有管理员(已登录)可见;
  2. 增加退出登录按钮。

在 GitHub 上打开(App.tsx)文件

找到

<div className="p-4 border-t border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 shrink-0">

整段替换成

{authToken && (
<div className="p-4 border-t border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 shrink-0">
    <div className="grid grid-cols-4 gap-2 mb-2">
        <button 
            onClick={() => { if(!authToken) setIsAuthOpen(true); else setIsImportModalOpen(true); }}
            className="flex flex-col items-center justify-center gap-1 p-2 text-xs text-slate-600 dark:text-slate-300 hover:bg-white dark:hover:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 transition-all"
            title="导入书签"
        >
            <Upload size={14} />
            <span>导入</span>
        </button>
        <button 
            onClick={() => { if(!authToken) setIsAuthOpen(true); else setIsBackupModalOpen(true); }}
            className="flex flex-col items-center justify-center gap-1 p-2 text-xs text-slate-600 dark:text-slate-300 hover:bg-white dark:hover:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 transition-all"
            title="备份与恢复"
        >
            <CloudCog size={14} />
            <span>备份</span>
        </button>
        <button 
            onClick={() => setIsSettingsModalOpen(true)}
            className="flex flex-col items-center justify-center gap-1 p-2 text-xs text-slate-600 dark:text-slate-300 hover:bg-white dark:hover:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 transition-all"
            title="AI 设置"
        >
            <Settings size={14} />
            <span>设置</span>
        </button>
        {/* 新增:退出登录按钮 */}
        <button 
            onClick={() => {
                if (confirm('确定要退出登录吗?')) {
                    setAuthToken('');
                    localStorage.removeItem(AUTH_KEY);
                    setSyncStatus('idle');
                    // 可选:刷新页面或跳转
                    window.location.reload();
                }
            }}
            className="flex flex-col items-center justify-center gap-1 p-2 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800 transition-all"
            title="退出登录"
        >
            <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
                <polyline points="16 17 21 12 16 7" />
                <line x1="21" x2="9" y1="12" y2="12" />
            </svg>
            <span>退出</span>
        </button>
    </div>

    <div className="flex items-center justify-between text-xs px-2 mt-2">
       <div className="flex items-center gap-1 text-slate-400">
         {syncStatus === 'saving' && <Loader2 className="animate-spin w-3 h-3 text-blue-500" />}
         {syncStatus === 'saved' && <CheckCircle2 className="w-3 h-3 text-green-500" />}
         {syncStatus === 'error' && <AlertCircle className="w-3 h-3 text-red-500" />}
         <span className="text-green-600">已同步</span>
       </div>
    </div>
</div>
)}

{/* 未登录时显示简单的登录提示 */}
{!authToken && (
  <div className="flex sm:hidden p-4 border-t border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 shrink-0">
    <button
      onClick={() => setIsAuthOpen(true)}
      className="w-full flex items-center justify-center gap-2 py-2 text-xs text-slate-600 dark:text-slate-400 hover:bg-white dark:hover:bg-slate-700 rounded-lg border border-slate-200 dark:border-slate-600 transition-all"
    >
      <Lock size={14} />
      <span>登录</span>
    </button>
  </div>
)}

9.顶部右侧调整

在 GitHub 上打开(App.tsx)文件

找到

<div className="flex items-center gap-2">
  <div className="hidden md:flex bg-slate-100 dark:bg-slate-700 rounded-lg p-1 mr-2">
    ... 原有内容 ...
    <Plus size={16} /> <span className="hidden sm:inline">添加</span>
  </button>
</div>

整段替换成

<div className="flex items-center gap-2 ml-3">
{/* 模式切换按钮 - 仅登录用户显示 */}
{authToken && (
  <div className="flex bg-slate-100 dark:bg-slate-700 rounded-lg p-1">
    <button 
      onClick={() => updateData(links, categories, { ...siteSettings, cardStyle: 'simple' })}
      title="简约模式"
      className={`p-1.5 rounded transition-all ${
        siteSettings.cardStyle === 'simple' 
          ? 'bg-white dark:bg-slate-600 shadow text-blue-600' 
          : 'text-slate-400 hover:text-slate-600'
      }`}
    >
      <LayoutGrid size={16} />
    </button>
    <button 
      onClick={() => updateData(links, categories, { ...siteSettings, cardStyle: 'detailed' })}
      title="详情模式"
      className={`p-1.5 rounded transition-all ${
        siteSettings.cardStyle === 'detailed' 
          ? 'bg-white dark:bg-slate-600 shadow text-blue-600' 
          : 'text-slate-400 hover:text-slate-600'
      }`}
    >
      <List size={16} />
    </button>
  </div>
)}

<button onClick={toggleTheme} className="p-2 rounded-full text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700">
  {darkMode ? <Sun size={18} /> : <Moon size={18} />}
</button>

{!authToken && (
    <button onClick={() => setIsAuthOpen(true)} className="flex items-center gap-2 bg-slate-200 dark:bg-slate-700 px-3 py-1.5 rounded-full text-xs font-medium">
        <Cloud size={14} /> 登录
    </button>
)}

{authToken && (
  <button
    onClick={() => { if(!authToken) setIsAuthOpen(true); else { setEditingLink(undefined); setIsModalOpen(true); }}}
    className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-full text-sm font-medium shadow-lg shadow-blue-500/30"
  >
    <Plus size={16} /> <span className="hidden sm:inline">添加</span>
  </button>
)}
</div>

10. 移动端显示搜索框

在 GitHub 上打开(App.tsx)文件

找到

{/* Redesigned Search Bar */}
<div className="relative w-full max-w-xl hidden sm:flex items-center gap-3">
    {/* Search Mode Toggle (Pill) */}
    <div className="bg-slate-100 dark:bg-slate-700 p-1 rounded-full flex items-center shrink-0">

替换成

{/* Redesigned Search Bar */}
<div className="relative w-full max-w-xl flex items-center gap-3">
    {/* Search Mode Toggle (Pill) */}
    <div className="bg-slate-100 dark:bg-slate-700 p-1 rounded-full hidden sm:flex items-center shrink-0">

11. 鼠标右键弹窗菜单修改

在 GitHub 上打开(App.tsx)文件

11.1 修改 renderLinkCard 函数,使移动端也可支持编辑链接

找到

const renderLinkCard = (link: LinkItem) => {
      const iconDisplay = link.icon ? (
        ... 原有内容 ...
          )}
        </a>
      );
  };

整段替换成

const renderLinkCard = (link: LinkItem) => {
    const iconDisplay = link.icon ? (
       <img 
          src={link.icon} 
          alt="" 
          className="w-5 h-5 object-contain" 
          onError={(e) => {
              e.currentTarget.style.display = 'none';
              e.currentTarget.parentElement!.innerText = link.title.charAt(0);
          }}
       />
    ) : link.title.charAt(0);

    const isSimple = siteSettings.cardStyle === 'simple';

    // 使用普通的 let 变量,而不是 useRef
    let longPressTimer: NodeJS.Timeout | null = null;

    const handleTouchStart = (e: React.TouchEvent, link: LinkItem) => {
      longPressTimer = setTimeout(() => {
        // 触发长按菜单
        let x = e.touches[0].clientX;
        let y = e.touches[0].clientY;
        // 边界调整
        if (x + 180 > window.innerWidth) x = window.innerWidth - 190;
        if (y + 220 > window.innerHeight) y = window.innerHeight - 230;
        setContextMenu({ x, y, link });

        // 轻微震动(如果设备支持)
        if (navigator.vibrate) {
          navigator.vibrate(50);
        }
      }, 500); // 长按500ms触发
    };

    const handleTouchEnd = () => {
      if (longPressTimer) {
        clearTimeout(longPressTimer);
        longPressTimer = null;
      }
    };

    const handleTouchMove = () => {
      if (longPressTimer) {
        clearTimeout(longPressTimer);
        longPressTimer = null;
      }
    };

    return (
      <a
          key={link.id}
          href={link.url}
          target="_blank"
          rel="noopener noreferrer"
          onContextMenu={(e) => {
              e.preventDefault();
              e.stopPropagation();
              let x = e.clientX;
              let y = e.clientY;
              if (x + 180 > window.innerWidth) x = window.innerWidth - 190;
              if (y + 220 > window.innerHeight) y = window.innerHeight - 230;
              setContextMenu({ x, y, link });
              return false;
          }}
          // 移动端长按事件
          onTouchStart={(e) => handleTouchStart(e, link)}
          onTouchEnd={handleTouchEnd}
          onTouchMove={handleTouchMove}
          onTouchCancel={handleTouchEnd}
          className={`group relative flex flex-col ${isSimple ? 'p-2' : 'p-3'} bg-white dark:bg-slate-800 rounded-xl border border-slate-100 dark:border-slate-700/50 shadow-sm hover:shadow-lg hover:border-blue-200 dark:hover:border-slate-600 hover:-translate-y-0.5 transition-all duration-200 hover:bg-blue-50 dark:hover:bg-slate-750`}
          title={link.description || link.url}
      >
          <div className={`flex items-center gap-3 ${isSimple ? '' : 'mb-1.5'} pr-6`}>
              <div className={`${isSimple ? 'w-6 h-6 text-xs' : 'w-8 h-8 text-sm'} rounded-lg bg-slate-50 dark:bg-slate-700 text-blue-600 dark:text-blue-400 flex items-center justify-center font-bold uppercase shrink-0 overflow-hidden`}>
                  {iconDisplay}
              </div>
              <h3 className="font-medium text-sm text-slate-800 dark:text-slate-200 truncate flex-1 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
                  {link.title}
              </h3>
          </div>
          {!isSimple && (
              <div className="text-xs text-slate-500 dark:text-slate-400 line-clamp-1 h-4 w-full overflow-hidden">
                  {link.description || <span className="opacity-0">.</span>}
              </div>
          )}
      </a>
    );
};

11.2 未登录用户不显示编辑、置顶、删除

找到

{/* Right Click Context Menu */}
      {contextMenu && (
        <div 
            ref={contextMenuRef}
            ... 原有内容 ...
                  <Trash2 size={16}/> <span>删除链接</span>
              </button>
          </>
      )}
  </div>
)}

整段替换成

{/* Right Click Context Menu */}
{contextMenu && (
  <div 
      ref={contextMenuRef}
      className="fixed z-[9999] bg-white dark:bg-slate-800 rounded-xl shadow-2xl border border-slate-100 dark:border-slate-600 w-44 py-2 flex flex-col animate-in fade-in zoom-in duration-100 overflow-hidden"
      style={{ top: contextMenu.y, left: contextMenu.x }}
      onClick={(e) => e.stopPropagation()}
      onContextMenu={(e) => e.preventDefault()}
  >
      {/* 复制链接 - 所有人都可以 */}
      <button 
          onClick={() => { handleCopyLink(contextMenu.link!.url); setContextMenu(null); }} 
          className="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-colors text-left"
      >
          <Copy size={16} className="text-slate-400"/> <span>复制链接</span>
      </button>

      {/* 显示二维码 - 所有人都可以 */}
      <button 
          onClick={() => { setQrCodeLink(contextMenu.link); setContextMenu(null); }} 
          className="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-colors text-left"
      >
          <QrCode size={16} className="text-slate-400"/> <span>显示二维码</span>
      </button>

      {/* 只有登录用户才显示下面的操作 */}
      {authToken && (
          <>
              <div className="h-px bg-slate-100 dark:bg-slate-700 my-1 mx-2"/>

              {/* 编辑链接 */}
              <button 
                  onClick={() => { 
                      setEditingLink(contextMenu.link!); 
                      setIsModalOpen(true); 
                      setContextMenu(null); 
                  }} 
                  className="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-colors text-left"
              >
                  <Edit2 size={16} className="text-slate-400"/> <span>编辑链接</span>
              </button>

              {/* 置顶/取消置顶 */}
              <button 
                  onClick={() => { 
                      togglePin(contextMenu.link!.id); 
                      setContextMenu(null); 
                  }} 
                  className="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 transition-colors text-left"
              >
                  <Pin size={16} className={contextMenu.link!.pinned ? "fill-current text-blue-500" : "text-slate-400"}/> 
                  <span>{contextMenu.link!.pinned ? '取消置顶' : '置顶'}</span>
              </button>

              <div className="h-px bg-slate-100 dark:bg-slate-700 my-1 mx-2"/>

              {/* 删除链接 */}
              <button 
                  onClick={() => { 
                      handleDeleteLink(contextMenu.link!.id); 
                      setContextMenu(null); 
                  }} 
                  className="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-red-50 dark:hover:bg-red-900/20 text-red-500 transition-colors text-left"
              >
                  <Trash2 size={16}/> <span>删除链接</span>
              </button>
          </>
      )}
  </div>
)}

12. 修改初始化数据

在 GitHub 上打开(types.ts)文件

12.1 默认分类

找到

export const DEFAULT_CATEGORIES: Category[] = [
  { id: 'common', name: '常用推荐', icon: 'Star' },
  { id: 'dev', name: '开发工具', icon: 'Code' },
  { id: 'design', name: '设计资源', icon: 'Palette' },
  { id: 'read', name: '阅读资讯', icon: 'BookOpen' },
  { id: 'ent', name: '休闲娱乐', icon: 'Gamepad2' },
  { id: 'ai', name: '人工智能', icon: 'Bot' },
];

在这里面修改

12.2 默认链接

找到

export const INITIAL_LINKS: LinkItem[] = [
  { id: '1', title: 'GitHub', url: 'https://github.com', categoryId: 'dev', createdAt: Date.now(), description: '代码托管平台', pinned: true },
  { id: '2', title: 'ChatGPT', url: 'https://chat.openai.com', categoryId: 'ai', createdAt: Date.now(), description: 'OpenAI聊天机器人', pinned: true },
  { id: '3', title: 'Gemini', url: 'https://gemini.google.com', categoryId: 'ai', createdAt: Date.now(), description: 'Google DeepMind AI' },
];

在这里面修改

12.3 默认搜索引擎

找到

export const DEFAULT_SEARCH_ENGINES: SearchEngine[] = [
    { id: 'local', name: '站内', url: '', icon: 'Search' },
    { id: 'google', name: 'Google', url: 'https://www.google.com/search?q=', icon: 'https://www.faviconextractor.com/favicon/www.google.com?larger=true' },
    { id: 'baidu', name: '百度', url: 'https://www.baidu.com/s?wd=', icon: 'https://www.faviconextractor.com/favicon/www.baidu.com?larger=true' },
    { id: 'bing', name: '必应', url: 'https://www.bing.com/search?q=', icon: 'https://www.faviconextractor.com/favicon/www.bing.com?larger=true' },
    { id: 'bilibili', name: 'B站', url: 'https://search.bilibili.com/all?keyword=', icon: 'https://www.faviconextractor.com/favicon/www.bilibili.com?larger=true' },
];

在这里面修改