Files
wordpress-theme-aksrt/src/Admin/Native/PayChargeCardsList.php
T
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

242 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* @package Jio
*
* PayChargeCards 列表渲染(过滤 / 搜索 / 排序 / 批量删除 / 分页)与翻页参数拼装。
* 作为 flat sibling trait 被 PayChargeCards 使用(PSR-4:trait 必须与宿主类同目录)。
*/
namespace Jio\Admin\Native;
defined('ABSPATH') || exit;
trait PayChargeCardsList {
/**
* @param string $s 搜索词(由页面头部统一解析后传入,用于提示、过滤链接与查询)。
*/
private function render_list(string $s): void {
$svc = \Jio\Payment\ChargeCard::get_instance();
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : '';
$other = isset($_GET['other']) ? sanitize_text_field((string) wp_unslash($_GET['other'])) : '';
$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));
// 每页条数默认值取自共享助手;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') {
$status = 'active';
} elseif (!in_array($status, ['used', 'expired', 'disabled'], true)) {
$status = '';
}
if (!in_array($type, ['balance', 'vip', 'credits', 'custom'], true)) {
$type = '';
}
// 过滤链接(标题右侧按钮与搜索提示由页面头部统一输出)。
$sub_but = [
['name' => __('全部', 'jio'), 'href' => $this->page_url],
];
if ($s === '') {
$sub_but[] = ['name' => __('未使用', 'jio'), 'href' => add_query_arg('status', '0', $this->page_url)];
$sub_but[] = ['name' => __('已使用', 'jio'), 'href' => add_query_arg('status', 'used', $this->page_url)];
foreach ($this->type_labels() as $_k => $_v) {
$sub_but[] = ['name' => $_v, 'href' => add_query_arg('type', $_k, $this->page_url)];
}
}
$but_html = '';
foreach ($sub_but as $but) {
$but_html .= '<li><a href="' . esc_url($but['href']) . '">' . esc_html($but['name']) . '</a></li> | ';
}
$but_html = substr($but_html, 0, -2);
$search = '<form class="form-inline form-order" method="post">
<div class="form-group" style="float: right;">
<input type="text" class="form-control" name="s" placeholder="' . esc_attr__('搜索卡密', 'jio') . '">
<button type="submit" class="button">' . esc_html__('提交', 'jio') . '</button>
</div>
</form>';
// 当前筛选参数(不含 paged)。分页链接显式带上它,不再依赖 add_query_arg()
// 默认取 $_SERVER['REQUEST_URI'](链接会随环境漂移,且列条件容易丢)。
$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>' . $search . '</div>';
$filters = [
'status' => $status,
'type' => $type,
'other' => $other,
'search' => $s,
'orderby' => $orderby,
'order' => $order,
];
$result = $svc->get_list($filters + ['limit' => $per_page, 'offset' => ($paged - 1) * $per_page]);
$items = (array) ($result['items'] ?? []);
$count_all = (int) ($result['total'] ?? 0);
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外,
// 表现为「空白表格 + 一个分页控件都没有」的死胡同,只能靠手改 URL 退出。
// 这里回退到最后一页重查一次,保证越界页码也能拿到数据与分页条。
$max_page = max(1, (int) ($result['max_pages'] ?? 1));
if (!$items && $count_all > 0 && $paged > $max_page) {
$paged = $max_page;
$result = $svc->get_list($filters + ['limit' => $per_page, 'offset' => ($paged - 1) * $per_page]);
$items = (array) ($result['items'] ?? []);
$count_all = (int) ($result['total'] ?? $count_all);
}
if (!$items) {
NativeUi::table_card_open();
echo '<table class="widefat fixed striped posts table table-bordered jio-cardpass-table">';
echo '<tbody>' . NativeUi::empty_row(1, esc_html__('未找到对应内容,或暂无内容', 'jio')) . '</tbody>';
echo '</table>';
NativeUi::table_wrap_close();
NativeUi::card_close();
return;
}
// 表头(与参考主题同列、同排序链接)。
$theads = [
['width' => '6%', 'orderby' => 'card', 'name' => __('卡号', 'jio')],
['width' => '10%', 'orderby' => 'password', 'name' => __('密码', 'jio')],
['width' => '5%', 'orderby' => 'type', 'name' => __('类型', 'jio')],
['width' => '5%', 'orderby' => 'create_time', 'name' => __('创建时间', 'jio')],
['width' => '5%', 'orderby' => 'modified_time', 'name' => __('更新时间', 'jio')],
['width' => '5%', 'orderby' => 'status', 'name' => __('状态', 'jio')],
['width' => '8%', 'orderby' => 'other', 'name' => __('标识', 'jio')],
];
$thead_th = '<td id="cb" class="manage-column column-cb check-column" style="width: 2%;"><label class="screen-reader-text" for="cb-select-all-1">' . esc_html__('全选', 'jio') . '</label><input id="cb-select-all-1" type="checkbox"></td>';
foreach ($theads as $thead) {
$orderby_links = '';
if ($thead['orderby']) {
$orderby_url = add_query_arg('orderby', $thead['orderby']);
$orderby_links .= '<a title="' . esc_attr__('降序', 'jio') . '" href="' . esc_url(add_query_arg('desc', 'ASC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-up"></span></a>';
$orderby_links .= '<a title="' . esc_attr__('升序', 'jio') . '" href="' . esc_url(add_query_arg('desc', 'DESC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-down"></span></a>';
$orderby_links = '<span class="orderby-but">' . $orderby_links . '</span>';
}
$thead_th .= '<th class="" width="' . esc_attr($thead['width']) . '">' . esc_html($thead['name']) . $orderby_links . '</th>';
}
$tbody = '';
foreach ($items as $msg) {
$meta = [];
if (!empty($msg['meta'])) {
$decoded = json_decode((string) $msg['meta'], true);
$meta = is_array($decoded) ? $decoded : [];
}
// 自定义卡密不展示状态;其余类型按状态着色(类型链接统一在下方链表中生成)。
$status_html = '';
if ($msg['type'] !== 'custom') {
$status_html = '<span style="color: #3d7ffd;">' . esc_html__('未使用', 'jio') . '</span>';
if ($msg['status'] === 'used') {
$status_html = '<span style="color: #f93b3b;">' . esc_html__('已使用', 'jio') . '</span>';
if (!empty($msg['order_number'])) {
$order_link_url = admin_url('admin.php?page=jio_orders&s=' . urlencode((string) $msg['order_number']));
$status_html .= '<a style="color: #ff6215;" target="_blank" href="' . esc_url($order_link_url) . '"> [' . esc_html__('查看', 'jio') . ']</a>';
}
} elseif ($msg['status'] === 'expired') {
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已过期', 'jio') . '</span>';
} elseif ($msg['status'] === 'disabled') {
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已禁用', 'jio') . '</span>';
}
}
$other_a = '';
if (!empty($msg['note'])) {
$other_a = '<a href="' . esc_url(add_query_arg('other', $msg['note'], $this->page_url)) . '">' . esc_html($msg['note']) . '</a>';
}
if ($msg['type'] === 'vip') {
$type_html = '<a style="color:#975106" href="' . esc_url(add_query_arg('type', 'vip', $this->page_url)) . '">' . esc_html__('会员兑换', 'jio') . '</a>';
$level = (int) ($meta['level'] ?? 1);
$type_html .= '<br>' . esc_html($this->vip_level_name($level)) . ':' . esc_html($this->vip_time_text($meta));
} elseif ($msg['type'] === 'credits') {
$type_html = '<a style="color:#289d0f" href="' . esc_url(add_query_arg('type', 'credits', $this->page_url)) . '">' . esc_html__('积分兑换', 'jio') . '</a>';
$type_html .= '<br>' . esc_html__('积分:', 'jio') . esc_html((string) (int) ($meta['points'] ?? 0));
} elseif ($msg['type'] === 'balance') {
$type_html = '<a style="color:#0c76e6" href="' . esc_url(add_query_arg('type', 'balance', $this->page_url)) . '">' . esc_html__('余额充值', 'jio') . '</a>';
$type_html .= '<br>' . esc_html__('金额:', 'jio') . esc_html($this->balance_amount_text($meta));
} else {
$type_html = '<a style="color:#a42ade" href="' . esc_url(add_query_arg('type', 'custom', $this->page_url)) . '">' . esc_html__('自定义卡密', 'jio') . '</a>';
}
$card_name = (string) ($meta['custom_name']['card_name'] ?? '');
$password_name = (string) ($meta['custom_name']['password_name'] ?? '');
$tbody .= '<tr>';
$tbody .= '<th scope="row" class="check-column"><label class="screen-reader-text" for="cb-select-' . (int) $msg['id'] . '">' . esc_html__('选择', 'jio') . '</label>
<input id="cb-select-' . (int) $msg['id'] . '" type="checkbox" name="action_id[]" value="' . (int) $msg['id'] . '">
</th>';
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['card']) . "' data-clipboard-tag=\"" . esc_attr($card_name !== '' ? $card_name : __('卡号', 'jio')) . "\">" . esc_html($card_name) . ' ' . esc_html((string) $msg['card']) . '</span></td>';
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['password']) . "' data-clipboard-tag=\"" . esc_attr($password_name !== '' ? $password_name : __('密码', 'jio')) . "\">" . esc_html($password_name) . ' ' . esc_html((string) $msg['password']) . '</span></td>';
$tbody .= "<td>$type_html</td>";
$tbody .= '<td>' . esc_html((string) $msg['created_at']) . '</td>';
$tbody .= '<td>' . esc_html((string) ($msg['used_at'] ?: $msg['created_at'])) . '</td>';
$tbody .= "<td>$status_html</td>";
$tbody .= "<td>$other_a</td>";
$tbody .= '</tr>';
}
echo '<form class="jio-cardpass-list-form" method="post">';
wp_nonce_field(self::NONCE);
echo '<div class="bulkactions jio-native-bulk jio-cardpass-bulk">
<label for="bulk-action-selector-top" class="screen-reader-text">' . esc_html__('选择批量操作', 'jio') . '</label><select name="action" id="bulk-action-selector-top">
<option value="-1">' . esc_html__('批量操作', 'jio') . '</option>
<option value="delete">' . esc_html__('删除', 'jio') . '</option>
</select>
<input type="submit" class="button action" value="' . esc_attr__('应用', 'jio') . '">
</div>';
NativeUi::table_card_open();
echo '<table class="widefat fixed striped posts table table-bordered jio-cardpass-table">';
echo '<thead><tr>' . $thead_th . '</tr></thead>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
echo '<tbody>' . $tbody . '</tbody>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
echo '</table>';
NativeUi::table_wrap_close();
AdminListPagination::render(self::PAGE_SLUG, $count_all, $paged, $per_page, $query_args);
NativeUi::card_close();
echo '</form>';
}
/**
* 列表筛选参数(不含 paged / page),用于构造翻页链接与「每页显示」表单。
* 显式列条件而不是依赖 add_query_arg() 默认取 $_SERVER['REQUEST_URI']:
* 后者把链接绑到当前请求上,一旦请求形态变化(POST、代理改写)就会丢条件。
*
* @return array<string,string>
*/
private function list_query_args(string $status, string $type, string $other, string $s, string $orderby, string $order, int $per_page): array {
$args = [];
if ($status !== '') {
$args['status'] = $status;
}
if ($type !== '') {
$args['type'] = $type;
}
if ($other !== '') {
$args['other'] = $other;
}
if ($s !== '') {
$args['s'] = $s;
}
if ($orderby !== '' && $orderby !== 'modified_time') {
$args['orderby'] = $orderby;
}
if ($order === 'ASC') {
$args['desc'] = 'ASC';
}
if ($per_page !== AdminListPagination::DEFAULT_PER_PAGE) {
$args['ice_perpage'] = (string) $per_page;
}
return $args;
}
}