From 4c1d67b86bb4a686b85542e99bcfde75f20711f8 Mon Sep 17 00:00:00 2001 From: laoyang Date: Tue, 27 Oct 2026 12:18:53 +0800 Subject: [PATCH] =?UTF-8?q?3.2.54=20=E4=B8=BB=E9=A2=98=E6=8E=A5=E7=AE=A1?= =?UTF-8?q?=E3=80=8C=E6=96=87=E7=AB=A0=E5=88=86=E7=B1=BB=E3=80=8D=E6=8C=82?= =?UTF-8?q?=E4=BB=B6=EF=BC=9A=E7=88=B6=E5=AD=90=E6=8A=98=E5=8F=A0=E6=A0=91?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=E6=A0=B8=E5=BF=83=E4=B8=8B=E6=8B=89=E6=A1=86?= =?UTF-8?q?=EF=BC=88#24=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来侧栏那个分类挂件是 WP 核心的 WP_Widget_Categories,实例配置 dropdown=1,输出的是 ` —— 一个光秃秃的 + * 系统下拉框,跟侧栏其余挂件(圆角卡片、14/15px 字号、悬停反馈)完全不搭。 + * 更糟的是主题里那套 `.widget_categories ul li` 规则只匹配 `ul`, + * 下拉框里没有 `ul` ⇒ **写了也一条不生效**。 + * + * 现在改成主题自建的父子折叠树,和 `.w-comments` / `.w-tags` 同一套视觉。 + * + * ── 一条硬约束(3.2.51 的教训,别再踩)──────────────────────────────── + * 🔴 选择器**只依赖 `.w-cats` 自身**,绝不写 `.k-main .sidebar .w-cats`。 + * 3.2.51「个人简介撑爆」就是三段祖先选择器造成的:挂件在后台 + * 「外观→小工具」的预览里没有 `.k-main .sidebar` 那层,规则全部失效。 + * 前台看着正常,恰恰把问题藏死。 + * 🔴 所以这里**不写内边距/白底**:那由外层 `.widget` 提供 + * (前台 `.k-main .sidebar .widget`,后台由 WP 自己的挂件容器提供)。 + * + * ── 视觉层级 ────────────────────────────────────────────────────────── + * 父分类 15px #333 ← 主体,带折叠箭头 + * 子分类 14px #666 ← 比父级轻一档,靠左侧缩进 + 细线表示从属 + * 计数 12px #bbb ← 最轻,右对齐;父级数字含子级(pad_counts) + * 三层字号必须不同 —— 一样大的话 29 个分类会糊成一片,找不着重点。 + * ══════════════════════════════════════════════════════════════════════ */ + +.w-cats .yang-cat-list { + margin: 0; + padding: 0; + list-style: none; +} + +/* 首层每一行:让「名字 · 数字」两端对齐。 + * + * 🔴🔴 这里踩过一个真坑,记清楚 —— 两种行的 DOM 结构**不一样**: + * + * 无子级:
  • + * 名字 + * 12 ← a 的**兄弟** + *
  • + * 有子级:
  • + *
    + * + * 名字 ← 都是 summary 的**子项** + * 120 + * ...
    + *
  • + * + * 我第一版把 flex 写在 `.yang-cat-item > .yang-cat-link` 上(也就是 `` 自身), + * 结果无子级那一行里**只有 `` 变成 flex 容器,而它内部只有一个文本节点**, + * 于是 `space-between` 没有第二个子项可推 —— 数字留在原地紧贴文字, + * 实测右侧间隙 299px、行高被撑到 36.5px(数字挤到下一行)。 + * + * ⇒ 正确做法:flex 必须加在**同时包含名字和数字的那一层**, + * 也就是 `
  • `(无子级)与 ``(有子级)。 + * + * 🔴🔴 还要**重置主题自己的全局 `ul > li`**(kratos.css:74)—— + * 这条是 3.2.54 实测抓出来的,光看代码很难想到: + * + * ul > li { margin-bottom: 6px; padding-left: 2px; + * list-style: circle; line-height: 22px; } + * + * 它的特异性是 (0,0,2),而我们 `.w-cats .yang-cat-item` 是 (0,2,0), + * 本来能压过 —— 但 `padding-left` / `margin-bottom` 那几条 + * **只写在了 `:not(.has-children)` 上**,带子级的 `
  • ` 完全没被覆盖: + * + * 实测(线上真实页面注入新 DOM 后 getComputedStyle): + * 无子级 li display:flex pl:0px mb:6px + * 有子级 li display:list-item pl:2px mb:6px ← 没被覆盖 + * + * 后果有两个,都是线上能看出来的: + * ① 每行之间多出 6px 空隙,8 行累积 42px —— 行距看着松散、不齐; + * ② 有子级那行名字被推开 2px,与无子级行**对不齐**(实测 30 vs 45, + * 其中 13px 是箭头按设计占的宽度,剩下 2px 就是这个 padding)。 + * + * ⇒ 改成在 `.yang-cat-item` 上统一重置,不靠 `:not()` 分叉。 + * 这样两种行基线一致,将来加第三层结构也不会漏。 */ +.w-cats .yang-cat-item { + margin-bottom: 0; + padding-left: 0; + list-style: none; + /* `
  • ` 的 line-height 也被全局规则钉成 22px,这里跟上组件字号 */ + line-height: 1.5; +} + +/* 两种行都要成为 flex 容器,名字与数字分居两端。 + * 🔴 用 `display:flex` 覆盖 `list-item` 会**同时**干掉圆点标记 + * (list-item 是唯一能显示 marker 的 display 值), + * 所以这里不再需要额外写 list-style-type:none 去压 circle。 + * 🔴 上下 padding 不在这里给:带子级的行由 自己撑(它也写了 + * 7px),两处都写会变成 14px,行高直接翻倍。分开写在下面。 */ +.w-cats .yang-cat-item { + display: flex; + align-items: center; + justify-content: space-between; +} + +/* 只有「无子级」和「子级」这两种叶子行需要自己撑上下间距 */ +.w-cats .yang-cat-item:not(.has-children) { + padding: 7px 0; +} + +/* 行内文字(无子级 li 里的 a、有子级 summary 里的 a): + * 只负责字号/颜色/可换行,**不再**做 flex 容器。 */ +.w-cats .yang-cat-item > .yang-cat-link { + min-width: 0; + color: #333; + font-size: 15px; + line-height: 1.5; + text-decoration: none; + /* 中文长分类名没有空格,必须允许任意位置换行,否则撑破挂件 */ + overflow-wrap: break-word; + word-wrap: break-word; +} + +.w-cats .yang-cat-item > .yang-cat-link:hover { + color: #00a2ff; +} + +/* ── 折叠容器 ────────────────────────────────────────────────────────── */ + +/* 🔴 默认带 display:list-item 与三角形 marker。 + * 我们要自己的箭头(用 CSS 画,不依赖字体图标), + * 所以先关掉原生 marker —— 两个都显示会变成"双箭头"。 */ +.w-cats .yang-cat-summary { + display: flex; + align-items: center; + padding: 7px 0; + color: #333; + font-size: 15px; + line-height: 1.5; + cursor: pointer; + list-style: none; /* Firefox */ + /* 关掉长按选中,避免手机端点开时误选文字 */ + -webkit-user-select: none; + user-select: none; +} + +.w-cats .yang-cat-summary::-webkit-details-marker { + display: none; +} + +.w-cats .yang-cat-summary:hover { + color: #00a2ff; +} + +/* 🔴🔴 `
    ` 必须显式撑满整行宽度,否则**两种行的数字对不齐**。 + * + * 这是 3.2.54 视觉几何实测抓出来的真缺陷(不是量法问题): + * + * 无子级行( 是 li 的直接子项) 数字右边界 220.5 + * 有子级行(
    包着) 数字右边界 227.6 ← 差 7.1px + * + * 原因:无子级行的 `` 与数字是 li 的**两个** flex 子项, + * `space-between` 把它们推到两端,数字贴右沿。 + * 有子级行的 li 里只有 **一个** flex 子项 `
    `, + * `space-between` 没有第二个子项可推 ⇒ 不起作用; + * 而 `
    ` 默认宽度是 auto(按内容收缩), + * 于是整组靠左,数字停在内文字结尾处。 + * + * ⇒ 给 `
    ` 加 `flex: 1 1 auto; min-width: 0`, + * 让它吃满 li 的可用宽度,内部的 summary 才能把数字推到右沿。 + * `min-width: 0` 不能省:它是 flex 项允许收缩到内容宽度以下的前提, + * 长分类名才能按 overflow-wrap 换行而不是撑破容器(3.2.51 的教训)。 + * + * 这条同时解释了为什么 `` 自己写了 width 也没用 —— + * 父级 `
    ` 收缩了,子级撑满的只是那个收缩后的宽度。 */ +.w-cats .yang-cat-group { + flex: 1 1 auto; + min-width: 0; +} + +/* summary 里的名字链接:占满剩余宽度,让数字被推到最右 */ +.w-cats .yang-cat-summary .yang-cat-link { + flex: 1 1 auto; + min-width: 0; + color: inherit; + text-decoration: none; + overflow-wrap: break-word; + word-wrap: break-word; +} + +.w-cats .yang-cat-summary:hover .yang-cat-link { + color: #00a2ff; +} + +/* 箭头:CSS 画的实心三角,展开时旋转 90°。 + * + * 🔴 为什么不用字体图标:主题的 kicon 字体只包含固定的几个字形, + * 加一个要动字体文件;而这是个极简三角形,CSS 边框三角形足够, + * 且不会因字体没加载而"箭头消失"。 */ +.w-cats .yang-cat-caret { + flex: 0 0 auto; + width: 0; + height: 0; + margin-right: 8px; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 5px solid #bbb; + -webkit-transition: -webkit-transform 0.2s; + transition: transform 0.2s; +} + +.w-cats .yang-cat-summary:hover .yang-cat-caret { + border-left-color: #00a2ff; +} + +/* 展开时箭头朝下 */ +.w-cats details[open] > .yang-cat-summary .yang-cat-caret { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +/* 子级列表:左侧缩进 + 一条竖线表示从属关系。 + * 🔴 竖线用 border-left 而不是额外元素:分类层级只有 2 层, + * 加伪元素会让"缩进"和"线"两套定位各自出错。 */ +.w-cats .yang-cat-children { + margin: 0; + padding: 0 0 2px 13px; + border-left: 1px solid #f0f0f0; + list-style: none; +} + +.w-cats .yang-cat-children > .yang-cat-item { + list-style: none; + /* 子级行比父级紧一档(5px vs 7px):层级越深、行距越小, + * 视觉上才"收"得进去,否则子列表会显得和父级一样重。 */ + padding: 5px 0; +} + +/* 🔴 子级 li 的 flex 已由上面通用的 `.w-cats .yang-cat-item` 提供, + * 这里**只需**给行高与间距,绝不能再在 `` 上写 flex —— + * 那正是本组件第一版的原始 bug(见上方 DOM 结构说明): + * 子级的 `` 里只有一个文本节点,数字是它的**兄弟**, + * 在 `` 上写 flex + space-between 推不动任何东西。 */ +/* 子级每一行:与父级同样两端对齐,但字号轻一档、颜色浅一档 */ +.w-cats .yang-cat-children > .yang-cat-item > .yang-cat-link { + min-width: 0; + /* 左侧不用 padding:竖线已经提供了缩进,再加就成双倍缩进 */ + color: #666; + font-size: 14px; + line-height: 1.5; + text-decoration: none; + overflow-wrap: break-word; + word-wrap: break-word; +} + +.w-cats .yang-cat-children > .yang-cat-item > .yang-cat-link:hover { + color: #00a2ff; +} + +/* ── 文章数 ──────────────────────────────────────────────────────────── */ + +.w-cats .yang-cat-count { + flex: 0 0 auto; + margin-left: 8px; + padding: 1px 7px; + border-radius: 10px; + background-color: #f5f5f5; + color: #999; + font-size: 12px; + line-height: 18px; + -webkit-transition: 0.2s; + transition: 0.2s; +} + +/* 悬停联动:鼠标在名字(或整行)上时,数字胶囊也变成主题蓝。 + * + * 🔴 第一版写错了:`.yang-cat-item > .yang-cat-link:hover .yang-cat-count` + * 里的**空格**表示"数字是链接的后代",可实际上数字是链接的**兄弟** + * (见上面两种行的 DOM 结构)。于是无子级那一行悬停时胶囊颜色不变, + * 实测 rgb(245,245,245) → rgb(245,245,245),联动完全没生效。 + * + * 正确写法有两条: + * ① 无子级行:`.yang-cat-item:hover > .yang-cat-count` + * —— 悬停在整行(li)上,把**直接子级**的数字染色。 + * 用 `>` 而不是空格,避免连带命中子级的计数。 + * ② 有子级行:`.yang-cat-summary:hover .yang-cat-count` + * —— summary 里数字本来就是后代,这条原来是对的,保留。 */ +.w-cats .yang-cat-item:not(.has-children):hover > .yang-cat-count, +.w-cats .yang-cat-summary:hover .yang-cat-count { + background-color: #00a2ff; + color: #fff; +} + +/* ── 当前分类高亮 ────────────────────────────────────────────────────── */ + +/* 只染色不加粗:加粗会让这一行宽度变化,29 行里突兀地抖一下。 + * + * 🔴 三种情况都要覆盖(同上,注意无子级的 a 是 li 的直接子项、 + * 有子级的 a 在 summary 里,不能用同一个后代选择器一把抓): + * · 无子级的行高亮 → li.current-cat > .yang-cat-link + * · 有子级的行高亮 → li.current-cat > .yang-cat-summary + * 以及 summary 里的链接(summary 自身不染色时链接会覆盖掉) + * · 子级行高亮 → 子级 li.current-cat > .yang-cat-link + * (这条原来漏了:子级 li 也是 .yang-cat-item,但它的 a 是 + * 直接子项,第一条 `> .yang-cat-link` 其实已经覆盖, + * 这里显式写出以免将来改结构时断掉) */ +.w-cats .yang-cat-item.current-cat > .yang-cat-link, +.w-cats .yang-cat-item.current-cat > .yang-cat-summary, +.w-cats .yang-cat-item.current-cat > .yang-cat-summary .yang-cat-link, +.w-cats .yang-cat-children > .yang-cat-item.current-cat > .yang-cat-link { + color: #00a2ff; +} + +/* 看子分类时,父级也给个提示(不与真正的高亮抢戏) */ +.w-cats .yang-cat-item.current-cat-parent > .yang-cat-summary .yang-cat-link { + color: #00a2ff; +} + .k-main .sidebar .w-recommended .bookmark-item { display: block; overflow: hidden; @@ -2208,7 +2519,6 @@ button:focus { .k-main .sidebar .w-tags .item a:hover, .k-main .sidebar .widget_links ul li:hover, -.k-main .sidebar .widget_categories ul li:hover, .k-main .sidebar .widget_archive ul li:hover { background-color: #eeeeee; } @@ -2290,13 +2600,23 @@ button:focus { box-shadow: none; } -.k-main .sidebar .widget_categories ul, +/* ── 归档挂件(核心 widget_archive)的格子布局 + * + * 🔴 3.2.54 起这里**只剩归档**,分类那半已经删掉(工单 #24)。 + * 原因:原来那套 `.widget_categories ul li` 一条都没生效过 —— + * 线上分类挂件的实例配置是 `dropdown = 1`,核心输出的是 + * `` 下拉框(`wp_dropdown_categories`), + * 不是列表。主题 CSS 里那套 `.widget_categories ul li`(kratos.css 2293 行起) + * 因此**一条都没生效** —— 它只匹配 `ul li`,下拉框里根本没有 `ul`。 + * 「丑」的直接原因就在这里:不是样式没写,是**样式和渲染形态对不上**。 + * + * 所以本挂件用核心的同一个 id_base `categories` 顶替它(主题的 + * `widgets_init` 在核心 `init` 之后跑,`register_widget` 覆盖同 id_base 的项)。 + * 好处是**用户升级后什么都不同做**: + * · 侧栏里那个 `categories-3` 实例直接变成新样式,位置不变; + * · 已存的 `title` = 「文章分类」原样沿用,不用重新填; + * · 不存在"旧的还在旁边、要手动删掉"的迁移步骤。 + * + * 🔴 为什么 id_base 必须**沿用** `categories` 而不是换新名字: + * 换新 id_base 会得到新的 option 名(`widget_新名字`),旧实例 `categories-3` + * 的配置留在 `widget_categories` 里没人读 —— 用户看到的是"挂件凭空消失"。 + * 沿用 `categories` 则 option 名不变,存量实例继续有效。 + * 这也意味着 update() **必须容忍核心那套老字段**(dropdown 等),见下方说明。 + * + * ── 「父级常显 + 子级可折叠」的呈现(用户 2026-10-27 选定)────────────── + * 本站实测 29 个分类(9 父 + 20 子)。全展开侧栏要拉长约 1100px, + * 所以默认只列父级,有子级的父级带一个折叠箭头,点开才展开。 + * 🔴 折叠用 `
    /` 原生实现,**不写一行 JS**: + * - 不依赖 jQuery/Bootstrap 版本,不会跟主题其它脚本抢事件; + * - 键盘可操作、读屏软件能正确朗读,自带 `aria-expanded` 语义; + * - 关掉 JS 也能用(分类导航属于基础功能,不该依赖脚本)。 + * `` 里同时放"父级链接 + 箭头按钮"会有嵌套点击问题, + * 所以箭头用 `::marker` 之外的独立 span,父级链接本身仍是普通 ``。 + * + * ── 父级数字为什么要 pad_counts(这条最容易做错)──────────────────────── + * 核心 `wp_list_categories()` 在 `show_count && hierarchical` 时**自动**打开 + * `pad_counts`(wp-includes/category-template.php:392-393)。实测本站: + * + * 生活记录 原始 count = 188 + * 生活记录 带 pad_counts = 220 (188 + 博士6 + 美国22 + 游泳4) + * + * 线上那个下拉框显示的也是 220。如果这里取原始 count,同一个分类会在 + * 升级前后显示成两个不同的数字(220 → 188),用户会以为文章丢了。 + * 所以这里显式 `'pad_counts' => true`,与核心行为保持一致。 + * + * ── 分类名里的 HTML 实体:实测结论是「不用管」(记一次自我纠正)────── + * 线上有一个分类名在库里存的就是 `Ubuntu&Debian`(id=122), + * 不是 `Ubuntu&Debian`。我一开始认定必须 `html_entity_decode()` 一层, + * 理由是"下拉框里会显示成 `Ubuntu&Debian`"。**实测证明这个判断是错的**: + * + * raw = [Ubuntu&Debian] + * decode = [Ubuntu&Debian] + * esc(raw) = [Ubuntu&Debian] + * esc(decode) = [Ubuntu&Debian] ← 两者完全相同 + * + * 关键点:`esc_html()` 对 `&` 的输出**恒为** `&`。所以 + * · 库里存 `&` → esc_html 变 `&` → 浏览器显示 `&` + * · 库里存 `&` → esc_html 不动它 → 浏览器显示 `&` + * 两种数据经 esc_html 后**字节完全一致**,浏览器里都是 `Ubuntu&Debian`, + * 本来就是正常的。加 decode 不会改变任何输出,只是个看着"更讲究"的冗余。 + * + * ⇒ 所以这里**不 decode**,直接用 `esc_html()`。少一层处理 = 少一处 + * 可能出错的地方(decode 反而有风险:若将来分类名里有人写了 + * `<script>` 这种真·字面量,decode 会把它变成真标签)。 + * 这个教训和工单 #23 那次一样:**先实测,再写注释**。 + * ====================================================================== */ + +class widget_categories extends WP_Widget +{ + public function __construct() + { + /* id_base 沿用核心的 'categories'(接管,理由见上方), + * 名字走第 2 参(工单 #23,理由见 widget_ad 的注释)。 */ + $widget_ops = array( + 'classname' => 'widget_categories', + 'description' => __('按父子层级展示文章分类,父级常显、子级可折叠', 'kratos'), + ); + + parent::__construct('categories', __('文章分类', 'kratos'), $widget_ops); + } + + /** + * 取实例字段;没填就用默认值。 + * + * 🔴 判空一律「trim 后是不是空串」,不用 empty(): + * `!empty('0')` 是 false。本挂件里的 `count` 是 0/1 开关、 + * 标题也可能就叫「0」,用 empty() 会被当"没填"。 + * 与 widget_about::pick() / widget_recent_comments::pick() 同一套判据。 + * + * @param array $instance + * @param string $key + * @param mixed $fallback + * @return mixed + */ + private function pick($instance, $key, $fallback) + { + if (!isset($instance[$key])) { + return $fallback; + } + + $v = $instance[$key]; + + // 布尔开关:checkbox 没勾时不提交,所以「缺键 = 关」由上面的 isset 处理, + // 键在则按真值判定(核心老实例存的是 1/0 整数,也能走这里) + if (is_bool($fallback)) { + return (bool) $v; + } + + if (is_int($fallback)) { + $s = is_scalar($v) ? trim((string) $v) : ''; + if ($s === '' || !preg_match('/^-?\d+$/', $s)) { + return $fallback; + } + + return (int) $s; + } + + if (!is_string($v)) { + return $fallback; + } + + $v = trim($v); + + return ($v === '') ? $fallback : $v; + } + + /** + * 分类名转成可安全输出的文本。 + * + * 只做 `esc_html()`,**不做** html_entity_decode —— 原因见类注释里的 + * 实测数据:esc_html 对 `&` 的输出恒为 `&`,所以库里存 `&` 还是 + * 存 `&`,最终 HTML 字节一致、浏览器显示也一致。decode 是冗余。 + * 反过来 decode 有真实风险:分类名里若有人写了字面量 `<script>`, + * decode 会把它还原成真标签再 esc 一遍 —— 白白多一层可被绕过的路径。 + * + * @param string $name + * @return string + */ + private function name_html($name) + { + return esc_html((string) $name); + } + + /** + * 取分类树。 + * + * @param bool $show_count 是否带上文章数(pad_counts 规则见类注释) + * @param bool $hide_empty 是否隐藏空分类 + * @param string $orderby 'name' | 'count' + * @param string $order 'ASC' | 'DESC' + * @return array + */ + private function get_tree($show_count, $hide_empty, $orderby, $order) + { + $args = array( + 'hide_empty' => (bool) $hide_empty, + 'orderby' => $orderby, + 'order' => $order, + /* 🔴 pad_counts 必须显式给:核心 wp_list_categories 是靠 + * 「show_count && hierarchical」**隐式**打开的 + * (category-template.php:392)。这里若不给,父级数字 + * 会比升级前小(220 → 188),看着像丢了文章。 */ + 'pad_counts' => (bool) $show_count, + ); + + /* 🔴🔴 「按文章数排序」不能交给 SQL —— 这条实测踩过。 + * + * `orderby=count` 在 WP_Term_Query::parse_orderby() 里被翻成 + * `ORDER BY tt.count`(class-wp-term-query.php:926-927), + * 排的是数据库里的**原始** count 列;而 pad_counts 是在查询 + * 之后由 PHP 补的(父级 = 自己 + 所有子孙)。 + * + * 两者不一致的直接后果(实测本站,orderby=count&order=DESC): + * + * 生活记录 220 ← 排第 1(原始 188) + * 评论杂谈 168 + * 信息技术 233 ← 排第 3(原始 233,最大!) + * 情感随笔 113 + * 人生感悟 120 + * + * 界面上看就是"数字大的排在小的后面"—— 用户会认为排序坏了。 + * 所以 count 排序改成「先按 name 取回、再在 PHP 里按补过的 + * count 排」。取回本来就要全量(要建树),不会多一次查询。 */ + if ($orderby === 'count') { + $args['orderby'] = 'name'; + $args['order'] = 'ASC'; + } + + $cats = get_categories($args); + if (!is_array($cats)) { + return array(); + } + + /* get_categories 返回的是平铺数组(按 orderby 排好), + * 这里按 parent 归拢成树。不用 Walker,因为要输出
    , + * 而 Walker_Category 只认