Typecho 实现一个目录树

前言

最近发现主题的目录树很多地方不是很满意,一方面是自己对于php的知识不是很好,大部分都是从网上分析的代码中拿取cv一下就用了,加上重构主题是一个比较大的工程,所以有些地方都是奔着先跑起来再说的思路。

像极了项目开发时的场景,先上线再说,用户量大了我们再优化!

昨晚我看了下typecho的目录树,都使用了一个全局变量:

global $catalog;

$catalog是一个扁平化的数组,里面存放着按顺序提取的页面h1-h6标题数据,由于php的数组可视化巨难看,我们先通过ts的方式去了解和实现这个处理。

教程

$catalog的数据结构大致如下:

const catalog = [
  {
    level: 1,
    name: "一级目录",
    index: 1,
  },
  {
    level: 2,
    name: "二级目录",
    index: 2,
  },
  {
    level: 3,
    name: "三级目录",
    index: 3,
  },
  {
    level: 4,
    name: "四级目录",
    index: 4,
  },
  {
    level: 5,
    name: "五级目录",
    index: 5,
  },
  {
    level: 6,
    name: "六级目录",
    index: 6,
  },
  {
    level: 2,
    name: "二级目录",
    index: 7,
  },
  {
    level: 3,
    name: "三级目录",
    index: 8,
  },
  {
    level: 3,
    name: "三级目录",
    index: 9,
  },
  {
    level: 2,
    name: "二级目录",
    index: 10,
  },
  {
    level: 2,
    name: "二级目录",
    index: 11,
  },
  {
    level: 3,
    name: "三级目录",
    index: 12,
  },
  {
    level: 3,
    name: "三级目录",
    index: 13,
  },
  {
    level: 1,
    name: "一级目录",
    index: 14,
  },
];

数组中的子对象有三个属性,当然这三个属性名我没有完全按照typecho的设置来,我们先用更好理解的字段来实现这个功能先。

  • level 表示是h1-h6的数字,h1就是1,以此类推
  • name 表示标题元素的文本内容
  • index 表示标题元素的顺序

index这里还用不到,但是在php处理hash跳转的时候我们会用到,所以这里先放着。

现在我们需要实现一个函数,它可以将下一个level大于上一个level时,将下一个作为上一个的children子集数据存起来。以此类推,直到存在同级level或者小于level结束嵌套。

想了很久,我得出一个非常棒的处理方案: 从末尾往前处理,一层一层处理嵌套

用代码表示是这样:

//源数据
[1, 2, 3, 4, 5, 6, 2, 3, 3, 2, 2, 3, 3, 1]

//处理第一层
[1, 2, 3, 4, 5, 2, 3, 3, 2, 2, 3, 3, 1]

//处理第二层
[1, 2, 3, 4, 2, 3, 3, 2, 2, 3, 3, 1]

//处理第三层
[1, 2, 3, 2, 3, 3, 2, 2, 3, 3, 1]

//处理第四层
[1, 2, 2, 2, 2, 1]

//处理第五层
[1, 1]

//处理第六层:已经到顶了,原样返回
[1, 1]  

非常好理解,我们先从后往前处理,循环到6的时候,我们判断到它的level是6,然后我们获取它的上一级,它的上一级就是它当前的index-1,当然也不一定是-1,所以我们需要做一个while循环去往前拿,直到拿到的level值是小于6的,然后我们把6作为parent的children值传入,注意这里我们得通过unshift方法,因为我们是倒序循环,存的时候为了保证顺序就得反着存。

存完我把这个数据splice删除;

当我们倒序循环完毕后,递归自身,处理下一层。

于是我们可以这么写:

type TreeData = Array<{
  level: number;
  name: string;
  index: number;
  children?: TreeData;
}>;

const catalog: TreeData = [...];

function generateTree(list: TreeData, level = 6): TreeData {
  if (level <= 1) return list;

  for (let i = list.length - 1; i >= 0; i--) {
    const item = list[i];
    if (item.level === level) {
      let parentIndex = i - 1;
      let parent = list[parentIndex];
      while (parent?.level >= level) {
        parent = list[--parentIndex];
      }
      if (!parent) break;
      if (!Array.isArray(parent.children)) parent.children = [];
      parent.children.unshift(item);
      list.splice(i, 1);
    }
  }

  return generateTree(list, level - 1);
}

const tree = generateTree(catalog);
console.log("🚀 ~ file: main.ts:104 ~ tree:", tree);

需要注意的是list[parentIndex]可能是没有的,所以在下面我用了可选链避免undefined.level导致的报错。

然后就是break,跳出本次for循环,之前沙雕了,直接return,return会导致整个for都不走了。

此时我们查看打印可以得到一个完美的嵌套结构:

微信截图_20230710202905.png

如果你的目的就是这样,那么看到这里就足够了。

