Merge pull request 'feat(admin): 支付中心列表页统一为 WP 原生分页' (#38) from worktree/pay-pages-pagination into main

Reviewed-on: #38
This commit was merged in pull request #38.
This commit is contained in:
2026-10-08 14:23:19 +00:00
10 changed files with 516 additions and 242 deletions
+74
View File
@@ -0,0 +1,74 @@
<?php
/**
* @package AKSRT
*
* 后台列表页分页条(WP 原生 paginate_links)。
*
* 支付中心各页面(订单明细 / 售后服务 / 付费文章 / 优惠码 / 创作分成 / 提现审核 / 卡密管理)
* 原先各写一份几乎相同的 paginate_links 调用,且基准 URL 都写成
* add_query_arg('paged', '%#%') —— 只有两个参数时 WordPress 会退到
* $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态漂移,列条件也容易丢。
* 这里统一到一处:显式以「page 参数 + 当前筛选参数」为基准拼装,页码先夹到有效范围。
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
class AdminListPagination {
/** 每页条数默认值。所有列表页一致,且不提供「每页显示」控件。 */
public const DEFAULT_PER_PAGE = 20;
/**
* 渲染分页条。
*
* 页码夹取到 [1, total_pages] 再决定渲染与否:历史写法在此处会留下「第 2 页 / 共 1 页」
* 这类越界状态。真正的数据回退(数据也要跟着落到最后一页)由各页面在查询前完成,
* 这里只负责把显示状态收敛到自洽。
*
* @param string $page_slug 当前 admin.php 的 page 参数。
* @param int $total_count 当前筛选条件下的记录总数。
* @param int $current_page 当前页码(未经夹取亦可)。
* @param int $per_page 每页条数。
* @param array<string, string|int> $query_args 当前筛选参数,不含 paged。
*/
public static function render(
string $page_slug,
int $total_count,
int $current_page,
int $per_page,
array $query_args = []
): void {
if ($total_count <= 0 || $per_page <= 0) {
return;
}
$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);
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>';
}
}
+4 -55
View File
@@ -21,9 +21,6 @@ class PayChargeCards {
public const NONCE = 'aksrt_card_pass_page';
public const EXPORT_AJAX_ACTION = 'aksrt_card_pass_export';
/** 每页条数默认值;列表页可由 ice_perpage 覆盖(无界面控件,仅供 URL / 程序化调用)。 */
public const DEFAULT_PER_PAGE = 20;
private static $instance = null;
/** @var string 当前页面 URL */
@@ -119,7 +116,8 @@ class PayChargeCards {
$orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : 'modified_time';
$order = isset($_GET['desc']) ? (strtoupper((string) wp_unslash($_GET['desc'])) === 'ASC' ? 'ASC' : 'DESC') : 'DESC';
$paged = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = max(1, min(100, (int) ($_REQUEST['ice_perpage'] ?? self::DEFAULT_PER_PAGE)));
// 每页条数默认值取自共享助手;ice_perpage 仅作为 URL 参数被尊重,无界面控件。
$per_page = max(1, min(100, (int) ($_REQUEST['ice_perpage'] ?? AdminListPagination::DEFAULT_PER_PAGE)));
// 参考主题:status=0 表示未使用;本主题规范状态为 active。
if ($status === '0' || $status === 'active') {
@@ -297,7 +295,7 @@ class PayChargeCards {
echo '</form>';
echo '<div class="clear"></div>';
$this->render_pagenavi($count_all, $per_page, $query_args);
AdminListPagination::render(self::PAGE_SLUG, $count_all, $paged, $per_page, $query_args);
}
/* ------------------------------------------------------------------
@@ -1059,67 +1057,18 @@ class PayChargeCards {
if ($order === 'ASC') {
$args['desc'] = 'ASC';
}
if ($per_page !== self::DEFAULT_PER_PAGE) {
if ($per_page !== AdminListPagination::DEFAULT_PER_PAGE) {
$args['ice_perpage'] = (string) $per_page;
}
return $args;
}
/**
* 分页(参考主题 zibpay_admin_pagenavi 同款结构与禁用逻辑)。
*
* 与参考实现的两处差异,均为缺陷修复:
* 1) 页码先夹到 [1, total_pages]。历史写法在此处会出现「第 2 页 / 共 1 页」这类越界状态,
* 四个分支一个都不命中,上一页/下一页同时停在 '#',按钮既不禁用也点不动。
* 2) 链接基于显式基准 URL(page_url + 当前筛选条件),不再依赖 REQUEST_URI。
*
* @param array<string,string> $query_args 当前筛选参数(不含 paged)。
*/
private function render_pagenavi(int $total_count, int $number_per_page, array $query_args = []): void {
if ($total_count <= 0 || $number_per_page <= 0) {
return;
}
$total_pages = max(1, (int) ceil($total_count / $number_per_page));
$current_page = min(max(1, (int) ($_GET['paged'] ?? 1)), $total_pages);
$base = $query_args ? add_query_arg($query_args, $this->page_url) : $this->page_url;
$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);
?>
<div class="tablenav bottom">
<div class="tablenav-pages">
<span class="displaying-num"><?php printf(esc_html__('每页 %1$s 共%2$s', 'aksrt'), esc_html((string) $number_per_page), esc_html((string) $total_count)); ?></span>
<span class="pagination-links">
<a class="first-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往第一页', 'aksrt'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
<a class="prev-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往上一页', '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 esc_attr_e('前往下一页', 'aksrt'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
<a class="last-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往最后一页', 'aksrt'); ?>" href="<?php echo esc_url($last_page_url); ?>">»</a>
</span>
</div>
</div>
<?php
}
/** 行内样式与脚本:依赖显隐 + 点击复制。 */
private function print_assets(): void {
?>
<style>
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
.order-header .subsubsub { margin: 6px 0; }
/* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部;
这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */
.tablenav-pages a.disabled { pointer-events: none; }
/* 排序箭头对齐:宿主做成零尺寸 inline-block —— 空 inline-block 的基线即其下边缘,
会与文字基线对齐,于是箭头可用 bottom 精确锚定到「基线以上 N em」。
箭头盒中心等于 bottom 值(translateY(50%) 抵消自身高度,与盒子高度无关):
+25 -19
View File
@@ -171,7 +171,7 @@ class PayCoupons {
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
$type_labels = [
'fixed' => __('固定金额', 'aksrt'),
'percent' => __('折扣百分比', 'aksrt'),
@@ -199,7 +199,21 @@ class PayCoupons {
'offset' => ($page_num - 1) * $per_page,
]);
$items = $result['items'] ?? [];
$pages = max(1, (int) ($result['total_pages'] ?? 1));
$total = (int) ($result['total'] ?? 0);
$pages = max(1, (int) ceil($total / $per_page));
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
if ($page_num > $pages) {
$page_num = $pages;
$result = $svc->get_list([
'status' => $status,
'search' => $search,
'limit' => $per_page,
'offset' => ($page_num - 1) * $per_page,
]);
$items = $result['items'] ?? [];
}
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>ID</th><th>' . esc_html__('代码', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('面值', 'aksrt') . '</th><th>' . esc_html__('门槛/封顶', 'aksrt') . '</th><th>' . esc_html__('范围', 'aksrt') . '</th><th>' . esc_html__('已用/上限/每人', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('过期', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
@@ -240,8 +254,16 @@ class PayCoupons {
}
echo '</td></tr>';
}
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
$query_args = array_filter([
'status' => $status !== '' ? $status : null,
's' => $search !== '' ? $search : null,
], static function ($value): bool {
return $value !== null;
});
echo '</tbody></table>';
$this->render_pagination($pages, $page_num);
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
}
public function handle_create_coupon(): void {
@@ -457,20 +479,4 @@ class PayCoupons {
private function page_url(array $args = []): string {
return add_query_arg(array_merge(['page' => self::PAGE_SLUG], $args), admin_url('admin.php'));
}
private function render_pagination(int $pages, int $page_num): void {
if ($pages <= 1) {
return;
}
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
}
+27 -18
View File
@@ -51,7 +51,7 @@ class PayIncome {
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
$order_statuses = class_exists('\\AKSRT\\Payment\\Order')
? \AKSRT\Payment\Order::get_valid_statuses()
: [];
@@ -124,7 +124,31 @@ class PayIncome {
$result = $svc->get_list($list_args);
$items = $result['items'] ?? [];
$total = (int) ($result['total'] ?? 0);
$pages = max(1, (int) ($result['max_pages'] ?? 1));
$pages = max(1, (int) ceil($total / $per_page));
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
if ($page_num > $pages) {
$page_num = $pages;
$list_args['offset'] = ($page_num - 1) * $per_page;
$result = $svc->get_list($list_args);
$items = $result['items'] ?? [];
}
// 翻页链接显式带上当前筛选条件(不含 paged)。必须在下面的行循环之前取好:
// 循环里 $user_id 会被每行的 author_id 覆盖。
$query_args = array_filter([
'status' => $status !== '' ? $status : null,
'order_status' => $order_status !== '' ? $order_status : null,
'user_id' => $user_id > 0 ? $user_id : null,
'post_id' => $post_id > 0 ? $post_id : null,
'currency' => $currency !== '' ? $currency : null,
'order_number' => $order_number !== '' ? $order_number : null,
'date_from' => $date_from !== '' ? $date_from : null,
'date_to' => $date_to !== '' ? $date_to : null,
], static function ($value): bool {
return $value !== null;
});
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th><input type="checkbox" form="aksrt-pay-batch-settle-income" aria-label="' . esc_attr__('全选待结算记录', 'aksrt') . '" /></th><th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
@@ -164,7 +188,7 @@ class PayIncome {
echo '</td></tr>';
}
echo '</tbody></table>';
$this->render_pagination($pages, $page_num);
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
$this->render_footer();
}
@@ -256,19 +280,4 @@ class PayIncome {
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
}
private function render_pagination(int $pages, int $page_num): void {
if ($pages <= 1) {
return;
}
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
}
+76 -50
View File
@@ -77,7 +77,7 @@ class PayOrders {
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
if (!array_key_exists($product_type, $type_tabs)) {
$product_type = '';
@@ -118,20 +118,38 @@ class PayOrders {
? \AKSRT\Core\DomainModel::order_status_labels()
: [];
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
if ($page_num > $pages) {
$page_num = $pages;
$args['offset'] = ($page_num - 1) * $per_page;
$items = $repo->get_orders_admin($args, $search);
}
// 当前筛选参数(不含 paged):标签页链接与翻页链接共用同一份,避免两处漂移。
$query_args = array_filter([
'product_type' => $product_type !== '' ? $product_type : null,
'product_id' => $product_id > 0 ? $product_id : null,
'user_id' => $user_id > 0 ? $user_id : null,
'payment_method' => $pay_method !== '' ? $pay_method : null,
'status' => ($status !== '' && $status !== 'all') ? $status : null,
'date_from' => $date_from !== '' ? $date_from : null,
'date_to' => $date_to !== '' ? $date_to : null,
's' => $search !== '' ? $search : null,
], static function ($value): bool {
return $value !== null;
});
// Type tabs.
echo '<ul class="subsubsub" style="float:none;margin:8px 0 12px;">';
$tab_bits = [];
foreach ($type_tabs as $tkey => $tlabel) {
$url = $this->page_url('aksrt_orders', array_filter([
'product_type' => $tkey !== '' ? $tkey : null,
'product_id' => $product_id > 0 ? $product_id : null,
'user_id' => $user_id > 0 ? $user_id : null,
'payment_method' => $pay_method !== '' ? $pay_method : null,
'status' => ($status !== '' && $status !== 'all') ? $status : null,
'date_from' => $date_from !== '' ? $date_from : null,
'date_to' => $date_to !== '' ? $date_to : null,
's' => $search !== '' ? $search : null,
]));
$url = $this->page_url('aksrt_orders', array_filter(
['product_type' => $tkey !== '' ? $tkey : null] + $query_args,
static function ($value): bool {
return $value !== null;
}
));
$cls = ($product_type === $tkey) ? 'current' : '';
$tab_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($tlabel) . '</a></li>';
}
@@ -142,16 +160,12 @@ class PayOrders {
echo '<ul class="subsubsub" style="float:none;margin:0 0 12px;">';
$m_bits = [];
foreach ($method_tabs as $mkey => $mlabel) {
$url = $this->page_url('aksrt_orders', array_filter([
'product_type' => $product_type !== '' ? $product_type : null,
'product_id' => $product_id > 0 ? $product_id : null,
'user_id' => $user_id > 0 ? $user_id : null,
'payment_method' => $mkey !== '' ? $mkey : null,
'status' => ($status !== '' && $status !== 'all') ? $status : null,
'date_from' => $date_from !== '' ? $date_from : null,
'date_to' => $date_to !== '' ? $date_to : null,
's' => $search !== '' ? $search : null,
]));
$url = $this->page_url('aksrt_orders', array_filter(
['payment_method' => $mkey !== '' ? $mkey : null] + $query_args,
static function ($value): bool {
return $value !== null;
}
));
$cls = ($pay_method === $mkey) ? 'current' : '';
$m_bits[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($mlabel) . '</a></li>';
}
@@ -279,18 +293,7 @@ class PayOrders {
echo '</tbody></table>';
if ($pages > 1) {
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
$this->render_footer();
}
@@ -400,12 +403,30 @@ class PayOrders {
private function render_after_sales_page(): void {
$service = \AKSRT\Payment\AfterSaleService::get_instance();
$status = isset($_GET['after_sale_status']) ? sanitize_key((string) wp_unslash($_GET['after_sale_status'])) : '';
// admin_list() 会按状态常量白名单过滤,count() 不会。这里先归一一次,
// 否则手改 URL 传个野状态会变成「count 为 0 而 list 返回全部」,分页条直接说谎。
if (!in_array($status, $this->after_sale_statuses(), true)) {
$status = '';
}
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
$date_from = preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_from) ? $date_from : '';
$date_to = preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_to) ? $date_to : '';
$filter_args = ['status' => $status, 'date_from' => $date_from, 'date_to' => $date_to];
$items = $service->admin_list($filter_args + ['limit' => 100]);
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
$items = $service->admin_list($filter_args + ['limit' => $per_page, 'offset' => ($page_num - 1) * $per_page]);
$total = $service->count($filter_args);
$pages = max(1, (int) ceil($total / $per_page));
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
if ($page_num > $pages) {
$page_num = $pages;
$items = $service->admin_list($filter_args + ['limit' => $per_page, 'offset' => ($page_num - 1) * $per_page]);
}
echo '<form method="get"><input type="hidden" name="page" value="' . esc_attr(self::PAGE_AFTER_SALES_SLUG) . '" />';
echo '<select name="after_sale_status"><option value="">' . esc_html__('全部售后状态', 'aksrt') . '</option>';
foreach ([\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消'] as $key => $label) echo '<option value="' . esc_attr($key) . '"' . selected($status, $key, false) . '>' . esc_html($label) . '</option>';
@@ -535,6 +556,16 @@ class PayOrders {
echo '</div></td></tr>';
}
echo '</tbody></table>';
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
$query_args = array_filter([
'after_sale_status' => $status !== '' ? $status : null,
'date_from' => $date_from !== '' ? $date_from : null,
'date_to' => $date_to !== '' ? $date_to : null,
], static function ($value): bool {
return $value !== null;
});
AdminListPagination::render(self::PAGE_AFTER_SALES_SLUG, $total, $page_num, $per_page, $query_args);
// 事件委托:点击「操作」切换手风琴详情;「取消」仅收起面板;「确认」以 POST 提交选中的售后状态
echo '<script>(function(){'
. 'var postUrl=' . wp_json_encode(esc_url_raw(admin_url('admin-post.php'))) . ';'
@@ -574,6 +605,17 @@ class PayOrders {
. '})();</script>';
}
/** 售后状态白名单:admin_list() 据此过滤,count() 不校验,故查询前先按它归一。 */
private function after_sale_statuses(): array {
return [
\AKSRT\Payment\AfterSaleService::STATUS_PENDING,
\AKSRT\Payment\AfterSaleService::STATUS_PROCESSING,
\AKSRT\Payment\AfterSaleService::STATUS_COMPLETED,
\AKSRT\Payment\AfterSaleService::STATUS_REJECTED,
\AKSRT\Payment\AfterSaleService::STATUS_CANCELLED,
];
}
public function handle_after_sale_status(): void {
if (!current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
$id = isset($_POST['after_sale_id']) ? absint($_POST['after_sale_id']) : 0;
@@ -859,20 +901,4 @@ class PayOrders {
private function page_url(string $slug, array $args = []): string {
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
}
private function render_pagination(int $pages, int $page_num): void {
if ($pages <= 1) {
return;
}
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
}
+24 -19
View File
@@ -38,7 +38,7 @@ class PayProducts {
$pay_type = isset($_GET['pay_type']) ? sanitize_key((string) wp_unslash($_GET['pay_type'])) : '';
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
$type_labels = \AKSRT\Core\DomainModel::pay_type_labels();
unset($type_labels[\AKSRT\Core\DomainModel::PAY_TYPE_NO]);
@@ -51,9 +51,22 @@ class PayProducts {
]);
$posts = $query['posts'];
$total = $query['total'];
$total = (int) $query['total'];
$pages = max(1, (int) ceil($total / $per_page));
// 页码越界(手工改 URL、或筛选后总数变小)会让 WP_Query 停在空页上:表格空掉
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
if ($page_num > $pages) {
$page_num = $pages;
$query = $this->query_paid_posts([
'pay_type' => $pay_type,
'search' => $search,
'page' => $page_num,
'per_page' => $per_page,
]);
$posts = $query['posts'];
}
$sales_map = [];
if ($posts && class_exists('\AKSRT\Payment\OrderRepository')) {
$ids = array_map(static function ($p) {
@@ -146,8 +159,16 @@ class PayProducts {
echo '</tr>';
}
// 翻页链接显式带上当前筛选条件(不含 paged),口径与 render_filters() 的表单一致。
$query_args = array_filter([
'pay_type' => $pay_type !== '' ? $pay_type : null,
's' => $search !== '' ? $search : null,
], static function ($value): bool {
return $value !== null;
});
echo '</tbody></table>';
$this->render_pagination($pages, $page_num);
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
$this->render_footer();
}
@@ -352,20 +373,4 @@ class PayProducts {
private function page_url(string $slug, array $args = []): string {
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
}
private function render_pagination(int $pages, int $page_num): void {
if ($pages <= 1) {
return;
}
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
}
+29 -28
View File
@@ -44,7 +44,7 @@ class PayWithdrawals {
$date_from = isset($_GET['date_from']) ? $this->sanitize_date((string) wp_unslash($_GET['date_from'])) : '';
$date_to = isset($_GET['date_to']) ? $this->sanitize_date((string) wp_unslash($_GET['date_to'])) : '';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$per_page = AdminListPagination::DEFAULT_PER_PAGE;
$svc = \AKSRT\Payment\Withdrawal::get_instance();
$stats = $svc->get_stats();
$retry_id = isset($_GET['retry_id']) ? max(0, (int) $_GET['retry_id']) : 0;
@@ -111,6 +111,33 @@ class PayWithdrawals {
]);
$pages = max(1, (int) ceil($total / $per_page));
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉
// 且分页条不输出,只能手改 URL 退出。回退到最后一页重查一次。
if ($page_num > $pages) {
$page_num = $pages;
$items = $svc->get_all([
'status' => $status,
'keyword' => $keyword,
'user_id' => $user_filter,
'date_from' => $date_from,
'date_to' => $date_to,
'limit' => $per_page,
'offset' => ($page_num - 1) * $per_page,
]);
}
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
// retry_id / retry_key 是一次性动作参数,绝不能进翻页链接。
$query_args = array_filter([
'status' => $status !== '' ? $status : null,
's' => $keyword !== '' ? $keyword : null,
'user_id' => $user_filter > 0 ? $user_filter : null,
'date_from' => $date_from !== '' ? $date_from : null,
'date_to' => $date_to !== '' ? $date_to : null,
], static function ($value): bool {
return $value !== null;
});
echo '<table class="wp-list-table widefat fixed striped">';
echo '<thead><tr>';
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
@@ -184,18 +211,7 @@ class PayWithdrawals {
echo '</tbody></table>';
if ($pages > 1) {
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args);
$this->render_footer();
}
@@ -278,19 +294,4 @@ class PayWithdrawals {
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1 ? $date : '';
}
private function render_pagination(int $pages, int $page_num): void {
if ($pages <= 1) {
return;
}
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
}
+43 -52
View File
@@ -3,7 +3,7 @@
/**
* 「卡密管理」分页契约。
*
* 背景(已修复的缺陷):
* 背景(两次演进):
* 1) 翻页链接此前写作 add_query_arg('paged', N) —— 只有两个参数时 WordPress 会落到
* $_SERVER['REQUEST_URI'],链接绑在当前请求上,随请求形态漂移,列条件也容易丢。
* 2) render_pagenavi() 的 URL 推导是一串 if/elseif。页码越界时(例如 per_page 缩小后
@@ -12,14 +12,12 @@
* 极端情况下 offset 落到数据之外,表格直接变空态并且连分页条都不输出,只能手改 URL 退出。
* 3) ice_perpage 只在服务端被读取、默认每页条数偏大。条数小于默认值时永远只有一页,
* 四个按钮恒为 disabled —— 看上去就是「分页是坏的」。
* 处置:默认值定为 20;**不在工具栏增加「每页显示」控件**(用户明确要求),
* ice_perpage 仅作为 URL 参数被尊重。
*
* 本契约固化四件事:
* - 页码先夹到 [1, total_pages],禁用状态由夹取后的页码推导;
* - 翻页链接基于显式基准 URL(page_url + 当前筛选参数),不再依赖 REQUEST_URI;
* 现状:渲染已迁移到共享助手 AdminListPagination(WP 原生 paginate_links),
* 基准 URL 与页码夹取都在那里做。本文件只固化卡密管理仍特有的部分:
* - ice_perpage 仍作为 URL 参数被尊重,且工具栏不新增「每页显示」控件;
* - 越界页码回退到最后一页并重查,不留空态死胡同;
* - 每页条数默认 20,且工具栏不得新增「每页显示」控件。
* - 筛选参数白名单化拼装,排序与每页条数随链接带走。
*/
const assert = require('node:assert/strict');
@@ -42,61 +40,58 @@ const slice = (from, to) => {
};
const list = slice('private function render_list(', 'private function render_add_form(');
const args = slice('private function list_query_args(', 'private function render_pagenavi(');
const pager = slice('private function render_pagenavi(', 'private function print_assets(');
const args = slice('private function list_query_args(', 'private function print_assets(');
test('翻页链接使用显式基准 URL,不再依赖 REQUEST_URI', () => {
// 反面写法:只有两个参数的 add_query_arg —— 会退回到 $_SERVER['REQUEST_URI']。
assert.doesNotMatch(page, /add_query_arg\('paged',\s*[^,)]+\)/);
test('渲染已迁移到共享助手 AdminListPagination', () => {
assert.match(
list,
/AdminListPagination::render\(self::PAGE_SLUG, \$count_all, \$paged, \$per_page, \$query_args\);/
);
assert.match(pager, /\$base\s*=\s*\$query_args\s*\?\s*add_query_arg\(\$query_args,\s*\$this->page_url\)\s*:\s*\$this->page_url;/);
assert.match(pager, /add_query_arg\('paged',\s*\$page,\s*\$base\)/);
assert.equal(count(pager, /add_query_arg\('paged'/g), 1, '翻页 URL 只应有一个构造点');
// 自定义标记与专属常量都应清空。
assert.doesNotMatch(page, /render_pagenavi/);
assert.doesNotMatch(page, /first-page button|prev-page button|next-page button|last-page button/);
assert.doesNotMatch(page, /public const DEFAULT_PER_PAGE/);
assert.doesNotMatch(page, /self::DEFAULT_PER_PAGE/);
assert.match(page, /AdminListPagination::DEFAULT_PER_PAGE/);
// 只服务自定义禁用按钮的 CSS 规则一并删除。
assert.doesNotMatch(page, /\.tablenav-pages a\.disabled \{ pointer-events: none; \}/);
});
test('页码夹取到有效范围,禁用状态由夹取后的页码推导', () => {
assert.match(pager, /\$total_pages\s*=\s*max\(1,\s*\(int\)\s*ceil\(\$total_count \/ \$number_per_page\)\)/);
assert.match(pager, /\$current_page\s*=\s*min\(max\(1,\s*\(int\)\s*\(\$_GET\['paged'\] \?\? 1\)\),\s*\$total_pages\)/);
test('ice_perpage 仍作为 URL 参数被尊重,默认每页 20', () => {
assert.match(
list,
/\(int\) \(\$_REQUEST\['ice_perpage'\] \?\? AdminListPagination::DEFAULT_PER_PAGE\)/
);
// 越界时 if/elseif 全不命中的分支结构不得回归。
assert.match(pager, /\$on_first\s*=\s*\$current_page <= 1;/);
assert.match(pager, /\$on_last\s*=\s*\$current_page >= \$total_pages;/);
assert.equal(count(pager, /\$on_first \? '#' :/g), 2, '第一页/上一页都要按 on_first 落到 #');
assert.equal(count(pager, /\$on_last \? '#' :/g), 2, '下一页/最后一页都要按 on_last 落到 #');
assert.doesNotMatch(pager, /\$current_page == \$total_pages\b/);
assert.doesNotMatch(pager, /\$prev_page_url\s*=\s*'#';/);
});
test('越界页码回退到最后一页,不再出现无分页条的死胡同', () => {
assert.match(list, /\$max_page\s*=\s*max\(1,\s*\(int\)\s*\(\$result\['max_pages'\] \?\? 1\)\)/);
assert.match(list, /if \(!\$items && \$count_all > 0 && \$paged > \$max_page\)/);
assert.match(list, /\$paged\s*=\s*\$max_page;/);
// 回退之后必须重查,否则表格依旧是空的。
assert.equal(count(list, /\$svc->get_list\(/g), 2);
// 分页条必须在空态分支之后渲染,否则空态 return 会把分页条一起吞掉。
assert.ok(list.indexOf('render_pagenavi(') > list.indexOf('未找到对应内容'), '分页渲染必须晚于空态处理');
});
test('每页条数默认 20,工具栏不新增「每页显示」控件', () => {
assert.match(page, /public const DEFAULT_PER_PAGE = 20;/);
assert.match(list, /\(int\) \(\$_REQUEST\['ice_perpage'\] \?\? self::DEFAULT_PER_PAGE\)/);
// 明确约定:不在列表工具栏增加「每页显示」下拉/按钮。任何一处回来都视为回退。
// 明确约定:不在列表工具栏增加「每页显示」下拉/按钮。
assert.doesNotMatch(page, /per_page_selector/);
assert.doesNotMatch(page, /PER_PAGE_CHOICES/);
assert.doesNotMatch(page, /aksrt-perpage-form/);
assert.doesNotMatch(page, /name="ice_perpage"/);
assert.doesNotMatch(page, /order-header-right/);
// 但 ice_perpage 仍作为 URL 参数被尊重(无界面入口,供 URL / 程序化调用)。
// 但 ice_perpage 仍随链接带走(无界面入口,供 URL / 程序化调用)。
assert.match(args, /\$args\['ice_perpage'\] =/);
assert.match(list, /\$query_args = \$this->list_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('越界页码回退到最后一页,不再出现无分页条的死胡同', () => {
assert.match(list, /\$max_page\s*=\s*max\(1, \(int\) \(\$result\['max_pages'\] \?\? 1\)\)/);
assert.match(list, /if \(!\$items && \$count_all > 0 && \$paged > \$max_page\)/);
assert.match(list, /\$paged\s*=\s*\$max_page;/);
// 回退之后必须重查,否则表格依旧是空的。
assert.equal(count(list, /\$svc->get_list\(/g), 2);
// 分页条必须在空态分支之后渲染,否则空态 return 会把分页条一起吞掉。
assert.ok(
list.indexOf('AdminListPagination::render(') > list.indexOf('未找到对应内容'),
'分页渲染必须晚于空态处理'
);
});
test('筛选参数白名单化拼装,排序与每页条数随链接带走', () => {
for (const key of ['status', 'type', 'other', 's', 'orderby', 'desc', 'ice_perpage']) {
assert.match(args, new RegExp(`\\$args\\['${key}'\\] =`), `缺少 ${key}`);
@@ -104,11 +99,7 @@ test('筛选参数白名单化拼装,排序与每页条数随链接带走', ()
// 默认值不进 URL,避免链接里塞满无意义参数。
assert.match(args, /\$orderby !== 'modified_time'/);
assert.match(args, /\$order === 'ASC'/);
assert.match(args, /\$per_page !== self::DEFAULT_PER_PAGE/);
// paged 由 render_pagenavi 单独追加,不能混进基准参数里(否则会被自己覆盖)。
assert.match(args, /\$per_page !== AdminListPagination::DEFAULT_PER_PAGE/);
// paged 由 AdminListPagination 单独追加,不能混进基准参数里(否则会被自己覆盖)。
assert.doesNotMatch(args, /\$args\['paged'\]/);
});
test('禁用的翻页按钮不可点击,避免点空还滚回页首', () => {
assert.match(page, /\.tablenav-pages a\.disabled \{ pointer-events: none; \}/);
});
+210
View File
@@ -0,0 +1,210 @@
'use strict';
/**
* 支付中心列表页分页契约(WP 原生 paginate_links,统一到 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(),不再有各自的翻页实现;
* - 每页条数默认 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;/);
// 空态与单页都不渲染。
assert.match(helper, /if \(\$total_count <= 0 \|\| \$per_page <= 0\) \{\s*return;\s*\}/);
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\);/);
// 原生 WP 分页 + 总数提示。
assert.match(helper, /<div class="tablenav"><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 自行转义。/);
});
+4 -1
View File
@@ -55,6 +55,7 @@ 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 ''; }
}
@@ -83,6 +84,7 @@ namespace AKSRT\Payment {
public static array $rows = [];
public static function get_instance(): self { return self::$i ?: self::$i = new self(); }
public function admin_list(array $args = []): array { return array_values(self::$rows); }
public function count(array $args = []): int { return count(self::$rows); }
public function get(int $id): ?array { return self::$rows[$id] ?? null; }
public function can_finalize_order_refund(int $after_sale_id, string $order_number): bool {
$row = self::$rows[$after_sale_id] ?? null;
@@ -197,7 +199,8 @@ namespace AKSRT\Settings {
}
namespace {
require 'C:/Users/Xing/Desktop/AKSRT/AKSRT-theme/src/Admin/Native/PayOrders.php';
require __DIR__ . '/../../src/Admin/Native/AdminListPagination.php';
require 'C:/Users/Xing/Desktop/AKSRT/AKSRT-theme/src/Admin/Native/PayOrders.php';
$scenario = $argv[1] ?? '';