一、修改 types.ts 文件

  1. 打开 types.ts 文件
  2. 找到 Category 接口,在 password?: string; 下面添加一行:
parentId?: string;        // 父分类ID
level?: number;           // 分类层级(0=顶级,1=二级,2=三级...)
sortOrder?: number;       // 同级分类的排序顺序

二、修改 components/CategoryManagerModal.tsx 文件

1. 导入的图标新增

找到

import { X, ArrowUp, ArrowDown, Trash2, Edit2, Plus, Check, Lock, Merge, Smile } from 'lucide-react';

替换成

import { X, ArrowUp, ArrowDown, Trash2, Edit2, Plus, Check, Lock, Merge, Smile, ChevronRight, ChevronDown } from 'lucide-react';

2. 在文件顶部添加常量(放在 COMMON_ICONS 下面)

COMMON_ICONS 定义之后添加:

// 预定义常用图标列表
const COMMON_ICONS = [
  // ... 原有的图标列表 ...
];

// 添加这个常量
const NO_PARENT_VALUE = 'no-parent';

3. 新增状态变量

找到 const [newCatPassword, setNewCatPassword] = useState(‘’); 这行,在后面添加:

const [newCatParentId, setNewCatParentId] = useState<string>(NO_PARENT_VALUE);
const [editParentId, setEditParentId] = useState<string>(NO_PARENT_VALUE);

const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());  // 记录展开的文件夹

4. 修改现有函数 - 添加 parentId 支持

4.1 startEdit 函数

