fix(admin): 支付中心分页条改回四按钮式 #39

Merged
kate522 merged 1 commits from worktree/pay-pages-pagination-style into main 2026-10-09 03:56:26 +00:00
4 changed files with 104 additions and 48 deletions
+65 -28
View File
@@ -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) {
return;
}
// 显式基准 URL:不再让 add_query_arg() 只有两个参数时退回 $_SERVER['REQUEST_URI']。
$base = add_query_arg(array_merge(['page' => $page_slug], $query_args), admin_url('admin.php'));
$base = add_query_arg('paged', '%#%', $base);
$page_url = static function (int $page) use ($base): string {
return add_query_arg('paged', $page, $base);
};
echo '<div class="tablenav"><div class="tablenav-pages">';
echo '<span class="displaying-num">' . esc_html(sprintf(
/* translators: %s: 记录总数 */
__('共 %s 项', 'aksrt'),
number_format_i18n($total_count)
)) . '</span>';
echo paginate_links([
'base' => $base,
'format' => '',
'current' => $current_page,
'total' => $total_pages,
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
]); // phpcs:ignore WordPress.Security.EscapeOutput -- paginate_links 自行转义。
echo '</div></div>';
$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);
?>
<div class="tablenav bottom">
<div class="tablenav-pages">
<span class="displaying-num"><?php printf(esc_html__('每页 %1$s 共%2$s', 'aksrt'), esc_html((string) $per_page), esc_html((string) $total_count)); ?></span>
<span class="pagination-links">
<a class="first-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往第一页', 'aksrt'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
<a class="prev-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往上一页', 'aksrt'); ?>" href="<?php echo esc_url($prev_page_url); ?>">‹</a>
<span class="paging-input"><?php printf(esc_html__('第 %1$s 页,共 %2$s 页', 'aksrt'), esc_html((string) $current_page), '<span class="total-pages">' . esc_html((string) $total_pages) . '</span>'); ?></span>
<a class="next-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往下一页', 'aksrt'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
<a class="last-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php echo esc_attr__('前往最后一页', 'aksrt'); ?>" href="<?php echo esc_url($last_page_url); ?>">»</a>
</span>
</div>
</div>
<?php
}
/**
* 行内样式。
*
* 这些原生后台页面不加载主题 CSS(admin-main.css 只在 edit.php 文章列表入队),
* 所以分页条依赖的规则随标记一起内联输出。
*/
private static function print_assets(): void {
if (self::$assets_printed) {
return;
}
self::$assets_printed = true;
?>
<style>
/* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部;
这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */
.tablenav-pages a.disabled { pointer-events: none; }
</style>
<?php
}
}
+4 -3
View File
@@ -13,8 +13,9 @@
* 3) ice_perpage 只在服务端被读取、默认每页条数偏大。条数小于默认值时永远只有一页,
* 四个按钮恒为 disabled —— 看上去就是「分页是坏的」。
*
* 现状:渲染已迁移到共享助手 AdminListPagination(WP 原生 paginate_links),
* 基准 URL 与页码夹取都在那里做。本文件只固化卡密管理仍特有的部分:
* 现状:渲染已迁移到共享助手 AdminListPagination,四按钮标记由助手统一输出
* (与这里原来那套逐行一致),基准 URL 与页码夹取也都在那里做。
* 本文件只固化卡密管理仍特有的部分:
* - ice_perpage 仍作为 URL 参数被尊重,且工具栏不新增「每页显示」控件;
* - 越界页码回退到最后一页并重查,不留空态死胡同;
* - 筛选参数白名单化拼装,排序与每页条数随链接带走。
@@ -55,7 +56,7 @@ test('渲染已迁移到共享助手 AdminListPagination', () => {
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; \}/);
});
+34 -14
View File
@@ -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, /<div class="tablenav"><div class="tablenav-pages">/);
// 卡密管理式四按钮标记。
assert.match(helper, /<div class="tablenav bottom">/);
assert.match(helper, /<div class="tablenav-pages">/);
assert.match(helper, /<span class="displaying-num">/);
assert.match(helper, /number_format_i18n\(\$total_count\)/);
assert.match(helper, /echo paginate_links\(\[/);
assert.match(helper, /'prev_text' => '&laquo;',/);
assert.match(helper, /'next_text' => '&raquo;',/);
assert.match(helper, /phpcs:ignore WordPress\.Security\.EscapeOutput -- paginate_links 自行转义。/);
assert.match(helper, /每页 %1\$s 共%2\$s/);
assert.match(helper, /<span class="pagination-links">/);
assert.match(helper, /<span class="paging-input">/);
assert.match(helper, /第 %1\$s 页,共 %2\$s 页/);
assert.match(helper, /<span class="total-pages">/);
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 落到 #');
// 禁用的翻页按钮不可点击,否则 <a href="#"> 会把页面滚回顶部。
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\(/);
});
-2
View File
@@ -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 {