但是,我们的需求还要更近一步,我希望能过滤掉指定的层级,就以掘金来举例,它最大只支持三级层级嵌套,我们也需要实现这个效果。

所以我们实现一个删除指定层级深度的函数:

function removeChildren(list: TreeData, depth: number, currentDepth = 0): TreeData {
  list.forEach((item) => {
    if (item.children && item.children.length > 0) {
      if (currentDepth < depth - 1) {
        removeChildren(item.children, depth, currentDepth + 1);
      } else {
        delete item.children;
      }
    }
  });
  return list;
}

效果:

const tree = generateTree(catalog);
const maxTree = removeChildren(tree, 3);
console.log("🚀 ~ file: main.ts:117 ~ maxTree:", maxTree);

微信截图_20230710202510.png

至此我们的核心逻辑已经实现,下面我们就需要将其搬运到php上使用,转译一下。

/**
 * @description: 将扁平化目录树数组转成结构化目录树数组
 * @param {*} $list 目录树数组
 * @param {*} $depth  最大层级
 * @Date: 2023-06-03 15:42:21
 * @Author: mulingyuer
 */
function generateTreeList($list, $depth = 6) {
    if (count($list) <= 0 || $depth <= 1) {
        return $list;
    }

    for ($i = count($list) - 1; $i >= 0; $i--) {
        $item = $list[$i];
        if ($item['depth'] == $depth) {
            $parentIndex = $i - 1;
            while ($parentIndex >= 0) {
                $parent = &$list[$parentIndex];
                if ($parent['depth'] < $depth) {
                    break;
                }
                $parentIndex--;
            }

            if ($parentIndex < 0) {
                break;
            }

            if ( ! is_array($parent['children'])) {
                $parent['children'] = array();
            }

            array_unshift($parent['children'], $item);
            array_splice($list, $i, 1);
        }
    }

    $list = array_values($list);
    return generateTreeList($list, $depth - 1);
}复制代码

这个方法只是生成层级结构数据,我们还需要限制层级数,于是转义第二个函数:

/**
 * @description: 删除目录树数组指定层级children
 * @param {*} $list 目录树数组
 * @param {*} $depth  最大层级
 * @param {*} $currentDepth 当前层级
 * @Date: 2023-06-03 15:49:03
 * @Author: mulingyuer
 */
function removeChildren($list, $depth, $currentDepth = 0) {
    foreach ($list as &$item) {
        if (isset($item['children']) && count($item['children']) > 0) {
            if ($currentDepth < $depth - 1) {
                $item['children'] = removeChildren($item['children'], $depth, $currentDepth + 1);
            } else {
                unset($item['children']);
            }
        }
    }
    return $list;
}复制代码

使用这两个函数配合,我们可以生成指定层级数量的目录树结构数据,下面我们就要开始生成HTML,经过我个人测试发现,我们可以在生成HTML结构的时候同时限制层级数量,这样就可以节省一个函数使用,代码如下:

/**
 * @description: 生成目录树html
 * @param {*} $arr 目录树数组
 * @param {*} $depth  最大层级
 * @param {*} $currentDepth 当前层级
 * @param {*} $isChildren 是否是子级
 * @Date: 2023-06-03 16:48:54
 * @Author: mulingyuer
 */
function generateTreeTemplate($arr, $depth, $currentDepth = 1, $isChildren = false) {
    if (count($arr) <= 0) {
      return '<div>暂无目录</div>';
    }
    if ($currentDepth > $depth) {
        return '';
    }
    $output =  ! $isChildren ? '<ul>' : '';
    foreach ($arr as $item) {
        $output .= '<li><a href="#heading-'.$item['count'].'" title="'.$item['text'].'">'.$item['text'].'</a>';
        if ( ! empty($item['children']) && $currentDepth < $depth) {
            $output .= '<ul>';
            $output .= generateTreeTemplate($item['children'], $depth, $currentDepth + 1, true);
            $output .= '</ul>';
        }
        $output .= '</li>';
    }
    $output .=  ! $isChildren ? '</ul>' : '';
    return $output;
}复制代码

通过制定第二个参数,我们可以生成指定层级的HTML。

使用:

/**
 * @description: 获取目录树
 * @param {*} $maxDirectory 最大层级
 * @Date: 2023-06-03 22:30:49
 * @Author: mulingyuer
 */
function getJJDirectoryTree($maxDirectory = 3) {
    global $catalog;
    $treeList = generateTreeList(array_replace_recursive(array(), $catalog));
    echo generateTreeTemplate($treeList, $maxDirectory);
}复制代码

在typecho的主题中,我们可以调用该函数就能获取到一个目录树结构。

 <?php getJJDirectoryTree();?>复制代码

效果图

微信截图_20230710202337.png

这是一个我自己实现的效果,还是很不错的,有兴趣的可以自己研究更多玩法。

评论区
头像