fix(payment): 卡密管理每页默认 20 条,撤掉工具栏「每页显示」控件

- DEFAULT_PER_PAGE 由 30 改为 20。
- 删除 per_page_selector() 及其表单样式(.aksrt-perpage-form / .order-header-right),
  工具栏恢复为「过滤链接 + 搜索表单」两段式。
  ice_perpage 仍作为 URL 参数被尊重,只是不再提供界面入口。
- 保留上一提交的分页健壮性修复:页码夹取到 [1, total_pages]、翻页链接用显式基准 URL、
  越界页码回退并重查、禁用按钮 pointer-events:none。
- 契约测试同步改写:断言默认值为 20,并显式禁止「每页显示」控件回归。

验证:默认态渲染为「每页 20 共10」,页面内不存在 aksrt-perpage-form;
ice_perpage=5 仍能翻到第 2 页且链接携带该参数;paged=2 越界仍回退到第 1 页的 10 行;
node --test 三套 18/18;php -l 通过;无头 Chrome 截图确认工具栏无多余控件。
This commit is contained in:
2026-09-17 13:03:05 +08:00
parent 0e7b888277
commit 52d790c654
2 changed files with 26 additions and 66 deletions
+5 -50
View File
@@ -21,11 +21,8 @@ class PayChargeCards {
public const NONCE = 'aksrt_card_pass_page'; public const NONCE = 'aksrt_card_pass_page';
public const EXPORT_AJAX_ACTION = 'aksrt_card_pass_export'; public const EXPORT_AJAX_ACTION = 'aksrt_card_pass_export';
/** 每页条数默认值;列表页可由 ice_perpage 覆盖。 */ /** 每页条数默认值;列表页可由 ice_perpage 覆盖(无界面控件,仅供 URL / 程序化调用)。 */
public const DEFAULT_PER_PAGE = 30; public const DEFAULT_PER_PAGE = 20;
/** 「每页显示」下拉的可选值。 */
private const PER_PAGE_CHOICES = [10, 20, 30, 50, 100];
private static $instance = null; private static $instance = null;
@@ -158,12 +155,11 @@ class PayChargeCards {
</div> </div>
</form>'; </form>';
// 当前筛选参数(不含 paged)。分页链接与「每页显示」表单都显式带上它, // 当前筛选参数(不含 paged)。分页链接显式带上它,不再依赖 add_query_arg()
// 不再依赖 add_query_arg() 默认取 $_SERVER['REQUEST_URI'](链接会随环境漂移,且列条件容易丢)。 // 默认取 $_SERVER['REQUEST_URI'](链接会随环境漂移,且列条件容易丢)。
$query_args = $this->list_query_args($status, $type, $other, $s, $orderby, $order, $per_page); $query_args = $this->list_query_args($status, $type, $other, $s, $orderby, $order, $per_page);
echo '<div class="order-header"><ul class="subsubsub">' . $but_html . '</ul>' echo '<div class="order-header"><ul class="subsubsub">' . $but_html . '</ul>' . $search . '</div>';
. '<div class="order-header-right">' . $this->per_page_selector($per_page, $query_args) . $search . '</div></div>';
$filters = [ $filters = [
'status' => $status, 'status' => $status,
@@ -1050,44 +1046,6 @@ class PayChargeCards {
return $args; return $args;
} }
/**
* 「每页显示」选择器。
*
* 服务端一直支持 ice_perpage,但界面上没有任何地方能改它 —— 每页条数恒等于默认值,
* 于是数据量小于默认每页条数时永远只有一页,四个翻页按钮恒为 disabled,
* 看起来就是「分页是坏的」。补上这个入口之后,条数、页码才有意义。
*
* @param array<string,string> $query_args 当前筛选参数(ice_perpage 由本表单自己提供)。
*/
private function per_page_selector(int $per_page, array $query_args): string {
$choices = self::PER_PAGE_CHOICES;
if (!in_array($per_page, $choices, true)) {
$choices[] = $per_page;
sort($choices);
}
$hidden = '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '">';
foreach ($query_args as $key => $value) {
if ($key === 'ice_perpage') {
continue;
}
$hidden .= '<input type="hidden" name="' . esc_attr((string) $key) . '" value="' . esc_attr((string) $value) . '">';
}
$options = '';
foreach ($choices as $choice) {
$options .= '<option value="' . esc_attr((string) $choice) . '"' . selected($per_page, $choice, false) . '>' . esc_html((string) $choice) . '</option>';
}
return '<form class="aksrt-perpage-form" method="get" action="' . esc_url(admin_url('admin.php')) . '">'
. $hidden
. '<label class="screen-reader-text" for="aksrt-perpage">' . esc_html__('每页显示条数', 'aksrt') . '</label>'
. '<span class="aksrt-perpage-label">' . esc_html__('每页显示', 'aksrt') . '</span>'
. '<select id="aksrt-perpage" name="ice_perpage" onchange="this.form.submit();">' . $options . '</select>'
. '<button type="submit" class="button">' . esc_html__('应用', 'aksrt') . '</button>'
. '</form>';
}
/** /**
* 分页(参考主题 zibpay_admin_pagenavi 同款结构与禁用逻辑)。 * 分页(参考主题 zibpay_admin_pagenavi 同款结构与禁用逻辑)。
* *
@@ -1140,9 +1098,6 @@ class PayChargeCards {
<style> <style>
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; } .order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
.order-header .subsubsub { margin: 6px 0; } .order-header .subsubsub { margin: 6px 0; }
.order-header-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.aksrt-perpage-form { display: flex; align-items: center; gap: 6px; margin: 0; }
.aksrt-perpage-form select { min-width: 64px; }
/* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部; /* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部;
这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */ 这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */
.tablenav-pages a.disabled { pointer-events: none; } .tablenav-pages a.disabled { pointer-events: none; }
+21 -16
View File
@@ -10,13 +10,16 @@
* URL 里还留着 paged=2,而总数只剩 1 页)三个分支一个都不命中,上一页/下一页 * URL 里还留着 paged=2,而总数只剩 1 页)三个分支一个都不命中,上一页/下一页
* 同时停在 '#':按钮既不打 disabled 也点不动,还停在「第 1 页」状态无法自洽。 * 同时停在 '#':按钮既不打 disabled 也点不动,还停在「第 1 页」状态无法自洽。
* 极端情况下 offset 落到数据之外,表格直接变空态并且连分页条都不输出,只能手改 URL 退出。 * 极端情况下 offset 落到数据之外,表格直接变空态并且连分页条都不输出,只能手改 URL 退出。
* 3) ice_perpage 只在服务端被读取,界面上没有任何入口能改每页条数。条数小于默认每页条数时 * 3) ice_perpage 只在服务端被读取、默认每页条数偏大。条数小于默认值时永远只有一页,
* 永远只有一页,四个按钮恒为 disabled —— 看上去就是「分页是坏的」。 * 四个按钮恒为 disabled —— 看上去就是「分页是坏的」。
* 处置:默认值定为 20;**不在工具栏增加「每页显示」控件**(用户明确要求),
* ice_perpage 仅作为 URL 参数被尊重。
* *
* 本契约固化三件事: * 本契约固化四件事:
* - 页码先夹到 [1, total_pages],禁用状态由夹取后的页码推导; * - 页码先夹到 [1, total_pages],禁用状态由夹取后的页码推导;
* - 翻页链接基于显式基准 URL(page_url + 当前筛选参数),不再依赖 REQUEST_URI; * - 翻页链接基于显式基准 URL(page_url + 当前筛选参数),不再依赖 REQUEST_URI;
* - 列表页提供「每页显示」选择器(name=ice_perpage),并把筛选参数带进翻页链接。 * - 越界页码回退到最后一页并重查,不留空态死胡同;
* - 每页条数默认 20,且工具栏不得新增「每页显示」控件。
*/ */
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
@@ -39,8 +42,7 @@ const slice = (from, to) => {
}; };
const list = slice('private function render_list(', 'private function render_add_form('); const list = slice('private function render_list(', 'private function render_add_form(');
const args = slice('private function list_query_args(', 'private function per_page_selector('); const args = slice('private function list_query_args(', 'private function render_pagenavi(');
const perpage = slice('private function per_page_selector(', 'private function render_pagenavi(');
const pager = slice('private function render_pagenavi(', 'private function print_assets('); const pager = slice('private function render_pagenavi(', 'private function print_assets(');
test('翻页链接使用显式基准 URL,不再依赖 REQUEST_URI', () => { test('翻页链接使用显式基准 URL,不再依赖 REQUEST_URI', () => {
@@ -75,21 +77,24 @@ test('越界页码回退到最后一页,不再出现无分页条的死胡同',
assert.ok(list.indexOf('render_pagenavi(') > list.indexOf('未找到对应内容'), '分页渲染必须晚于空态处理'); assert.ok(list.indexOf('render_pagenavi(') > list.indexOf('未找到对应内容'), '分页渲染必须晚于空态处理');
}); });
test('每页条数由界面提供,且与筛选参数保持一致', () => { test('每页条数默认 20,工具栏不新增「每页显示」控件', () => {
assert.match(page, /public const DEFAULT_PER_PAGE = 30;/); assert.match(page, /public const DEFAULT_PER_PAGE = 20;/);
assert.match(list, /\(int\) \(\$_REQUEST\['ice_perpage'\] \?\? self::DEFAULT_PER_PAGE\)/); assert.match(list, /\(int\) \(\$_REQUEST\['ice_perpage'\] \?\? self::DEFAULT_PER_PAGE\)/);
assert.match(perpage, /method="get"/); // 明确约定:不在列表工具栏增加「每页显示」下拉/按钮。任何一处回来都视为回退。
assert.match(perpage, /name="ice_perpage"/); assert.doesNotMatch(page, /per_page_selector/);
assert.match(perpage, /name="page" value="' \. esc_attr\(self::PAGE_SLUG\)/); assert.doesNotMatch(page, /PER_PAGE_CHOICES/);
assert.match(perpage, /selected\(\$per_page, \$choice, false\)/); assert.doesNotMatch(page, /aksrt-perpage-form/);
assert.match(perpage, /onchange="this\.form\.submit\(\);"/); assert.doesNotMatch(page, /name="ice_perpage"/);
// 非预设值也要出现在下拉里,否则当前状态无法回显、一提交就被重置。 assert.doesNotMatch(page, /order-header-right/);
assert.match(perpage, /if \(!in_array\(\$per_page, \$choices, true\)\) \{/);
// 但 ice_perpage 仍作为 URL 参数被尊重(无界面入口,供 URL / 程序化调用)。
assert.match(args, /\$args\['ice_perpage'\] =/);
assert.match(list, /\$query_args = \$this->list_query_args\(/); assert.match(list, /\$query_args = \$this->list_query_args\(/);
assert.match(list, /per_page_selector\(\$per_page, \$query_args\)/);
assert.match(list, /render_pagenavi\(\$count_all, \$per_page, \$query_args\)/); assert.match(list, /render_pagenavi\(\$count_all, \$per_page, \$query_args\)/);
// 工具栏恢复成「过滤链接 + 搜索表单」两段式。
assert.match(list, /echo '<div class="order-header"><ul class="subsubsub">' \. \$but_html \. '<\/ul>' \. \$search \. '<\/div>';/);
}); });
test('筛选参数白名单化拼装,排序与每页条数随链接带走', () => { test('筛选参数白名单化拼装,排序与每页条数随链接带走', () => {