Add NativeUi helper + native-ui.css (tokens mirror the SPA Naive UI theme) and move the pay center, orders, products, coupons, charge cards, income, withdrawals, tickets, invite codes, VIP, verify, messages and report/ban pages onto shared header, stat cards, cards, filters, badges and empty states. Remove inline styles and dead helpers; split PayChargeCards into sibling traits to stay under the file size limit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
242 lines
14 KiB
PHP
242 lines
14 KiB
PHP
<?php
|
||
/**
|
||
* @package AKSRT
|
||
*
|
||
* PayChargeCards 列表渲染(过滤 / 搜索 / 排序 / 批量删除 / 分页)与翻页参数拼装。
|
||
* 作为 flat sibling trait 被 PayChargeCards 使用(PSR-4:trait 必须与宿主类同目录)。
|
||
*/
|
||
|
||
namespace AKSRT\Admin\Native;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
trait PayChargeCardsList {
|
||
|
||
/**
|
||
* @param string $s 搜索词(由页面头部统一解析后传入,用于提示、过滤链接与查询)。
|
||
*/
|
||
private function render_list(string $s): void {
|
||
$svc = \AKSRT\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' => __('全部', 'aksrt'), 'href' => $this->page_url],
|
||
];
|
||
if ($s === '') {
|
||
$sub_but[] = ['name' => __('未使用', 'aksrt'), 'href' => add_query_arg('status', '0', $this->page_url)];
|
||
$sub_but[] = ['name' => __('已使用', 'aksrt'), '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__('搜索卡密', 'aksrt') . '">
|
||
<button type="submit" class="button">' . esc_html__('提交', 'aksrt') . '</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 aksrt-cardpass-table">';
|
||
echo '<tbody>' . NativeUi::empty_row(1, esc_html__('未找到对应内容,或暂无内容', 'aksrt')) . '</tbody>';
|
||
echo '</table>';
|
||
NativeUi::table_wrap_close();
|
||
NativeUi::card_close();
|
||
return;
|
||
}
|
||
|
||
// 表头(与参考主题同列、同排序链接)。
|
||
$theads = [
|
||
['width' => '6%', 'orderby' => 'card', 'name' => __('卡号', 'aksrt')],
|
||
['width' => '10%', 'orderby' => 'password', 'name' => __('密码', 'aksrt')],
|
||
['width' => '5%', 'orderby' => 'type', 'name' => __('类型', 'aksrt')],
|
||
['width' => '5%', 'orderby' => 'create_time', 'name' => __('创建时间', 'aksrt')],
|
||
['width' => '5%', 'orderby' => 'modified_time', 'name' => __('更新时间', 'aksrt')],
|
||
['width' => '5%', 'orderby' => 'status', 'name' => __('状态', 'aksrt')],
|
||
['width' => '8%', 'orderby' => 'other', 'name' => __('标识', 'aksrt')],
|
||
];
|
||
|
||
$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__('全选', 'aksrt') . '</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__('降序', 'aksrt') . '" href="' . esc_url(add_query_arg('desc', 'ASC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-up"></span></a>';
|
||
$orderby_links .= '<a title="' . esc_attr__('升序', 'aksrt') . '" 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__('未使用', 'aksrt') . '</span>';
|
||
if ($msg['status'] === 'used') {
|
||
$status_html = '<span style="color: #f93b3b;">' . esc_html__('已使用', 'aksrt') . '</span>';
|
||
if (!empty($msg['order_number'])) {
|
||
$order_link_url = admin_url('admin.php?page=aksrt_orders&s=' . urlencode((string) $msg['order_number']));
|
||
$status_html .= '<a style="color: #ff6215;" target="_blank" href="' . esc_url($order_link_url) . '"> [' . esc_html__('查看', 'aksrt') . ']</a>';
|
||
}
|
||
} elseif ($msg['status'] === 'expired') {
|
||
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已过期', 'aksrt') . '</span>';
|
||
} elseif ($msg['status'] === 'disabled') {
|
||
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已禁用', 'aksrt') . '</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__('会员兑换', 'aksrt') . '</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__('积分兑换', 'aksrt') . '</a>';
|
||
$type_html .= '<br>' . esc_html__('积分:', 'aksrt') . 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__('余额充值', 'aksrt') . '</a>';
|
||
$type_html .= '<br>' . esc_html__('金额:', 'aksrt') . 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__('自定义卡密', 'aksrt') . '</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__('选择', 'aksrt') . '</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 : __('卡号', 'aksrt')) . "\">" . 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 : __('密码', 'aksrt')) . "\">" . 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="aksrt-cardpass-list-form" method="post">';
|
||
wp_nonce_field(self::NONCE);
|
||
echo '<div class="bulkactions aksrt-cardpass-bulk">
|
||
<label for="bulk-action-selector-top" class="screen-reader-text">' . esc_html__('选择批量操作', 'aksrt') . '</label><select name="action" id="bulk-action-selector-top">
|
||
<option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option>
|
||
<option value="delete">' . esc_html__('删除', 'aksrt') . '</option>
|
||
</select>
|
||
<input type="submit" class="button action" value="' . esc_attr__('应用', 'aksrt') . '">
|
||
</div>';
|
||
|
||
NativeUi::table_card_open();
|
||
echo '<table class="widefat fixed striped posts table table-bordered aksrt-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;
|
||
}
|
||
}
|