From afd08885fc2f9660fc2d5e24061137769f250942 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 9 Oct 2026 11:49:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E6=94=AF=E4=BB=98=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E5=88=86=E9=A1=B5=E6=9D=A1=E6=94=B9=E5=9B=9E=E5=9B=9B?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AdminListPagination 的输出标记从 WP 原生 paginate_links() 换回「卡密管理」 原版的四按钮式:`« ‹ 第 X 页,共 Y 页 › »` +「每页 X 共Y」计数,没有数字页码 链接。七个页面调用点不变,外观统一为卡密管理那一套。 - 删除 $total_pages <= 1 早退:只要总数 > 0 就渲染分页条,单页时四枚按钮全 disabled —— 与原版 render_pagenavi 行为一致 - 基准 URL 与页码夹取沿用现有实现(显式拼装、夹到 [1, total_pages]), 这两处缺陷修复不动 - 标题属性由 esc_attr_e() 改为 esc_attr__():输出相同,且同文件与 dev harness 都只用后者 - 新增私有静态 print_assets(),由 render() 调用并以 $assets_printed 保证每页 只输出一次;内容只有 .tablenav-pages a.disabled { pointer-events: none; } 及其注释。这些原生后台页面不加载主题 CSS,故随标记内联 清理 harness 中已无人调用的 number_format_i18n() 与 paginate_links() stub。 验证:tests/admin/ 76 项通过;tests/widget-system/ 458/459(仅剩与本次无关的 lazyload-contract 既有失败);用 git show HEAD~1 取出原 render_pagenavi() 与 新助手在 8 组输入(含单页、总数为 0、per_page 为 0、页码越界)下比对,规范化 空白后输出逐字节一致;harness 渲染售后面板确认四按钮标记与内联样式就位。 --- src/Admin/Native/AdminListPagination.php | 95 ++++++++++++++------ tests/admin/charge-cards-pagination.test.cjs | 7 +- tests/admin/pay-pages-pagination.test.cjs | 48 +++++++--- tests/dev/render-after-sales-harness.php | 2 - 4 files changed, 104 insertions(+), 48 deletions(-) diff --git a/src/Admin/Native/AdminListPagination.php b/src/Admin/Native/AdminListPagination.php index 6e5891fc..299cb63b 100644 --- a/src/Admin/Native/AdminListPagination.php +++ b/src/Admin/Native/AdminListPagination.php @@ -2,13 +2,22 @@ /** * @package AKSRT * - * 后台列表页分页条(WP 原生 paginate_links)。 + * 后台列表页分页条 —— 1:1 复刻「卡密管理」页的 zibpay_admin_pagenavi 结构。 * * 支付中心各页面(订单明细 / 售后服务 / 付费文章 / 优惠码 / 创作分成 / 提现审核 / 卡密管理) - * 原先各写一份几乎相同的 paginate_links 调用,且基准 URL 都写成 - * add_query_arg('paged', '%#%') —— 只有两个参数时 WordPress 会退到 - * $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态漂移,列条件也容易丢。 - * 这里统一到一处:显式以「page 参数 + 当前筛选参数」为基准拼装,页码先夹到有效范围。 + * 原先各写一份几乎相同的翻页代码。这里统一到一处,标记沿用卡密管理那一套: + * `« ‹ 第 X 页,共 Y 页 › »` 四枚按钮 + 「每页 X 共Y」计数,没有数字页码链接。 + * + * 与参考实现的两处差异,均为缺陷修复: + * 1) 基准 URL 显式以「page 参数 + 当前筛选参数」拼装。历史写法里 add_query_arg('paged', N) + * 只有两个参数时会落到 $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态 + * 漂移,列条件也容易丢。 + * 2) 页码先夹到 [1, total_pages],禁用状态由夹取后的页码推导。历史写法在此处会留下 + * 「第 2 页 / 共 1 页」这类越界状态,四个分支一个都不命中,上一页/下一页同时停在 + * '#':按钮既不禁用也点不动。 + * + * 单页也渲染分页条(只要总数 > 0):与参考实现一致,运营能直接看到「每页 20 共 5」 + * 这样的口径,四枚按钮全 disabled 也明确了「没有更多页」。 */ namespace AKSRT\Admin\Native; @@ -20,12 +29,14 @@ class AdminListPagination { /** 每页条数默认值。所有列表页一致,且不提供「每页显示」控件。 */ public const DEFAULT_PER_PAGE = 20; + /** @var bool 内联样式只输出一次 */ + private static $assets_printed = false; + /** * 渲染分页条。 * - * 页码夹取到 [1, total_pages] 再决定渲染与否:历史写法在此处会留下「第 2 页 / 共 1 页」 - * 这类越界状态。真正的数据回退(数据也要跟着落到最后一页)由各页面在查询前完成, - * 这里只负责把显示状态收敛到自洽。 + * 真正的数据回退(数据也要跟着落到最后一页)由各页面在查询前完成,这里只把页码 + * 夹到有效范围,保证显示状态自洽。 * * @param string $page_slug 当前 admin.php 的 page 参数。 * @param int $total_count 当前筛选条件下的记录总数。 @@ -44,31 +55,57 @@ class AdminListPagination { return; } + self::print_assets(); + $total_pages = max(1, (int) ceil($total_count / $per_page)); $current_page = min(max(1, $current_page), $total_pages); - if ($total_pages <= 1) { + // 显式基准 URL:不再让 add_query_arg() 只有两个参数时退回 $_SERVER['REQUEST_URI']。 + $base = add_query_arg(array_merge(['page' => $page_slug], $query_args), admin_url('admin.php')); + $page_url = static function (int $page) use ($base): string { + return add_query_arg('paged', $page, $base); + }; + + $on_first = $current_page <= 1; + $on_last = $current_page >= $total_pages; + + $first_page_url = $on_first ? '#' : $page_url(1); + $prev_page_url = $on_first ? '#' : $page_url($current_page - 1); + $next_page_url = $on_last ? '#' : $page_url($current_page + 1); + $last_page_url = $on_last ? '#' : $page_url($total_pages); + ?> +
+
+ + + « + ‹ + ' . esc_html((string) $total_pages) . ''); ?> + › + » + +
+
+ $page_slug], $query_args), admin_url('admin.php')); - $base = add_query_arg('paged', '%#%', $base); - - echo '
'; - echo '' . esc_html(sprintf( - /* translators: %s: 记录总数 */ - __('共 %s 项', 'aksrt'), - number_format_i18n($total_count) - )) . ''; - echo paginate_links([ - 'base' => $base, - 'format' => '', - 'current' => $current_page, - 'total' => $total_pages, - 'prev_text' => '«', - 'next_text' => '»', - ]); // phpcs:ignore WordPress.Security.EscapeOutput -- paginate_links 自行转义。 - echo '
'; + self::$assets_printed = true; + ?> + + { assert.doesNotMatch(page, /self::DEFAULT_PER_PAGE/); assert.match(page, /AdminListPagination::DEFAULT_PER_PAGE/); - // 只服务自定义禁用按钮的 CSS 规则一并删除。 + // 只服务自定义禁用按钮的 CSS 规则已随标记一起搬进助手。 assert.doesNotMatch(page, /\.tablenav-pages a\.disabled \{ pointer-events: none; \}/); }); diff --git a/tests/admin/pay-pages-pagination.test.cjs b/tests/admin/pay-pages-pagination.test.cjs index 9a068ae4..1b81c740 100644 --- a/tests/admin/pay-pages-pagination.test.cjs +++ b/tests/admin/pay-pages-pagination.test.cjs @@ -1,7 +1,7 @@ 'use strict'; /** - * 支付中心列表页分页契约(WP 原生 paginate_links,统一到 AdminListPagination)。 + * 支付中心列表页分页契约(四按钮式,统一到 AdminListPagination)。 * * 背景(本次收敛的缺陷): * 1) 各页面的翻页基准 URL 写成 add_query_arg('paged', '%#%') —— 只有两个参数时 @@ -15,6 +15,8 @@ * * 本契约固化: * - 七个页面全部改走 AdminListPagination::render(),不再有各自的翻页实现; + * - 渲染标记是卡密管理那一套:« ‹ 第 X 页,共 Y 页 › » 四枚按钮 + 「每页 X 共Y」计数, + * 没有数字页码链接;只要总数 > 0 就渲染,单页时四枚按钮全 disabled; * - 每页条数默认 20 且来自共享常量,不新增「每页显示」控件; * - 每页都在查询前夹取页码,越界时重查,杜绝「空表且没有分页条」的死胡同; * - 翻页链接显式带上当前筛选参数,不依赖 REQUEST_URI。 @@ -185,26 +187,44 @@ test('提现审核不把一次性动作参数带进翻页链接', () => { } }); -test('共享助手自身:页码夹取、早退与显式基准 URL', () => { +test('共享助手自身:页码夹取、四按钮标记与显式基准 URL', () => { assert.match(helper, /namespace AKSRT\\Admin\\Native;/); assert.match(helper, /defined\('ABSPATH'\) \|\| exit;/); - // 空态与单页都不渲染。 + // 总数为 0 或每页条数非法时不渲染;单页照样渲染(与原版 render_pagenavi 一致)。 assert.match(helper, /if \(\$total_count <= 0 \|\| \$per_page <= 0\) \{\s*return;\s*\}/); + assert.doesNotMatch(helper, /\$total_pages <= 1/, '单页也必须渲染分页条'); assert.match(helper, /\$total_pages\s*=\s*max\(1, \(int\) ceil\(\$total_count \/ \$per_page\)\);/); assert.match(helper, /\$current_page = min\(max\(1, \$current_page\), \$total_pages\);/); - assert.match(helper, /if \(\$total_pages <= 1\) \{\s*return;\s*\}/); - // 基准 URL 显式拼装:add_query_arg 必带第三个参数(那正是两参数写法退回 REQUEST_URI 的根治点)。 - assert.match(helper, /\$base = add_query_arg\(array_merge\(\['page' => \$page_slug\], \$query_args\), admin_url\('admin\.php'\)\);/); - assert.match(helper, /\$base = add_query_arg\('paged', '%#%', \$base\);/); + // 基准 URL 显式拼装:add_query_arg 必带第三个参数(两参数写法会退回 REQUEST_URI)。 + assert.match(helper, /\$base\s*=\s*add_query_arg\(array_merge\(\['page' => \$page_slug\], \$query_args\), admin_url\('admin\.php'\)\);/); + assert.match(helper, /\$page_url = static function \(int \$page\) use \(\$base\): string \{/); + assert.match(helper, /return add_query_arg\('paged', \$page, \$base\);/); + assert.doesNotMatch(helper, /add_query_arg\('paged',\s*'[^']*'\s*\)/); - // 原生 WP 分页 + 总数提示。 - assert.match(helper, /
/); + // 卡密管理式四按钮标记。 + assert.match(helper, /
/); + assert.match(helper, /
/); assert.match(helper, //); - assert.match(helper, /number_format_i18n\(\$total_count\)/); - assert.match(helper, /echo paginate_links\(\[/); - assert.match(helper, /'prev_text' => '«',/); - assert.match(helper, /'next_text' => '»',/); - assert.match(helper, /phpcs:ignore WordPress\.Security\.EscapeOutput -- paginate_links 自行转义。/); + assert.match(helper, /每页 %1\$s 共%2\$s/); + assert.match(helper, //); + assert.match(helper, //); + assert.match(helper, /第 %1\$s 页,共 %2\$s 页/); + assert.match(helper, //); + for (const cls of ['first-page', 'prev-page', 'next-page', 'last-page']) { + assert.match(helper, new RegExp(`class="${cls} button`), `缺少 ${cls} 按钮`); + } + assert.match(helper, /\$on_first = \$current_page <= 1;/); + assert.match(helper, /\$on_last\s*=\s*\$current_page >= \$total_pages;/); + assert.equal(count(helper, /\$on_first \? '#' :/g), 2, '第一页/上一页都要按 on_first 落到 #'); + assert.equal(count(helper, /\$on_last \? '#' :/g), 2, '下一页/最后一页都要按 on_last 落到 #'); + + // 禁用的翻页按钮不可点击,否则 会把页面滚回顶部。 + assert.match(helper, /\.tablenav-pages a\.disabled \{ pointer-events: none; \}/); + assert.match(helper, /private static \$assets_printed = false;/); + assert.match(helper, /private static function print_assets\(\): void \{/); + + // 不再使用 WP 原生 paginate_links。 + assert.doesNotMatch(helper, /echo paginate_links\(/); }); diff --git a/tests/dev/render-after-sales-harness.php b/tests/dev/render-after-sales-harness.php index 9ffedf91..9dc831cf 100644 --- a/tests/dev/render-after-sales-harness.php +++ b/tests/dev/render-after-sales-harness.php @@ -55,8 +55,6 @@ function wp_die($m) { throw new Exception('wp_die: ' . $m); } function get_userdata($id) { return $id > 0 ? (object) ['display_name' => 'User' . $id] : null; } function get_the_title($id) { return 'Product' . $id; } function is_admin() { return true; } -function number_format_i18n($n, $d = 0) { return number_format((float) $n, $d); } -function paginate_links($args = []) { return ''; } } namespace AKSRT\Ledger {