找到 const startEdit = (cat: Category) => { ,在里面添加一行:

const startEdit = (cat: Category) => {
  setEditingId(cat.id);
  setEditName(cat.name);
  setEditIcon(cat.icon || 'Folder');
  setEditPassword(cat.password || '');
  setEditParentId((cat as any).parentId || NO_PARENT_VALUE);  // 👈 添加这一行
  setMergingCatId(null);
};

4.2 saveEdit 函数

找到

const saveEdit = () => {
  if (!editingId || !editName.trim()) return;
  const newCats = categories.map(c => c.id === editingId ? { 
      ...c, 
      name: editName.trim(),
      icon: editIcon.trim(),
      password: editPassword.trim() || undefined
  } : c);
  onUpdateCategories(newCats);
  setEditingId(null);
};

替换成

const saveEdit = () => {
  if (!editingId || !editName.trim()) return;
  const newCats = categories.map(c => c.id === editingId ? { 
      ...c, 
      name: editName.trim(),
      icon: editIcon.trim(),
      password: editPassword.trim() || undefined,
      parentId: editParentId === NO_PARENT_VALUE ? undefined : editParentId  // ← 新增
  } : c);
  onUpdateCategories(newCats);
  setEditingId(null);
};

4.3 handleAdd 函数

找到

const handleAdd = () => {
  if (!newCatName.trim()) return;
  const newCat: Category = {
    id: Date.now().toString(),
    name: newCatName.trim(),
    icon: newCatIcon.trim() || 'Folder',
    password: newCatPassword.trim() || undefined
  };
  onUpdateCategories([...categories, newCat]);
  setNewCatName('');
  setNewCatIcon('Folder');
  setNewCatPassword('');
};

替换成

const handleAdd = () => {
  if (!newCatName.trim()) return;
  const newCat: Category = {
    id: Date.now().toString(),
    name: newCatName.trim(),
    icon: newCatIcon.trim() || 'Folder',
    password: newCatPassword.trim() || undefined,
    parentId: newCatParentId === NO_PARENT_VALUE ? undefined : newCatParentId  // ← 新增
  };
  onUpdateCategories([...categories, newCat]);
  setNewCatName('');
  setNewCatIcon('Folder');
  setNewCatPassword('');
  setNewCatParentId(NO_PARENT_VALUE);  // ← 新增
};

5. 新增辅助函数

找到

const handleAdd = () => {
    // ... 原有内容 ...
  };

在它下面添加

// 切换文件夹折叠状态
const toggleFolder = (catId: string, e: React.MouseEvent) => {
  e.stopPropagation();
  setExpandedFolders(prev => {
    const newSet = new Set(prev);
    if (newSet.has(catId)) {
      newSet.delete(catId);
    } else {
      newSet.add(catId);
    }
    return newSet;
  });
};

// 获取分类的所有子分类(递归)
const getAllChildrenIds = (parentId: string): string[] => {
  const children = categories.filter(c => c.parentId === parentId);
  return children.reduce((acc, child) => {
    return [...acc, child.id, ...getAllChildrenIds(child.id)];
  }, [] as string[]);
};

6. 渲染部分的重大改动

6.1 分类列表过滤

找到

<div className="flex-1 overflow-y-auto p-4 space-y-2">
  ... 原有内容 ...
</div>

整个替换成

<div className="flex-1 overflow-y-auto p-4 space-y-2">
  {/* 只显示顶级分类(没有 parentId 的) */}
  {categories.filter(cat => !cat.parentId).map((cat, index) => {
    // 找到这个分类在原始数组中的真实索引(用于上下移动)
    const realIndex = categories.findIndex(c => c.id === cat.id);
    return (
    <div key={cat.id} className="flex flex-col p-3 bg-slate-50 dark:bg-slate-700/50 rounded-lg group gap-2 border border-slate-100 dark:border-slate-600">
      {/* 第一行:左侧固定区域 + 排序按钮 + 图标 + 名称区域 + 操作按钮 */}
      <div className="flex items-start gap-2">

        {/* 左侧固定宽度区域:用于显示展开箭头或留白 */}
        <div className="w-6 shrink-0 flex justify-center mt-1">
          {categories.some(c => c.parentId === cat.id) && editingId !== cat.id && mergingCatId !== cat.id ? (
            // 有子分类:显示展开/折叠按钮
            <button
              onClick={(e) => toggleFolder(cat.id, e)}
              className="text-slate-400 hover:text-blue-500"
            >
              {expandedFolders.has(cat.id) ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
            </button>
          ) : (
            // 没有子分类:留出空白占位,保持对齐
            editingId !== cat.id && mergingCatId !== cat.id && <div className="w-4"></div>
          )}
        </div>

        {/* 上下箭头 - 所有分类都显示(合并模式下也显示) */}
        {editingId !== cat.id && (
          <div className="flex flex-col gap-1 mr-1 shrink-0">
            <button 
              onClick={() => handleMove(realIndex, 'up')}
              disabled={realIndex === 0}
              className="p-0.5 text-slate-400 hover:text-blue-500 disabled:opacity-30"
            >
              <ArrowUp size={14} />
            </button>
            <button 
              onClick={() => handleMove(realIndex, 'down')}
              disabled={realIndex === categories.length - 1}
              className="p-0.5 text-slate-400 hover:text-blue-500 disabled:opacity-30"
            >
              <ArrowDown size={14} />
            </button>
          </div>
        )}

        {/* 图标和文字区域 */}
        <div className="flex-1 min-w-0">
          {editingId === cat.id ? (
            // 编辑模式
            <div className="flex flex-col gap-2">
              <div className="flex gap-2">
                <div className="relative w-32 shrink-0">
                  <select
                    value={editIcon}
                    onChange={(e) => setEditIcon(e.target.value)}
                    className="w-full p-1.5 text-sm rounded border border-blue-500 dark:bg-slate-800 dark:text-white outline-none appearance-none"
                  >
                    {COMMON_ICONS.map(icon => (
                      <option key={icon.value} value={icon.value}>{icon.label}</option>
                    ))}
                  </select>
                  <div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-slate-500">
                    <Icon name={editIcon} size={14} />
                  </div>
                </div>
                <input 
                  type="text" 
                  value={editName}
                  onChange={(e) => setEditName(e.target.value)}
                  className="flex-1 p-1.5 px-2 text-sm rounded border border-blue-500 dark:bg-slate-800 dark:text-white outline-none"
                  placeholder="分类名称"
                  autoFocus
                />
              </div>
              <div className="flex items-center gap-2">
                <Lock size={14} className="text-slate-400" />
                <input 
                  type="text" 
                  value={editPassword}
                  onChange={(e) => setEditPassword(e.target.value)}
                  className="flex-1 p-1.5 px-2 text-xs rounded border border-slate-300 dark:border-slate-600 dark:bg-slate-800 dark:text-white outline-none"
                  placeholder="设置密码 (留空则不加密)"
                />
              </div>
              {/* 父分类选择 */}
              <div className="flex items-center gap-2">
                <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" className="text-slate-400">
                  <path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"></path>
                </svg>
                <select
                  value={editParentId}
                  onChange={(e) => setEditParentId(e.target.value)}
                  className="flex-1 p-1.5 px-2 text-xs rounded border border-slate-300 dark:border-slate-600 dark:bg-slate-800 dark:text-white outline-none"
                >
                  <option value={NO_PARENT_VALUE}>作为顶级分类</option>
                  {categories
                    .filter(c => c.id !== cat.id && !c.parentId)
                    .map(parent => (
                      <option key={parent.id} value={parent.id}>
                        作为「{parent.name}」子分类
                      </option>
                    ))}
                </select>
              </div>
              {/* 编辑模式下的保存和取消按钮 */}
              {editingId === cat.id && (
                <div className="flex justify-end gap-2 mt-2">
                  <button 
                    onClick={saveEdit}
                    className="px-3 py-1 bg-green-500 text-white text-sm rounded-lg hover:bg-green-600 transition-colors flex items-center gap-1"
                  >
                    <Check size={14} /> 保存
                  </button>
                  <button 
                    onClick={() => {
                      setEditingId(null);
                      setEditName('');
                      setEditIcon('Folder');
                      setEditPassword('');
                      setEditParentId(NO_PARENT_VALUE);
                    }}
                    className="px-3 py-1 bg-slate-400 text-white text-sm rounded-lg hover:bg-slate-500 transition-colors flex items-center gap-1"
                  >
                    <X size={14} /> 取消
                  </button>
                </div>
              )}
            </div>
          ) :  mergingCatId === cat.id ? (
          // 合并模式
          <div className="flex items-center gap-2 bg-blue-50 dark:bg-blue-900/20 p-2 rounded">
            <span className="text-sm dark:text-slate-200 whitespace-nowrap">合并到 →</span>
            <select 
              value={targetMergeId}
              onChange={(e) => setTargetMergeId(e.target.value)}
              className="flex-1 text-sm p-1 rounded border border-slate-300 dark:border-slate-600 dark:bg-slate-800 dark:text-white"
              style={{ maxHeight: '200px', overflowY: 'auto' }}
            >
              <option value="" disabled>请选择目标分类</option>
              {categories
                .filter(c => c.id !== cat.id) // 排除自身
                .filter(c => !c.parentId) // 先取顶级分类
                .map(topCat => {
                  // 获取这个顶级分类下的所有子分类(排除自身)
                  const children = categories.filter(c => c.parentId === topCat.id && c.id !== cat.id);

                  return (
                    <React.Fragment key={topCat.id}>
                      {/* 顶级分类 */}
                      <option value={topCat.id}>{topCat.name}</option>
                      {/* 子分类 - 缩进显示 */}
                      {children.map(child => (
                        <option key={child.id} value={child.id} style={{ paddingLeft: '24px' }}>
                          └ {child.name}
                        </option>
                      ))}
                    </React.Fragment>
                  );
                })}
            </select>
            <button onClick={executeMerge} className="text-xs bg-blue-600 text-white px-2 py-1 rounded">确认</button>
            <button onClick={() => setMergingCatId(null)} className="text-xs text-slate-500 px-2 py-1">取消</button>
          </div>
          ) : (
            // 正常显示模式
            <div className="flex items-center gap-3">
              {/* 图标 */}
              <div className="w-8 h-8 rounded bg-white dark:bg-slate-800 flex items-center justify-center text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-600 shrink-0">
                {cat.icon && cat.icon.length <= 4 && !/^[a-zA-Z]+$/.test(cat.icon) 
                  ? <span className="text-lg">{cat.icon}</span> 
                  : <Icon name={cat.icon} size={16} />
                }
              </div>

              {/* 文字信息 */}
              <div className="flex flex-col">
                {/* 分类名称 + 密码锁 */}
                <div className="flex items-center gap-2">
                  <span className="font-medium dark:text-slate-200 truncate">{cat.name}</span>
                  {cat.password && <Lock size={12} className="text-amber-500 shrink-0" />}
                </div>

                {/* 链接数量 */}
                <span className="text-xs text-slate-400">{links.filter(l => l.categoryId === cat.id).length} 个链接</span>

                {/* 可见性下拉框 */}
                <div className="pt-2">
                  <select
                    value={
                      (cat as any).isVisible === false ? "hidden" :
                      (cat as any).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 {
                        isVisible = true;
                        isAdminOnly = false;
                      }

                      const updatedCategories = categories.map(c => 
                        c.id === cat.id ? { ...c, isVisible, isAdminOnly } : c
                      );
                      onUpdateCategories(updatedCategories);
                    }}
                    className="text-xs p-1 pr-6 rounded 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.25rem center',
                      backgroundRepeat: 'no-repeat',
                      backgroundSize: '1.2em 1.2em',
                    }}
                  >
                    <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>
              </div>
            </div>
          )}
        </div>

        {/* 操作按钮 - 只在非编辑且非合并模式下显示 */}
        {editingId !== cat.id && mergingCatId !== cat.id && (
          <div className="flex items-center gap-1 ml-auto">
            <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>
        )}
      </div>
        {/* 子分类列表 */}
        {expandedFolders.has(cat.id) && (
          <div className="w-full mt-2 space-y-2">
            {categories
              .filter(sub => sub.parentId === cat.id)
              .map((sub, subIndex) => {
                const subCategoryIndex = categories.findIndex(c => c.id === sub.id);
                return (
                  <div 
                    key={sub.id} 
                    className="w-full p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700"
                  >
                    {/* 第一行:子分类基本信息 + 操作按钮 - 完全仿照顶级分类 */}
                    <div className="flex items-start gap-2">

                      {/* 上下箭头 - 子分类移动 */}
                      <div className="flex flex-col gap-1 mr-1 shrink-0">
                        <button 
                          onClick={() => {
                            // 找出当前父分类下的所有子分类
                            const siblings = categories.filter(c => c.parentId === cat.id);
                            const currentIndex = siblings.findIndex(s => s.id === sub.id);

                            if (currentIndex > 0) {
                              // 和上一个子分类交换位置
                              const newCategories = [...categories];
                              const prevSibling = siblings[currentIndex - 1];

                              // 找到这两个分类在数组中的真实位置
                              const currentPos = newCategories.findIndex(c => c.id === sub.id);
                              const prevPos = newCategories.findIndex(c => c.id === prevSibling.id);

                              // 交换它们的位置
                              [newCategories[currentPos], newCategories[prevPos]] = 
                              [newCategories[prevPos], newCategories[currentPos]];

                              onUpdateCategories(newCategories);
                            }
                          }}
                          disabled={(() => {
                            const siblings = categories.filter(c => c.parentId === cat.id);
                            const currentIndex = siblings.findIndex(s => s.id === sub.id);
                            return currentIndex === 0;
                          })()}
                          className="p-0.5 text-slate-400 hover:text-blue-500 disabled:opacity-30"
                        >
                          <ArrowUp size={14} />
                        </button>
                        <button 
                          onClick={() => {
                            // 找出当前父分类下的所有子分类
                            const siblings = categories.filter(c => c.parentId === cat.id);
                            const currentIndex = siblings.findIndex(s => s.id === sub.id);

                            if (currentIndex < siblings.length - 1) {
                              // 和下一个子分类交换位置
                              const newCategories = [...categories];
                              const nextSibling = siblings[currentIndex + 1];

                              // 找到这两个分类在数组中的真实位置
                              const currentPos = newCategories.findIndex(c => c.id === sub.id);
                              const nextPos = newCategories.findIndex(c => c.id === nextSibling.id);

                              // 交换它们的位置
                              [newCategories[currentPos], newCategories[nextPos]] = 
                              [newCategories[nextPos], newCategories[currentPos]];

                              onUpdateCategories(newCategories);
                            }
                          }}
                          disabled={(() => {
                            const siblings = categories.filter(c => c.parentId === cat.id);
                            const currentIndex = siblings.findIndex(s => s.id === sub.id);
                            return currentIndex === siblings.length - 1;
                          })()}
                          className="p-0.5 text-slate-400 hover:text-blue-500 disabled:opacity-30"
                        >
                          <ArrowDown size={14} />
                        </button>
                      </div>

                      {/* 图标和名称区域 - 完全仿照顶级分类 */}
                      <div className="flex-1 min-w-0">
                        {editingId === sub.id ? (
                          // 子分类编辑模式 - 和顶级分类保持一致
                          <div className="flex flex-col gap-2">
                            <div className="flex gap-2">
                              <div className="relative w-32 shrink-0">
                                <select
                                  value={editIcon}
                                  onChange={(e) => setEditIcon(e.target.value)}
                                  className="w-full p-1.5 text-sm rounded border border-blue-500 dark:bg-slate-800 dark:text-white outline-none appearance-none"
                                >
                                  {COMMON_ICONS.map(icon => (
                                    <option key={icon.value} value={icon.value}>{icon.label}</option>
                                  ))}
                                </select>
                                <div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-slate-500">
                                  <Icon name={editIcon} size={14} />
                                </div>
                              </div>
                              <input 
                                type="text" 
                                value={editName}
                                onChange={(e) => setEditName(e.target.value)}
                                className="flex-1 p-1.5 px-2 text-sm rounded border border-blue-500 dark:bg-slate-800 dark:text-white outline-none"
                                placeholder="分类名称"
                                autoFocus
                              />
                            </div>
                            <div className="flex items-center gap-2">
                              <Lock size={14} className="text-slate-400" />
                              <input 
                                type="text" 
                                value={editPassword}
                                onChange={(e) => setEditPassword(e.target.value)}
                                className="flex-1 p-1.5 px-2 text-xs rounded border border-slate-300 dark:border-slate-600 dark:bg-slate-800 dark:text-white outline-none"
                                placeholder="设置密码 (留空则不加密)"
                              />
                            </div>
                            <div className="flex items-center gap-2">
                              <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" className="text-slate-400">
                                <path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"></path>
                              </svg>
                              <select
                                value={editParentId}
                                onChange={(e) => setEditParentId(e.target.value)}
                                className="flex-1 p-1.5 px-2 text-xs rounded border border-slate-300 dark:border-slate-600 dark:bg-slate-800 dark:text-white outline-none"
                              >
                                <option value={NO_PARENT_VALUE}>作为顶级分类</option>
                                {categories
                                  .filter(c => c.id !== sub.id && !c.parentId)
                                  .map(parent => (
                                    <option key={parent.id} value={parent.id}>
                                      作为「{parent.name}」子分类
                                    </option>
                                  ))}
                              </select>
                            </div>
                            {/* 子分类编辑模式下的保存和取消按钮 */}
                            <div className="flex justify-end gap-2 mt-2">
                              <button 
                                onClick={saveEdit}
                                className="px-3 py-1 bg-green-500 text-white text-sm rounded-lg hover:bg-green-600 transition-colors flex items-center gap-1"
                              >
                                <Check size={14} /> 保存
                              </button>
                              <button 
                                onClick={() => {
                                  setEditingId(null);
                                  setEditName('');
                                  setEditIcon('Folder');
                                  setEditPassword('');
                                  setEditParentId(NO_PARENT_VALUE);
                                }}
                                className="px-3 py-1 bg-slate-400 text-white text-sm rounded-lg hover:bg-slate-500 transition-colors flex items-center gap-1"
                              >
                                <X size={14} /> 取消
                              </button>
                            </div>
                          </div>
                        ) : (
                          // 子分类正常显示模式
                          <div className="flex items-center gap-3">
                            {/* 图标 */}
                            <div className="w-6 h-6 rounded bg-slate-100 dark:bg-slate-700 flex items-center justify-center text-slate-500 shrink-0">
                              {sub.icon && sub.icon.length <= 4 && !/^[a-zA-Z]+$/.test(sub.icon) 
                                ? <span className="text-sm">{sub.icon}</span> 
                                : <Icon name={sub.icon} size={12} />
                              }
                            </div>

                            {/* 文字信息 */}
                            <div className="flex flex-col">
                              {/* 分类名称 + 密码锁 */}
                              <div className="flex items-center gap-2">
                                <span className="font-medium dark:text-slate-200">{sub.name}</span>
                                {sub.password && <Lock size={10} className="text-amber-500 shrink-0" />}
                              </div>

                              {/* 链接数量 */}
                              <span className="text-xs text-slate-400">
                                {links.filter(l => l.categoryId === sub.id).length} 个链接
                              </span>
                              {/* 第二行:可见性下拉框 */}
                              {editingId !== sub.id && mergingCatId !== sub.id && (
                                <div className="pt-2">
                                  <select
                                    value={
                                      (sub as any).isVisible === false ? "hidden" :
                                      (sub as any).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 {
                                        isVisible = true;
                                        isAdminOnly = false;
                                      }

                                      const updatedCategories = categories.map(c => 
                                        c.id === sub.id ? { ...c, isVisible, isAdminOnly } : c
                                      );
                                      onUpdateCategories(updatedCategories);
                                    }}
                                    className="text-xs p-1 pr-5 rounded border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 dark:text-white outline-none appearance-none cursor-pointer w-28"
                                    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.2rem center',
                                      backgroundRepeat: 'no-repeat',
                                      backgroundSize: '1em 1em',
                                    }}
                                  >
                                    <option value="public">👥 全员可见</option>
                                    <option value="admin">👑 仅管理员</option>
                                    <option value="hidden">🚫 隐藏</option>
                                  </select>
                                </div>
                              )}
                            </div>
                          </div>
                        )}
                      </div>

                      {/* 操作按钮 - 完全仿照顶级分类 */}
                      {editingId !== sub.id && mergingCatId !== sub.id && (
                        <div className="flex items-center gap-1 ml-auto shrink-0">
                          <button 
                            onClick={() => startEdit(sub)}
                            className="p-1.5 text-slate-400 hover:text-blue-500 hover:bg-slate-200 dark:hover:bg-slate-700 rounded" 
                            title="编辑"
                          >
                            <Edit2 size={14} />
                          </button>
                          <button 
                            onClick={() => openMerge(sub.id)}
                            className="p-1.5 text-slate-400 hover:text-purple-500 hover:bg-slate-200 dark:hover:bg-slate-700 rounded" 
                            title="合并"
                          >
                            <Merge size={14} />
                          </button>
                          <button 
                            onClick={() => { 
                              if(confirm(`确定删除"${sub.name}"分类吗?`)) onDeleteCategory(sub.id); 
                            }}
                            className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-slate-200 dark:hover:bg-slate-700 rounded"
                            title="删除"
                          >
                            <Trash2 size={14} />
                          </button>
                        </div>
                      )}
                    </div>
                    {/* 在这里添加子分类的合并模式显示 */}
                    {mergingCatId === sub.id && (
                      <div className="flex items-center gap-2 bg-blue-50 dark:bg-blue-900/20 p-2 rounded mt-2">
                        <span className="text-sm dark:text-slate-200 whitespace-nowrap">合并到 →</span>
                        <select 
                          value={targetMergeId}
                          onChange={(e) => setTargetMergeId(e.target.value)}
                          className="flex-1 text-sm p-1 rounded border border-slate-300 dark:border-slate-600 dark:bg-slate-800 dark:text-white"
                          style={{ maxHeight: '200px', overflowY: 'auto' }}
                        >
                          <option value="" disabled>请选择目标分类</option>
                          {categories
                            .filter(c => c.id !== sub.id) // 排除自身
                            .filter(c => !c.parentId) // 先取顶级分类
                            .map(topCat => {
                              // 获取这个顶级分类下的所有子分类(排除自身)
                              const children = categories.filter(c => c.parentId === topCat.id && c.id !== sub.id);

                              return (
                                <React.Fragment key={topCat.id}>
                                  {/* 顶级分类 */}
                                  <option value={topCat.id}>{topCat.name}</option>
                                  {/* 子分类 - 缩进显示 */}
                                  {children.map(child => (
                                    <option key={child.id} value={child.id} style={{ paddingLeft: '24px' }}>
                                      └ {child.name}
                                    </option>
                                  ))}
                                </React.Fragment>
                              );
                            })}
                        </select>
                        <button onClick={executeMerge} className="text-xs bg-blue-600 text-white px-2 py-1 rounded">确认</button>
                        <button onClick={() => setMergingCatId(null)} className="text-xs text-slate-500 px-2 py-1">取消</button>
                      </div>
                    )}
                  </div>
                );
            })}
          </div>
        )}
    </div>
  );
  })}
