分类挂件下拉面板列出全部层级并缩进 #24
需求(2026-10-27):把多级目录也放进去,现在只显示第一级,把其他的 也放进去,注意缩进。线上实测 29 个分类(8 一级 + 21 二级,无第三层), 因此面板从「只列一级」改成「有几层列几层」。 inc/theme-widgets.php - get_tree() 从写死两层改成任意深度递归。带 $seen 已访问集合 + $max_depth=10 双重保护:parent 成环时只跳过、不抛异常 —— 侧栏 挂件白屏会把整站带下去,宁可少显示一条也不能崩。 - 排序递归到所有层(原来只排前两层,第三层往后顺序与「按文章数排」不一致)。 - 新增 find_name():触发条文案与面板渲染共用同一套遍历。原来两段各自 硬编码两层,层数一变必漏一处。 - 新增 render_level():widget() 面板改成单次递归调用。关闭 children 时把 children 清空交给同一函数,不另写一份循环 —— 两份循环必然漂移。 - 下一层 <ul> 放在本层 li 内部(HTML 里 ul 的直接子元素只能是 li)。 - children 兜底值 false → true,**两处**(widget 第 1501 行、form 第 1701 行)。线上实例 categories-3 没有这个键 ⇒ 走兜底 ⇒ 用户零操作生效。 assets/css/kratos.css - .yang-cats-item 加 flex-wrap:wrap;子级 ul 用 flex:0 0 100% 独占 一整行,落在父级 li 的内容盒里 ⇒ 缩进逐层累加且所有层共用一个右边界。 - 缩进只由子级 li 的 padding-left 给,**子级 ul 的 padding-left 归零**。 第一版两处都给了 12px,几何实测每层缩进 24px(stepPx=[24,24,18]), 第 3 层名字只剩 148px,收敛规则也一起失效。 - 第 4 层起收缩到 6px([style*="--cat-d:3"] 起),避免深层吃光面板宽度。 - 每层 padding-right 一律 0,右侧 12px 只由顶层统一给 ⇒ 任意深度的 数字右沿都在同一竖线(数字要竖着比大小,错开一列就没法比)。 assets/css/kratos.min.css 重新压缩;style.css 版本号 → 3.2.56。 验证 - 端到端(真实 WordPress):120 通过 / 0 失败;旧代码 100 / 17 - 反证 20 组(R1~R19 + R13b)全部命中 - 视觉几何(线上真实页面):56 通过 / 0 失败 - 打包内容自检 26 项全过;发布判据 79 条全过 - 发布:update.json → 3.2.56,md5 0aa90b30e76d754de41febcb9271ddef - 红线:服务器主题目录仍 3.2.55(未覆盖),等用户在后台点升级
This commit is contained in:
+212
-99
@@ -1284,8 +1284,8 @@ class widget_categories extends WP_Widget
|
||||
}
|
||||
|
||||
/* get_categories 返回的是平铺数组(按 orderby 排好),
|
||||
* 这里按 parent 归拢成树。不用 Walker,因为要输出 <details>,
|
||||
* 而 Walker_Category 只认 <ul>/<li>。 */
|
||||
* 这里按 parent 归拢成树。不用 Walker,因为要输出 <details> 面板,
|
||||
* 而 Walker_Category 只认 <ul>/<li> 的固定形态。 */
|
||||
$by_parent = array();
|
||||
foreach ($cats as $c) {
|
||||
$pid = (int) $c->parent;
|
||||
@@ -1295,25 +1295,55 @@ class widget_categories extends WP_Widget
|
||||
$by_parent[$pid][] = $c;
|
||||
}
|
||||
|
||||
// 只保留「根(parent=0)→ 直接子级」两层。实测本站最深就是 2 层;
|
||||
// 若将来有第三层,递归会把 <details> 套娃,侧栏观感反而更差。
|
||||
$tree = array();
|
||||
if (!isset($by_parent[0])) {
|
||||
return $tree;
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ($by_parent[0] as $root) {
|
||||
$rid = (int) $root->term_id;
|
||||
$children = isset($by_parent[$rid]) ? $by_parent[$rid] : array();
|
||||
$tree[] = array(
|
||||
'cat' => $root,
|
||||
'children' => $children,
|
||||
);
|
||||
}
|
||||
/* 🔴 3.2.56 起改成**任意深度**递归(3.2.55 是写死两层)。
|
||||
*
|
||||
* 用户的诉求是「把多级目录也放进去,注意缩进」。本站实测最深
|
||||
* 就 2 层(8 个一级 + 21 个二级,没有第三级),但"写死两层"
|
||||
* 的代码在将来有人加第三级时会**静默丢掉**那一层 —— 页面上
|
||||
* 看不出错,只是分类凭空少了。这种"少显示了但没人报错"的
|
||||
* 缺陷最难发现,所以这里一次做成递归。
|
||||
*
|
||||
* 🔴 递归必须带**已访问集合 + 深度保护**:
|
||||
* · 数据库里 parent 指针理论上可以成环(手工改库、导入坏数据
|
||||
* 都能造出来)。没有已访问集合的话,成环 ⇒ 无限递归 ⇒
|
||||
* PHP 直接栈溢出白屏。侧栏挂件白屏会把整站都带下去。
|
||||
* · 深度上限兜住其他意外(正常最多几级)。
|
||||
* 两重保护都只用"跳过"处理,**不抛异常** —— 分类导航是
|
||||
* 基础功能,宁可少显示一条也不能整站白屏。 */
|
||||
$max_depth = 10;
|
||||
|
||||
/* 按文章数排序:在 PHP 里按「补过的 count」排,两边都排。
|
||||
$build = function ($parent_id, $depth, $seen) use (&$build, $by_parent, $max_depth) {
|
||||
if ($depth > $max_depth || !isset($by_parent[$parent_id])) {
|
||||
return array();
|
||||
}
|
||||
$out = array();
|
||||
foreach ($by_parent[$parent_id] as $c) {
|
||||
$cid = (int) $c->term_id;
|
||||
// 成环或重复:跳过,不进入下一层
|
||||
if (isset($seen[$cid])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$cid] = true;
|
||||
$out[] = array(
|
||||
'cat' => $c,
|
||||
'children' => $build($cid, $depth + 1, $seen),
|
||||
);
|
||||
}
|
||||
return $out;
|
||||
};
|
||||
|
||||
$tree = $build(0, 0, array());
|
||||
|
||||
/* 按文章数排序:在 PHP 里按「补过的 count」排,**每一层都排**。
|
||||
* 用 usort + 显式 tie-break(count 相同时按名称),
|
||||
* 否则相等项的先后顺序取决于 usort 的实现,每次请求可能不同。 */
|
||||
* 否则相等项的先后顺序取决于 usort 的实现,每次请求可能不同。
|
||||
*
|
||||
* 🔴 这里必须递归到所有层 —— 3.2.55 只排了前两层,第三层往后
|
||||
* 永远保持 get_categories 给的顺序,与「按文章数排」不一致。 */
|
||||
if ($orderby === 'count') {
|
||||
$desc = ($order === 'DESC');
|
||||
|
||||
@@ -1327,33 +1357,148 @@ class widget_categories extends WP_Widget
|
||||
return strnatcasecmp($a->name, $b->name);
|
||||
};
|
||||
|
||||
usort($tree, function ($x, $y) use ($cmp) {
|
||||
return $cmp($x['cat'], $y['cat']);
|
||||
});
|
||||
|
||||
foreach ($tree as $i => $node) {
|
||||
if (count($node['children']) > 1) {
|
||||
usort($tree[$i]['children'], $cmp);
|
||||
$sort_level = function (&$nodes) use (&$sort_level, $cmp) {
|
||||
usort($nodes, function ($x, $y) use ($cmp) {
|
||||
return $cmp($x['cat'], $y['cat']);
|
||||
});
|
||||
foreach ($nodes as $i => $n) {
|
||||
if (!empty($n['children'])) {
|
||||
$sort_level($nodes[$i]['children']);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$sort_level($tree);
|
||||
}
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在树里按 term_id 找分类名(任意深度)。
|
||||
*
|
||||
* 🔴 单独抽出来,是为了让「触发条文案」和「面板渲染」用**同一套**
|
||||
* 遍历逻辑。3.2.55 那版是两段各自硬编码两层的循环 —— 一旦树的
|
||||
* 层数变了,两处必须同时改,漏一处就会「面板里有、触发条上找不到」。
|
||||
*
|
||||
* 找不到返回空串(调用方据此显示占位文案「选择分类」)。
|
||||
*
|
||||
* @param array $tree get_tree() 的返回值
|
||||
* @param int $cat_id
|
||||
* @return string
|
||||
*/
|
||||
private function find_name(array $tree, $cat_id)
|
||||
{
|
||||
foreach ($tree as $node) {
|
||||
if ((int) $node['cat']->term_id === (int) $cat_id) {
|
||||
return (string) $node['cat']->name;
|
||||
}
|
||||
if (!empty($node['children'])) {
|
||||
$got = $this->find_name($node['children'], $cat_id);
|
||||
if ($got !== '') {
|
||||
return $got;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归渲染一层分类(任意深度)。
|
||||
*
|
||||
* 每层的 DOM 完全一样:
|
||||
*
|
||||
* <li class="yang-cats-item" style="--cat-d: N">
|
||||
* <a class="yang-cats-link">名字</a>
|
||||
* <span class="yang-cats-count">N</span>
|
||||
* <ul class="yang-cats-children"> …下一层… </ul>
|
||||
* </li>
|
||||
*
|
||||
* 🔴 下一层的 `<ul>` 必须放在**本层 li 里面**,不能放在 li 外面当兄弟:
|
||||
* HTML 里 `<ul>` 的直接子元素只能是 `<li>`,写成
|
||||
* `<ul><li/><ul/><li/></ul>` 是非法结构,浏览器容错行为不可依赖。
|
||||
*
|
||||
* 🔴 缩进靠**逐层累加的左内边距**:
|
||||
* li 是 `display:flex; flex-wrap:wrap`
|
||||
* 下一层 ul 用 `flex: 0 0 100%` 独占一行(不进 flex 主轴)
|
||||
* ⇒ 嵌套 ul 落在父级 li 的**内容盒**内,天然右边界一致
|
||||
* ⇒ 所以每层 li 的 padding-left 相加就是缩进,且**数字右沿
|
||||
* 始终对齐到面板右侧同一列**(数字要竖着比大小,不能错开)
|
||||
*
|
||||
* 3.2.55 只做两层,靠 `<ul padding-left>` 做缩进,导致子级数字比
|
||||
* 父级多缩进 12px(那次修的是把子级 li 的 padding-right 归零)。
|
||||
* 改成"逐层 li padding-left、padding-right 只在顶层给"之后,
|
||||
* 这个对齐关系对**任意层数**都成立。
|
||||
*
|
||||
* @param array $nodes 本层节点
|
||||
* @param int $current_id 当前分类页的 term_id(0 = 不在分类页)
|
||||
* @param array $ancestors 当前分类的祖先 id 链
|
||||
* @param bool $show_count 是否显示文章数
|
||||
* @param int $depth 当前层深度(0 = 顶级)
|
||||
*/
|
||||
private function render_level(array $nodes, $current_id, array $ancestors, $show_count, $depth = 0)
|
||||
{
|
||||
if (count($nodes) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo $depth === 0
|
||||
? '<ul class="yang-cats-list">'
|
||||
: '<ul class="yang-cats-children">';
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$cat = $node['cat'];
|
||||
$cid = (int) $cat->term_id;
|
||||
$kids = isset($node['children']) ? $node['children'] : array();
|
||||
|
||||
$cls = 'yang-cats-item';
|
||||
if ($current_id === $cid) {
|
||||
$cls .= ' current-cat';
|
||||
} elseif (in_array($cid, $ancestors, true)) {
|
||||
/* 当前在看这个父级底下的子分类 —— 父级挂 current-cat-parent,
|
||||
* 与核心 Walker_Category 的类名保持一致,便于写样式。
|
||||
* 这条要**对每一层都生效**(3.2.55 只对第一层生效)。 */
|
||||
$cls .= ' current-cat-parent';
|
||||
}
|
||||
if (count($kids) > 0) {
|
||||
$cls .= ' has-children';
|
||||
}
|
||||
|
||||
echo '<li class="' . esc_attr($cls) . '" style="--cat-d:' . $depth . '">';
|
||||
echo '<a class="yang-cats-link" href="' . esc_url(get_category_link($cid)) . '">'
|
||||
. $this->name_html($cat->name) . '</a>';
|
||||
if ($show_count) {
|
||||
echo '<span class="yang-cats-count">' . (int) $cat->count . '</span>';
|
||||
}
|
||||
|
||||
/* 下一层直接接在 li 内部(合法结构),由 CSS 的 flex-wrap 让它独占一行 */
|
||||
if (count($kids) > 0) {
|
||||
$this->render_level($kids, $current_id, $ancestors, $show_count, $depth + 1);
|
||||
}
|
||||
|
||||
echo '</li>';
|
||||
}
|
||||
|
||||
echo '</ul>';
|
||||
}
|
||||
|
||||
public function widget($args, $instance)
|
||||
{
|
||||
$show_count = $this->pick($instance, 'count', true);
|
||||
$hide_empty = $this->pick($instance, 'hide_empty', true);
|
||||
/* 🔴 children 默认值 3.2.55 改成 **false**(3.2.54 是 true)。
|
||||
/* 🔴 children 默认值 3.2.56 改回 **true**(3.2.55 是 false)。
|
||||
*
|
||||
* 用户的诉求原话是「点击后把所有**一级**分类拉出来」—— 默认就该
|
||||
* 只有一级。线上那个实例 `categories-3` 是核心老配置,压根没有
|
||||
* children 这个键 ⇒ 走 fallback ⇒ 改默认值就等于改线上呈现,
|
||||
* 不需要用户去后台勾一下,也不用动数据库。
|
||||
* 3.2.55 时用户的诉求是「点击后把所有**一级**分类拉出来」,
|
||||
* 所以默认只列一级。2026-10-27 追加要求:「把多级目录也放进去,
|
||||
* 现在只显示第一级,把其他的也放进去,注意缩进」⇒ 默认改成
|
||||
* 列出**所有层级**(递归渲染,缩进表达层级)。
|
||||
*
|
||||
* 想连子级一起看的人,在后台勾上即可(update() 会存成 1)。 */
|
||||
$show_children = $this->pick($instance, 'children', false);
|
||||
* 线上那个实例 `categories-3` 是核心老配置,压根没有 children
|
||||
* 这个键 ⇒ 走 fallback ⇒ **改兜底值就等于改线上呈现**,用户
|
||||
* 不用进后台勾一下,也不用动数据库。这是本挂件最省事的杠杆。
|
||||
*
|
||||
* 仍然保留开关:侧栏分类特别多时,可以在后台关掉只留一级。 */
|
||||
$show_children = $this->pick($instance, 'children', true);
|
||||
$orderby = $this->pick($instance, 'orderby', 'name');
|
||||
$order = $this->pick($instance, 'order', 'ASC');
|
||||
|
||||
@@ -1410,18 +1555,7 @@ class widget_categories extends WP_Widget
|
||||
* 所以不必再往祖先链上找。 */
|
||||
$current_name = '';
|
||||
if ($current_id > 0) {
|
||||
foreach ($tree as $node) {
|
||||
if ((int) $node['cat']->term_id === $current_id) {
|
||||
$current_name = (string) $node['cat']->name;
|
||||
break;
|
||||
}
|
||||
foreach ($node['children'] as $kid) {
|
||||
if ((int) $kid->term_id === $current_id) {
|
||||
$current_name = (string) $kid->name;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
$current_name = $this->find_name($tree, $current_id);
|
||||
}
|
||||
|
||||
/* 面板 DOM id:一页上可能有多个实例(虽然实际很少),必须唯一,
|
||||
@@ -1450,63 +1584,42 @@ class widget_categories extends WP_Widget
|
||||
echo '</summary>';
|
||||
|
||||
echo '<div class="yang-cats-panel" id="' . esc_attr($panel_id) . '">';
|
||||
echo '<ul class="yang-cats-list">';
|
||||
|
||||
foreach ($tree as $node) {
|
||||
$root = $node['cat'];
|
||||
$rid = (int) $root->term_id;
|
||||
$kids = $node['children'];
|
||||
$has_kids = ($show_children && count($kids) > 0);
|
||||
|
||||
$root_cls = 'yang-cats-item';
|
||||
if ($has_kids) {
|
||||
$root_cls .= ' has-children';
|
||||
/* 🔴 3.2.56:子分类从「两层平铺」改成**递归渲染所有层级**。
|
||||
*
|
||||
* 用户 2026-10-27 的要求:「把多级目录也放进去,现在只显示第一级,
|
||||
* 把其他的也放进去,注意缩进」。
|
||||
*
|
||||
* ⇒ 面板里现在一次列全:一级 → 二级 → 三级(有几层列几层),
|
||||
* 点开一次就能看到全部,不用逐层点。
|
||||
*
|
||||
* 🔴 缩进**不是**靠 `--cat-d` 变量算的(那样每层都要写一条
|
||||
* 偏移量规则,层数一变就得改 CSS)。缩进靠**嵌套 + 逐层
|
||||
* padding-left** 自然累加:`.yang-cats-children` 用
|
||||
* `flex: 0 0 100%` 独占一行、嵌在父级 li 的内容盒里,
|
||||
* 所以每层 li 的 padding-left 相加就是缩进,右边界还一致。
|
||||
* `--cat-d` 只用来做两件事:① 标记真实深度供第 4 层起
|
||||
* 缩进收敛(`[style*="--cat-d:3"]` 起);② 给测试读。
|
||||
* **它不参与算缩进**,写错了也不会导致布局错位。
|
||||
*
|
||||
* 🔴 仍然保留 `children` 开关(默认已改回 **true**):
|
||||
* 有人侧栏分类多,全列出来面板太长要滚很久,这时可以关掉
|
||||
* 只留一级。关掉时不是"少一层",而是**只列一级**。
|
||||
*
|
||||
* 🔴 关掉时的实现不是另写一份循环,而是把每个节点的 children
|
||||
* 清空后交给同一个渲染函数 —— 两份循环必然会漂移(3.2.55
|
||||
* 就是两段各自硬编码两层),一处改了另一处忘改就出怪现象。 */
|
||||
if ($show_children) {
|
||||
$this->render_level($tree, $current_id, $current_ancestors, $show_count);
|
||||
} else {
|
||||
$roots = array();
|
||||
foreach ($tree as $node) {
|
||||
$node['children'] = array();
|
||||
$roots[] = $node;
|
||||
}
|
||||
if ($current_id === $rid) {
|
||||
$root_cls .= ' current-cat';
|
||||
} elseif (in_array($rid, $current_ancestors, true)) {
|
||||
/* 当前在看这个父级底下的子分类 —— 父级挂 current-cat-parent,
|
||||
* 与核心 Walker_Category 的类名保持一致,便于写样式。 */
|
||||
$root_cls .= ' current-cat-parent';
|
||||
}
|
||||
|
||||
echo '<li class="' . esc_attr($root_cls) . '">';
|
||||
echo '<a class="yang-cats-link" href="' . esc_url(get_category_link($rid)) . '">'
|
||||
. $this->name_html($root->name) . '</a>';
|
||||
if ($show_count) {
|
||||
echo '<span class="yang-cats-count">' . (int) $root->count . '</span>';
|
||||
}
|
||||
|
||||
/* 子分类:只有用户显式勾了「显示子分类」才输出。
|
||||
*
|
||||
* 🔴 这里**不再用 <details> 嵌套**(3.2.54 是那样)。原因:
|
||||
* 面板本身就是浮层,展开状态只由触发条一个开关管;
|
||||
* 再套一层折叠,用户点开面板后仍要再点一次才能看见子级,
|
||||
* 「点击后把所有的一级分类拉出来」这个诉求就没满足。
|
||||
* 子级直接平铺在父级下面、缩进显示 —— 一目了然,少一次点击。 */
|
||||
if ($has_kids) {
|
||||
echo '<ul class="yang-cats-children">';
|
||||
foreach ($kids as $kid) {
|
||||
$kid_id = (int) $kid->term_id;
|
||||
$kid_cls = 'yang-cats-item';
|
||||
if ($current_id === $kid_id) {
|
||||
$kid_cls .= ' current-cat';
|
||||
}
|
||||
echo '<li class="' . esc_attr($kid_cls) . '">';
|
||||
echo '<a class="yang-cats-link" href="' . esc_url(get_category_link($kid_id)) . '">'
|
||||
. $this->name_html($kid->name) . '</a>';
|
||||
if ($show_count) {
|
||||
echo '<span class="yang-cats-count">' . (int) $kid->count . '</span>';
|
||||
}
|
||||
echo '</li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
|
||||
echo '</li>';
|
||||
$this->render_level($roots, $current_id, $current_ancestors, $show_count);
|
||||
}
|
||||
|
||||
echo '</ul>';
|
||||
echo '</div>';
|
||||
echo '</details>';
|
||||
echo '</div>';
|
||||
@@ -1594,7 +1707,7 @@ class widget_categories extends WP_Widget
|
||||
$show_count = $this->pick($instance, 'count', true);
|
||||
$hide_empty = $this->pick($instance, 'hide_empty', true);
|
||||
// 与 widget() 保持同一默认值(理由见 widget() 里的注释)
|
||||
$show_children = $this->pick($instance, 'children', false);
|
||||
$show_children = $this->pick($instance, 'children', true);
|
||||
$orderby = $this->pick($instance, 'orderby', 'name');
|
||||
$order = $this->pick($instance, 'order', 'ASC');
|
||||
if ($orderby !== 'count') {
|
||||
@@ -1613,8 +1726,8 @@ class widget_categories extends WP_Widget
|
||||
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" id="<?php echo esc_attr($this->get_field_id('children')); ?>" name="<?php echo esc_attr($this->get_field_name('children')); ?>" <?php checked($show_children); ?>>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('children')); ?>"><?php _e('展开时同时列出子分类', 'kratos'); ?></label>
|
||||
<span class="description"><?php _e('默认只列一级分类。勾上后,有子分类的父级下面会缩进列出它的子分类。', 'kratos'); ?></span>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('children')); ?>"><?php _e('列出子分类(含更多层级)', 'kratos'); ?></label>
|
||||
<span class="description"><?php _e('默认列出全部层级,下级缩进显示。分类特别多、面板太长时,可以关掉只留一级分类。', 'kratos'); ?></span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
Reference in New Issue
Block a user