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 渲染售后面板确认四按钮标记与内联样式就位。
231 lines
11 KiB
JavaScript
231 lines
11 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* 支付中心列表页分页契约(四按钮式,统一到 AdminListPagination)。
|
||
*
|
||
* 背景(本次收敛的缺陷):
|
||
* 1) 各页面的翻页基准 URL 写成 add_query_arg('paged', '%#%') —— 只有两个参数时
|
||
* WordPress 会落到 $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态漂移,
|
||
* 列条件也容易丢。现在统一由 AdminListPagination 以 page 参数 + 当前筛选参数显式拼装。
|
||
* 2) 五个页面各复制一份几乎相同的 render_pagination(),其中「订单明细」与「提现审核」
|
||
* 的那份从未被调用(死代码),两页各自又内联了一份 paginate_links。
|
||
* 3) 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
|
||
* 且分页条不输出,只能手改 URL 退出。现在每页回退到最后一页重查一次。
|
||
* 4) 「售后服务」原本写死 limit => 100,完全没有分页。
|
||
*
|
||
* 本契约固化:
|
||
* - 七个页面全部改走 AdminListPagination::render(),不再有各自的翻页实现;
|
||
* - 渲染标记是卡密管理那一套:« ‹ 第 X 页,共 Y 页 › » 四枚按钮 + 「每页 X 共Y」计数,
|
||
* 没有数字页码链接;只要总数 > 0 就渲染,单页时四枚按钮全 disabled;
|
||
* - 每页条数默认 20 且来自共享常量,不新增「每页显示」控件;
|
||
* - 每页都在查询前夹取页码,越界时重查,杜绝「空表且没有分页条」的死胡同;
|
||
* - 翻页链接显式带上当前筛选参数,不依赖 REQUEST_URI。
|
||
*/
|
||
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
const test = require('node:test');
|
||
|
||
const root = path.resolve(__dirname, '..', '..');
|
||
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
||
|
||
const helper = read('src/Admin/Native/AdminListPagination.php');
|
||
const pages = {
|
||
orders: read('src/Admin/Native/PayOrders.php'),
|
||
products: read('src/Admin/Native/PayProducts.php'),
|
||
coupons: read('src/Admin/Native/PayCoupons.php'),
|
||
income: read('src/Admin/Native/PayIncome.php'),
|
||
withdrawals: read('src/Admin/Native/PayWithdrawals.php'),
|
||
chargeCards: read('src/Admin/Native/PayChargeCards.php'),
|
||
};
|
||
|
||
const labels = {
|
||
orders: '订单明细',
|
||
products: '付费文章',
|
||
coupons: '优惠码',
|
||
income: '创作分成',
|
||
withdrawals: '提现审核',
|
||
chargeCards: '卡密管理',
|
||
};
|
||
|
||
const count = (src, pattern) => (src.match(pattern) || []).length;
|
||
|
||
const emptyStates = {
|
||
orders: '暂无订单',
|
||
products: '暂无付费文章',
|
||
coupons: '暂无优惠码',
|
||
income: '暂无分成记录',
|
||
withdrawals: '暂无提现记录',
|
||
chargeCards: '未找到对应内容',
|
||
};
|
||
|
||
test('七个页面全部改走共享分页助手', () => {
|
||
for (const [key, src] of Object.entries(pages)) {
|
||
assert.ok(
|
||
src.includes('AdminListPagination::render('),
|
||
`${labels[key]} 未使用 AdminListPagination::render()`
|
||
);
|
||
}
|
||
|
||
// 订单明细 + 售后服务在同一个文件里,各有一处。
|
||
assert.equal(count(pages.orders, /AdminListPagination::render\(/g), 2, '订单明细与售后服务应各有一处');
|
||
for (const key of Object.keys(pages)) {
|
||
if (key === 'orders') continue;
|
||
assert.equal(count(pages[key], /AdminListPagination::render\(/g), 1, `${labels[key]} 只应有一处`);
|
||
}
|
||
});
|
||
|
||
test('不再有各页面自己的翻页实现,也没有 REQUEST_URI 基准 URL', () => {
|
||
// 只查六个页面:共享助手的 docblock 会引用这些反例,不能把它算进来。
|
||
for (const [key, src] of Object.entries(pages)) {
|
||
const label = labels[key];
|
||
assert.doesNotMatch(src, /private function render_pagenavi\(/, `${label} 仍保留私有 render_pagenavi()`);
|
||
assert.doesNotMatch(src, /private function render_pagination\(/, `${label} 仍保留私有 render_pagination()`);
|
||
assert.doesNotMatch(src, /public const DEFAULT_PER_PAGE/, `${label} 仍保留自己的每页条数常量`);
|
||
assert.doesNotMatch(src, /self::DEFAULT_PER_PAGE/, `${label} 仍引用自己的每页条数常量`);
|
||
|
||
// 两个参数的 add_query_arg('paged', ...) 会退回 $_SERVER['REQUEST_URI']。
|
||
assert.doesNotMatch(src, /add_query_arg\('paged',\s*'[^']*'\s*\)/, `${label} 仍以 REQUEST_URI 为基准`);
|
||
assert.doesNotMatch(src, /echo paginate_links\(/, `${label} 仍直接调用 paginate_links`);
|
||
}
|
||
});
|
||
|
||
test('每页条数默认 20 来自共享常量,且无「每页显示」控件', () => {
|
||
assert.match(helper, /public const DEFAULT_PER_PAGE = 20;/);
|
||
|
||
for (const [key, src] of Object.entries(pages)) {
|
||
assert.ok(
|
||
src.includes('AdminListPagination::DEFAULT_PER_PAGE'),
|
||
`${labels[key]} 未使用共享每页条数`
|
||
);
|
||
}
|
||
|
||
// 明确约定:任何页面都不新增「每页显示」下拉/按钮。
|
||
for (const [key, src] of Object.entries(pages)) {
|
||
assert.doesNotMatch(src, /per_page_selector/, `${labels[key]} 出现 per_page 选择器`);
|
||
assert.doesNotMatch(src, /PER_PAGE_CHOICES/, `${labels[key]} 出现 PER_PAGE_CHOICES`);
|
||
assert.doesNotMatch(src, /aksrt-perpage-form/, `${labels[key]} 出现每页条数表单`);
|
||
assert.doesNotMatch(src, /name="ice_perpage"/, `${labels[key]} 出现 ice_perpage 输入框`);
|
||
assert.doesNotMatch(src, /order-header-right/, `${labels[key]} 出现工具栏右侧每页控件`);
|
||
}
|
||
});
|
||
|
||
test('每页都在查询前夹取页码,越界时重查一次', () => {
|
||
// 六个页面统一按 $page_num / $pages 夹取;卡密管理沿用自己既有的 $paged / $max_page 回退。
|
||
for (const key of ['orders', 'products', 'coupons', 'income', 'withdrawals']) {
|
||
assert.match(pages[key], /if \(\$page_num > \$pages\) \{/, `${labels[key]} 缺少页码夹取`);
|
||
}
|
||
assert.match(
|
||
pages.chargeCards,
|
||
/if \(!\$items && \$count_all > 0 && \$paged > \$max_page\)/,
|
||
'卡密管理缺少越界回退'
|
||
);
|
||
|
||
const requeries = {
|
||
orders: [/\$items\s*=\s*\$repo->get_orders_admin\(/g, /\$items\s*=\s*\$service->admin_list\(/g],
|
||
products: [/\$query\s*=\s*\$this->query_paid_posts\(/g],
|
||
coupons: [/\$svc->get_list\(/g],
|
||
income: [/\$svc->get_list\(/g],
|
||
withdrawals: [/\$svc->get_all\(/g],
|
||
chargeCards: [/\$svc->get_list\(/g],
|
||
};
|
||
|
||
for (const [key, patterns] of Object.entries(requeries)) {
|
||
for (const pattern of patterns) {
|
||
assert.equal(count(pages[key], pattern), 2, `${labels[key]} 越界后必须重查(${pattern})`);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('售后服务改为真正的 limit/offset 分页', () => {
|
||
assert.doesNotMatch(pages.orders, /limit => 100/, '售后服务仍写死 limit => 100');
|
||
assert.match(pages.orders, /\$total = \$service->count\(\$filter_args\);/, '售后服务未用 AfterSaleService::count() 取总数');
|
||
|
||
// count() 不像 admin_list() 那样校验状态,查询前必须先归一,否则分页条会说谎。
|
||
assert.match(pages.orders, /if \(!in_array\(\$status, \$this->after_sale_statuses\(\), true\)\)/);
|
||
assert.match(pages.orders, /private function after_sale_statuses\(\): array \{/);
|
||
});
|
||
|
||
test('分页条晚于空态分支渲染', () => {
|
||
for (const [key, src] of Object.entries(pages)) {
|
||
const marker = emptyStates[key];
|
||
assert.ok(src.includes(marker), `${labels[key]} 找不到空态标记 ${marker}`);
|
||
|
||
if (key === 'orders') {
|
||
// 订单明细是第一处,售后服务是最后一处。
|
||
assert.ok(
|
||
src.indexOf('AdminListPagination::render(') > src.indexOf(marker),
|
||
'订单明细的分页条必须晚于空态分支'
|
||
);
|
||
assert.ok(
|
||
src.lastIndexOf('AdminListPagination::render(') > src.indexOf('暂无售后申请'),
|
||
'售后服务的分页条必须晚于空态分支'
|
||
);
|
||
continue;
|
||
}
|
||
|
||
assert.ok(
|
||
src.indexOf('AdminListPagination::render(') > src.indexOf(marker),
|
||
`${labels[key]} 的分页条必须晚于空态分支`
|
||
);
|
||
}
|
||
});
|
||
|
||
test('提现审核不把一次性动作参数带进翻页链接', () => {
|
||
// 只取 $query_args 数组本体:上下文的注释与行循环里的 $retry_id 用法都不算。
|
||
const start = pages.withdrawals.indexOf('$query_args = array_filter([');
|
||
assert.ok(start !== -1, '提现审核缺少 $query_args 白名单');
|
||
const slice = pages.withdrawals.slice(
|
||
start,
|
||
pages.withdrawals.indexOf('], static function', start)
|
||
);
|
||
assert.doesNotMatch(slice, /retry_id/, 'retry_id 不得进翻页链接');
|
||
assert.doesNotMatch(slice, /retry_key/, 'retry_key 不得进翻页链接');
|
||
for (const key of ['status', 's', 'user_id', 'date_from', 'date_to']) {
|
||
assert.match(slice, new RegExp(`'${key}'\\s*=>`), `缺少筛选参数 ${key}`);
|
||
}
|
||
});
|
||
|
||
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\);/);
|
||
|
||
// 基准 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*\)/);
|
||
|
||
// 卡密管理式四按钮标记。
|
||
assert.match(helper, /<div class="tablenav bottom">/);
|
||
assert.match(helper, /<div class="tablenav-pages">/);
|
||
assert.match(helper, /<span class="displaying-num">/);
|
||
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\(/);
|
||
});
|