</div>

6.2 添加新分类

找到

<div className="p-4 border-t border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
  ... 原有内容 ...
</div>

整个替换成

<div className="p-4 border-t border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
   <label className="text-xs font-semibold text-slate-500 uppercase mb-2 block">添加新分类</label>
   <div className="flex flex-col gap-2">
     <div className="flex gap-2">
         <div className="relative w-32 shrink-0">
            <select
                value={newCatIcon}
                onChange={(e) => setNewCatIcon(e.target.value)}
                className="w-full p-2 pl-2 pr-8 rounded-lg border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white text-sm outline-none appearance-none"
            >
                {COMMON_ICONS.map(icon => (
                    <option key={icon.value} value={icon.value}>
                        {icon.label}
                    </option>
                ))}
            </select>
            <div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-slate-500">
                <Icon name={newCatIcon} size={16} />
            </div>
         </div>

         <input 
            type="text"
            value={newCatName}
            onChange={(e) => setNewCatName(e.target.value)}
            placeholder="分类名称"
            className="flex-1 p-2 rounded-lg border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 outline-none"
         />
     </div>
     <div className="flex gap-2">
         <div className="flex-1 relative">
            <Lock size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
            <input 
                type="text"
                value={newCatPassword}
                onChange={(e) => setNewCatPassword(e.target.value)}
                placeholder="密码 (可选)"
                className="w-full pl-8 p-2 rounded-lg border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 outline-none"
                onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
            />
         </div>
         {/* 添加时的父分类选择 */}
          <div className="flex items-center gap-2">
            <select
              value={newCatParentId}
              onChange={(e) => setNewCatParentId(e.target.value)}
              className="flex-1 p-2 text-sm rounded-lg border border-slate-300 dark:border-slate-600 dark:bg-slate-700 dark:text-white outline-none focus:ring-2 focus:ring-blue-500"
            >
              <option value={NO_PARENT_VALUE}>作为顶级分类</option>
              {categories
                .filter(c => !c.parentId)
                .map(parent => (
                  <option key={parent.id} value={parent.id}>
                    作为「{parent.name}」子分类
                  </option>
                ))}
            </select>
          </div>
         <button 
            onClick={handleAdd}
            disabled={!newCatName.trim()}
            className="bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white px-4 py-2 rounded-lg transition-colors flex items-center"
         >
           <Plus size={18} />
         </button>
     </div>
   </div>
</div>

三、修改 App.tsx 文件

1. 导入的图标新增

找到

import { 
  Search, Plus, Upload, Moon, Sun, Menu, 
  Trash2, Edit2, Loader2, Cloud, CheckCircle2, AlertCircle,
  Pin, Settings, Lock, CloudCog, Github, GitFork, MoreVertical,
  QrCode, Copy, LayoutGrid, List, Check, ExternalLink, ArrowRight
} from 'lucide-react';

整个替换成

import { 
  Search, Plus, Upload, Moon, Sun, Menu, 
  Trash2, Edit2, Loader2, Cloud, CheckCircle2, AlertCircle,
  Pin, Settings, Lock, CloudCog, Github, GitFork, MoreVertical,
  QrCode, Copy, LayoutGrid, List, Check, ExternalLink, ArrowRight,
  ChevronRight, ChevronDown  // ← 新增
} from 'lucide-react';

2. 新增:路由相关

找到

import React, { useState, useEffect, useMemo, useRef } from 'react';

替换成

import React, { useState, useEffect, useMemo, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';

3. 添加路由 Hooks

找到

function App() {

替换成

function App() {
  const navigate = useNavigate(); // 新增:用于页面跳转
  const { categoryId } = useParams<{ categoryId: string }>(); // 新增:获取URL参数

4. 新增辅助函数

找到

// --- Render Components ---

在上面添加下面代码

// 处理更多按钮点击 - 使用路由跳转
const handleMoreClick = (catId: string) => {
  navigate(`/cat/${catId}`);
};

// 处理返回首页 - 使用路由跳转
const handleBackToHome = () => {
  navigate('/');
};

5. 新增状态变量

找到

const mainRef = useRef<HTMLDivElement>(null);

在上面添加下面代码

const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(new Set());
const [detailCategoryId, setDetailCategoryId] = useState<string | null>(null);  // 新增:当前查看的分类详情页

6. 新增辅助函数

找到

// --- Helpers ---

在上面添加下面代码

// 新增:监听URL参数变化,同步 detailCategoryId
useEffect(() => {
  if (categoryId) {
    setDetailCategoryId(categoryId);
  } else {
    setDetailCategoryId(null);
  }
}, [categoryId]);

// 安全地获取顶级分类
const getTopLevelCategories = useMemo(() => {
  return categories.filter(cat => !cat.parentId);
}, [categories]);

// 安全地获取子分类
const getSubCategories = (parentId: string) => {
  return categories.filter(cat => cat.parentId === parentId);
};

7. 修改 loadFromLocal 函数

找到

const loadFromLocal = () => {
  const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
  ... 原有内容 ...
};

整段替换成

const loadFromLocal = () => {
  const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
  if (stored) {
    try {
      const parsed = JSON.parse(stored);
      setLinks(parsed.links || INITIAL_LINKS);
      setCategories(parsed.categories || DEFAULT_CATEGORIES);
      if (parsed.settings) setSiteSettings(prev => ({ ...prev, ...parsed.settings }));

      // 设置默认折叠状态(新增)
      const initialCollapsed = new Set<string>();
      (parsed.categories || DEFAULT_CATEGORIES).forEach((cat: Category) => {
        const hasChildren = (parsed.categories || DEFAULT_CATEGORIES).some((c: Category) => c.parentId === cat.id);
        if (hasChildren) {
          initialCollapsed.add(cat.id);
        }
      });
      setCollapsedFolders(initialCollapsed);

    } catch (e) {
      setLinks(INITIAL_LINKS);
      setCategories(DEFAULT_CATEGORIES);
    }
  } else {
    setLinks(INITIAL_LINKS);
    setCategories(DEFAULT_CATEGORIES);
  }
};

8. 修改 initData 函数

找到

if (data.links && data.links.length > 0) {
  setLinks(data.links);
  ... 原有内容 ...
  return;
}

整段替换成

if (data.links && data.links.length > 0) {
  setLinks(data.links);
  setCategories(data.categories || DEFAULT_CATEGORIES);
  if (data.settings) setSiteSettings(prev => ({ ...prev, ...data.settings }));
  localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(data));

  // 数据加载完成后,设置默认折叠状态(新增)
  const initialCollapsed = new Set<string>();
  (data.categories || DEFAULT_CATEGORIES).forEach((cat: Category) => {
      const hasChildren = (data.categories || DEFAULT_CATEGORIES).some((c: Category) => c.parentId === cat.id);
      if (hasChildren) {
          initialCollapsed.add(cat.id);
      }
  });
  setCollapsedFolders(initialCollapsed);

  return;
}

9. 左侧菜单渲染的重大改动

{/* 左侧菜单过滤,先过滤出可见的分类,然后再渲染 */}
{categories
  .filter(cat => {
    // 如果是管理员(已登录),能看到"全员可见"和"仅管理员可见"的分类
  ... 原有内容 ...
      </button>
    );
})}

整段替换成

{/* 左侧菜单过滤,先过滤出可见的分类,然后再渲染 */}
{getTopLevelCategories
  .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);

    // 获取子分类
    const subCategories = categories.filter(sub => 
      sub.parentId === cat.id && 
      (authToken ? sub.isVisible !== false : sub.isVisible !== false && !sub.isAdminOnly)
    );

    const hasChildren = subCategories.length > 0;
    // 默认是折叠的(collapsed 为 true)
    const isCollapsed = collapsedFolders.has(cat.id);

    return (
      <div key={cat.id} className="space-y-1">
        {/* 顶级分类 */}
        <div className="flex items-center w-full">
          <button
            onClick={() => scrollToCategory(cat.id)}
            className={`flex-1 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}
              {cat.isAdminOnly && authToken && (
                <span className="ml-1.5 inline-flex items-center px-1 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>
          </button>

          {/* 箭头移到右侧 */}
          {hasChildren && (
            <button
              onClick={() => {
                setCollapsedFolders(prev => {
                  const newSet = new Set(prev);
                  if (newSet.has(cat.id)) {
                    newSet.delete(cat.id);  // 如果已经折叠,就展开
                  } else {
                    newSet.add(cat.id);      // 如果已经展开,就折叠
                  }
                  return newSet;
                });
              }}
              className="p-2 ml-1 text-slate-400 hover:text-blue-500 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700"
            >
              {isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
            </button>
          )}
        </div>

        {/* 子分类 - 默认不显示(因为 isCollapsed 初始为 true) */}
        {!isCollapsed && hasChildren && (
          <div className="ml-6 space-y-1">
            {subCategories.map(sub => {
              const isSubLocked = sub.password && !unlockedCategoryIds.has(sub.id);
              const isSubEmoji = sub.icon && sub.icon.length <= 4 && !/^[a-zA-Z]+$/.test(sub.icon);

              return (
                <button
                  key={sub.id}
                  onClick={() => scrollToCategory(sub.id)}
                  className={`w-full flex items-center gap-2 px-4 py-1.5 rounded-lg transition-all text-sm ${
                    activeCategory === sub.id 
                      ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400' 
                      : 'text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700'
                  }`}
                >
                  <div className="w-4 h-4 flex items-center justify-center">
                    {isSubLocked ? <Lock size={12} className="text-amber-500" /> : (isSubEmoji ? <span className="text-xs">{sub.icon}</span> : <Icon name={sub.icon} size={12} />)}
                  </div>
                  <span className="truncate flex-1 text-left flex items-center gap-1">
                    {sub.name}
                    {/* 新增:二级分类的「管」图标 */}
                    {sub.isAdminOnly && authToken && (
                      <span className="inline-flex items-center px-1 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>
                </button>
              );
            })}
          </div>
        )}
      </div>
    );
})}

10. 右侧内容区域的重构

<div className="p-4 lg:p-8 space-y-8">
  ... 原有内容 ...
</div>

整段替换成

<div className="p-4 lg:p-8 space-y-8">
  {detailCategoryId ? (
      /* 分类详情页 - 替换首页内容 */
      (() => {
          const cat = categories.find(c => c.id === detailCategoryId);
          if (!cat) return null;

          const subCategories = getSubCategories(cat.id).filter(sub => {
              if (authToken) return sub.isVisible !== false;
              return sub.isVisible !== false && !sub.isAdminOnly;
          });

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

          return (
              <div className="space-y-6">
                  {/* 返回首页按钮 - 使用路由跳转 */}
                  <div className="flex items-center gap-3">
                      <button
                          onClick={() => navigate('/')}
                          className="flex items-center gap-1 px-2.5 py-1.5 text-sm bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
                      >
                          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                              <path d="M19 12H5M12 19l-7-7 7-7"/>
                          </svg>
                          返回首页
                      </button>
                      <div className="text-sm text-slate-500">
                          当前分类:{catLinks.length} 个链接 · {subCategories.length} 个子分类
                      </div>
                  </div>

                  {/* 分类标题 */}
                  <div className="flex items-center gap-1">
                      <div className="w-8 h-8 flex items-center justify-center">
                          {cat.icon && cat.icon.length <= 4 && !/^[a-zA-Z]+$/.test(cat.icon) 
                              ? <span className="text-xl">{cat.icon}</span> 
                              : <Icon name={cat.icon} size={24} />
                          }
                      </div>
                      <div>
                          <h1 className="text-2xl font-bold text-slate-800 dark:text-slate-100">
                              {cat.name}
                          </h1>
                      </div>
                      {cat.isAdminOnly && authToken && (
                          <span className="ml-auto px-2 py-1 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>
                      )}
                  </div>

                  {/* 密码验证 */}
                  {isLocked ? (
                      <div className="bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700 p-12 flex flex-col items-center justify-center text-center">
                          <div className="w-16 h-16 bg-amber-100 dark:bg-amber-900/30 rounded-full flex items-center justify-center mb-4 text-amber-600 dark:text-amber-400">
                              <Lock size={32} />
                          </div>
                          <h3 className="text-xl text-slate-800 dark:text-slate-200 font-medium mb-2">私密目录</h3>
                          <p className="text-slate-500 dark:text-slate-400 mb-6">该分类已加密,需要验证密码才能查看内容</p>
                          <button 
                              onClick={() => setCatAuthModalData(cat)}
                              className="px-6 py-2 bg-amber-500 hover:bg-amber-600 text-white rounded-lg text-sm font-medium transition-colors"
                          >
                              输入密码解锁
                          </button>
                      </div>
                  ) : (
                      <div className="space-y-8">
                          {/* 顶级分类自己的链接 */}
                          {catLinks.length > 0 && (
                              <div>
                                  <div className={`grid gap-3 ${siteSettings.cardStyle === 'simple' ? 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6' : 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6'}`}>
                                      {catLinks.map(link => renderLinkCard(link))}
                                  </div>
                              </div>
                          )}

                          {/* 子分类 */}
                          {subCategories.map(sub => {
                              const subLinks = searchResults.filter(l => l.categoryId === sub.id);
                              const isSubLocked = sub.password && !unlockedCategoryIds.has(sub.id);

                              return (
                                  <div key={sub.id}>
                                      <div className="flex items-center gap-2 mb-4">
                                          <div className="w-8 h-8 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center">
                                              {sub.icon && sub.icon.length <= 4 && !/^[a-zA-Z]+$/.test(sub.icon) 
                                                  ? <span className="text-lg">{sub.icon}</span> 
                                                  : <Icon name={sub.icon} size={18} />
                                              }
                                          </div>
                                          <h2 className="text-lg font-semibold text-slate-700 dark:text-slate-300">
                                              {sub.name}
                                          </h2>
                                          {sub.isAdminOnly && authToken && (
                                              <span className="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>
                                          )}
                                          {isSubLocked && <Lock size={14} className="text-amber-500" />}
                                      </div>

                                      {isSubLocked ? (
                                          <div className="bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700 p-8 flex flex-col items-center justify-center text-center">
                                              <div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/30 rounded-full flex items-center justify-center mb-3 text-amber-600 dark:text-amber-400">
                                                  <Lock size={24} />
                                              </div>
                                              <h4 className="text-slate-800 dark:text-slate-200 font-medium mb-1">私密目录</h4>
                                              <p className="text-sm text-slate-500 dark:text-slate-400 mb-3">该子分类已加密,需要验证密码才能查看内容</p>
                                              <button 
                                                  onClick={() => setCatAuthModalData(sub)}
                                                  className="px-4 py-2 bg-amber-500 hover:bg-amber-600 text-white rounded-lg text-sm font-medium transition-colors"
                                              >
                                                  输入密码解锁
                                              </button>
                                          </div>
                                      ) : (
                                          <>
                                              {subLinks.length === 0 ? (
                                                  <div className="text-center py-6 text-slate-400 text-sm italic bg-slate-50 dark:bg-slate-800/50 rounded-lg">
                                                      暂无链接
                                                  </div>
                                              ) : (
                                                  <div className={`grid gap-3 ${siteSettings.cardStyle === 'simple' ? 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6' : 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6'}`}>
                                                      {subLinks.map(link => renderLinkCard(link))}
                                                  </div>
                                              )}
                                          </>
                                      )}
                                  </div>
                              );
                          })}

                          {/* 如果没有内容 */}
                          {catLinks.length === 0 && subCategories.length === 0 && (
                              <div className="text-center py-12 text-slate-400 text-sm italic">
                                  暂无内容
                              </div>
                          )}
                      </div>
                  )}
              </div>
          );
      })()
  ) : (
      /* 首页内容 */
      <>
          {/* 置顶链接部分 */}
          {pinnedLinks.length > 0 && !searchQuery && (
              <section>
                  <div className="flex items-center gap-2 mb-4">
                      <Pin size={16} className="text-blue-500 fill-blue-500" />
                      <h2 className="text-sm font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">
                          置顶 / 常用
                      </h2>
                  </div>
                  <div className={`grid gap-3 ${siteSettings.cardStyle === 'simple' ? 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6' : 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6'}`}>
                      {pinnedLinks.map(link => renderLinkCard(link))}
                  </div>
              </section>
          )}

          {/* 分类列表 */}
          {getTopLevelCategories
            .filter(cat => {
              if (authToken) return cat.isVisible !== false;
              return cat.isVisible !== false && !cat.isAdminOnly;
            })
            .map(cat => {
              const subCategories = getSubCategories(cat.id).filter(sub => {
                if (authToken) return sub.isVisible !== false;
                return sub.isVisible !== false && !sub.isAdminOnly;
              });

              const catLinks = searchResults.filter(l => l.categoryId === cat.id);
              const isLocked = cat.password && !unlockedCategoryIds.has(cat.id);
              const hasChildren = subCategories.length > 0;

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

              return (
                <section key={cat.id} id={`cat-${cat.id}`} className="scroll-mt-24">
                  <div className="flex items-center justify-between mb-4 pb-2 border-b border-slate-100 dark:border-slate-800">
                    <div className="flex items-center gap-2">
                      <div className="text-slate-400">
                        {cat.icon && cat.icon.length <= 4 && !/^[a-zA-Z]+$/.test(cat.icon) ? <span className="text-lg">{cat.icon}</span> : <Icon name={cat.icon} size={20} />}
                      </div>
                      <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>
                    </div>

                    {!isLocked && (
                      <button
                        onClick={() => handleMoreClick(cat.id)}
                        className="flex items-center text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors"
                      >
                        <span>更多</span>
                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                          <path d="m9 18 6-6-6-6"></path>
                        </svg>
                      </button>
                    )}
                  </div>

                  {isLocked ? (
                    <div className="bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700 p-8 flex flex-col items-center justify-center text-center">
                      <div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/30 rounded-full flex items-center justify-center mb-4 text-amber-600 dark:text-amber-400">
                        <Lock size={24} />
                      </div>
                      <h3 className="text-slate-800 dark:text-slate-200 font-medium mb-1">私密目录</h3>
                      <p className="text-sm text-slate-500 dark:text-slate-400 mb-4">该分类已加密,需要验证密码才能查看内容</p>
                      <button 
                        onClick={() => setCatAuthModalData(cat)}
                        className="px-4 py-2 bg-amber-500 hover:bg-amber-600 text-white rounded-lg text-sm font-medium transition-colors"
                      >
                        输入密码解锁
                      </button>
                    </div>
                  ) : (
                    <>
                      {/* 修改1:顶级分类自己的链接为0时显示暂无链接提示 */}
                      {catLinks.length === 0 ? (
                        <div className="text-center py-4 text-slate-400 text-sm italic mb-6">
                          暂无链接
                        </div>
                      ) : (
                        <div className={`grid gap-3 mb-6 ${siteSettings.cardStyle === 'simple' ? 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6' : 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6'}`}>
                          {catLinks.slice(0, 4).map(link => renderLinkCard(link))}
                          {catLinks.length > 4 && (
                            <div className="flex items-center justify-center p-4 text-sm text-slate-400 italic col-span-full">
                              还有 {catLinks.length - 4} 个链接,点击「更多」查看全部
                            </div>
                          )}
                        </div>
                      )}

                      {subCategories.slice(0, 2).map(sub => {
                        const subLinks = searchResults.filter(l => l.categoryId === sub.id);
                        const isSubLocked = sub.password && !unlockedCategoryIds.has(sub.id);

                        return (
                          <div key={sub.id} id={`cat-${sub.id}`} className="mb-6">
                            <div className="flex items-center gap-2 mb-3">
                              <div className="text-slate-400">
                                {sub.icon && sub.icon.length <= 4 && !/^[a-zA-Z]+$/.test(sub.icon) ? <span className="text-lg">{sub.icon}</span> : <Icon name={sub.icon} size={18} />}
                              </div>
                              <h3 className="text-md font-semibold text-slate-700 dark:text-slate-300">
                                {sub.name}
                              </h3>
                              {sub.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>
                              )}
                              {isSubLocked && <Lock size={14} className="text-amber-500" />}
                            </div>

                            {isSubLocked ? (
                              <div className="bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700 p-8 flex flex-col items-center justify-center text-center">
                                <div className="w-10 h-10 bg-amber-100 dark:bg-amber-900/30 rounded-full flex items-center justify-center mb-3 text-amber-600 dark:text-amber-400">
                                  <Lock size={20} />
                                </div>
                                <h4 className="text-slate-800 dark:text-slate-200 font-medium mb-1">私密目录</h4>
                                <p className="text-sm text-slate-500 dark:text-slate-400 mb-3">该子分类已加密,需要验证密码才能查看内容</p>
                                <button 
                                  onClick={() => setCatAuthModalData(sub)}
                                  className="px-3 py-1.5 bg-amber-500 hover:bg-amber-600 text-white rounded-lg text-xs font-medium transition-colors"
                                >
                                  输入密码解锁
                                </button>
                              </div>
                            ) : (
                              <>
                                {subLinks.length === 0 ? (
                                  <div className="text-center py-4 text-slate-400 text-sm italic">
                                    暂无链接
                                  </div>
                                ) : (
                                  <div className={`grid gap-3 ${siteSettings.cardStyle === 'simple' ? 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6' : 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6'}`}>
                                    {subLinks.slice(0, 4).map(link => renderLinkCard(link))}
                                    {subLinks.length > 4 && (
                                      <div className="flex items-center justify-center p-4 text-sm text-slate-400 italic col-span-full">
                                        还有 {subLinks.length - 4} 个链接
                                      </div>
                                    )}
                                  </div>
                                )}
                              </>
                            )}
                          </div>
                        );
                      })}

                      {/* 修改2:让「还有 X 个子分类」的文字可点击进入详情页 */}
                      {subCategories.length > 2 && (
                        <div className="text-center py-2 text-sm text-slate-400 italic border-t border-slate-100 dark:border-slate-800 pt-4">
                          还有 {subCategories.length - 2} 个子分类,点击
                          <button
                            onClick={() => handleMoreClick(cat.id)}
                            className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 mx-1 underline-offset-2 hover:underline transition-colors"
                          >
                            「更多」
                          </button>
                          查看全部
                        </div>
                      )}

                      {catLinks.length === 0 && subCategories.length === 0 && (
                        <div className="text-center py-8 text-slate-400 text-sm italic">
                          暂无链接
                        </div>
                      )}
                    </>
                  )}
                </section>
              );
          })}

          {/* 搜索空状态 */}
          {searchQuery && searchMode === 'local' && searchResults.length === 0 && (
              <div className="flex flex-col items-center justify-center py-20 text-slate-400">
                  <Search size={40} className="opacity-30 mb-4" />
                  <p>没有找到相关内容</p>
                  <button onClick={() => setIsModalOpen(true)} className="mt-4 text-blue-500 hover:underline">添加一个?</button>
              </div>
          )}

          {/* 底部留白 */}
          <div className="h-20"></div>
      </>
  )}
</div>

四、修改 components/LinkModal.tsx 文件

1. 修改分类下拉框显示的层级结构

找到

<div className="flex-1">
    <label className="block text-sm font-medium mb-1 dark:text-slate-300">分类</label>
    <select
    value={categoryId}
    onChange={(e) => setCategoryId(e.target.value)}
    className="w-full p-2 rounded-lg 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"
    >
    {categories.map(cat => (
        <option key={cat.id} value={cat.id}>{cat.name}</option>
    ))}
    </select>
</div>

整段替换成

<div className="flex-1">
    <label className="block text-sm font-medium mb-1 dark:text-slate-300">分类</label>
    <select
        value={categoryId}
        onChange={(e) => setCategoryId(e.target.value)}
        className="w-full p-2 rounded-lg 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"
        style={{ maxHeight: '200px', overflowY: 'auto' }}
    >
        <option value="" disabled>请选择目标分类</option>
        {categories
            .filter(cat => !cat.parentId) // 先取顶级分类
            .map(topCat => {
                // 获取这个顶级分类下的所有子分类
                const children = categories.filter(c => c.parentId === topCat.id);

                return (
                    <React.Fragment key={topCat.id}>
                        {/* 顶级分类 */}
                        <option value={topCat.id}>{topCat.name}</option>
                        {/* 子分类 - 缩进显示 */}
                        {children.map(child => (
                            <option key={child.id} value={child.id} style={{ paddingLeft: '24px' }}>
                                └ {child.name}
                            </option>
                        ))}
                    </React.Fragment>
                );
            })}
    </select>
</div>

2. 修改置顶按钮显示效果

找到

<div className="flex items-end pb-1">
  <button
      type="button"
      onClick={() => setPinned(!pinned)}
      className={`flex items-center gap-2 px-4 py-2.5 rounded-lg border transition-all h-[42px] ${

替换成

<div className="flex items-end">
  <button
      type="button"
      onClick={() => setPinned(!pinned)}
      className={`flex items-center gap-2 px-4 py-2.5 rounded border transition-all h-[39px] ${

五、修改 index.html 文件

直接替换

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="https://lucide.dev/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title></title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
      tailwind.config = {
        darkMode: 'class',
        theme: {
          extend: {
            colors: {
              primary: '#3b82f6',
              secondary: '#64748b',
              dark: '#0f172a',
              card: '#1e293b',
            }
          }
        }
      }
    </script>
    <style>
      body {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      }
      /* Custom scrollbar for sidebar */
      .scrollbar-hide::-webkit-scrollbar {
          display: none;
      }
      .scrollbar-hide {
          -ms-overflow-style: none;
          scrollbar-width: none;
      }
      /* Simple CSS-only tooltip transition */
      .tooltip-custom {
          transition: opacity 0.2s, visibility 0.2s;
          pointer-events: none;
      }
      /* 禁用所有元素的长按菜单 */
      * {
        -webkit-touch-callout: none !important;
        -webkit-user-select: none !important;
        user-select: none !important;
      }
      /* 允许输入框可以选中文本 */
      input, textarea {
        -webkit-user-select: auto !important;
        user-select: auto !important;
      }
      /* 禁用图片保存菜单 */
      img {
        -webkit-touch-callout: none !important;
        pointer-events: none;
      }
      a img {
        pointer-events: none;
      }
    </style>
  <script type="importmap">
{
  "imports": {
    "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
    "lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
    "react/": "https://aistudiocdn.com/react@^19.2.0/",
    "react": "https://aistudiocdn.com/react@^19.2.0",
    "uuid": "https://aistudiocdn.com/uuid@^13.0.0",
    "@google/genai": "https://aistudiocdn.com/@google/genai@^1.30.0",
    "buffer": "https://aistudiocdn.com/buffer@^6.0.3",
    "jszip": "https://aistudiocdn.com/jszip@^3.10.1"
  }
}
</script>
</head>
  <body class="bg-gray-50 dark:bg-slate-900 text-slate-900 dark:text-slate-50 transition-colors duration-300">
    <div id="root"></div>
  <script type="module" src="/index.tsx"></script>
</body>
</html>

六、修改 package.json 文件

找到

"buffer": "^6.0.3",

替换成

"buffer": "^6.0.3",
"react-router-dom": "^6.22.0",

七、修改 index.tsx 文件

整个替换成

import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import App from './App';

// 定义路由配置
const router = createBrowserRouter([
  {
    path: '/',
    element: <App />,
  },
  {
    path: '/cat/:categoryId',
    element: <App />,
  },
]);

const rootElement = document.getElementById('root');
if (!rootElement) {
  throw new Error("Could not find root element to mount to");
}

const root = ReactDOM.createRoot(rootElement);
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);