Files
wordpress-theme-aksrt/src/Admin/Native/PayChargeCards.php
T
kate522 5243bbf0d4 fix(payment): 添加卡密页清理依赖求值死代码并锁定显隐契约
render_add_form() 渲染 19 行表单,其中 17 行带 data-dep;4 条导入提示
(导入卡密 / 导入会员兑换卡密 / 自定义卡密导入 / 自定义卡密导入-单密码模式)
的显隐依赖已由服务端预隐藏与服务端默认值修正落地,本次仅做收尾。

变更:
- 删除行内脚本 IIFE 末尾的 evalDeps() 死调用。该调用位于表单之前的
  解析期(脚本在表单前输出),此刻 document 中尚无任何 tr[data-dep],
  属于必然空操作;初始求值统一交由 DOMContentLoaded 分支处理。
- 补充注释说明「脚本在表单前输出 → 解析期取不到行 → 必须等 DOM 就绪」,
  避免后人误加回解析期调用。
- .gitignore 追加 /tests/admin/charge-cards-dependency.test.cjs 白名单。

新增契约测试 tests/admin/charge-cards-dependency.test.cjs(5 用例,全过):
从源码抽取 field_row/submessage_row 的 data-dep,用 Node vm 加载真实行内
脚本并以最小 DOM 桩驱动,断言:
1) 依赖键须在 is_known_dep_key 与 default_form_value 成对登记;
2) 服务端预判与前端求值使用同一组操作符且覆盖表单实际用到的全部操作符;
3) 两类行渲染均接入服务端预隐藏,避免首帧铺开;
4) 行内脚本不再保留解析期空操作求值;
5) 导入提示跟随「卡密类型 × 添加方式」切换(balance/credits/vip/custom
   及单密码模式矩阵),且不同时铺开、切回 auto 全部收起。

负向验证:将 balance,credits 篡改为 balance,credits,vip 后测试转为
pass 4 / fail 1,护栏确实生效。

验证:node --test 两个 admin 契约测试 9/9 通过;php -l 无语法错误;
dev 与运行副本内容一致(行尾归一后逐字节相同)。
2026-09-17 11:03:34 +08:00

1256 lines
64 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 AKSRT
*
* 后台「卡密管理」页面 — 1:1 复刻参考主题 zibll 的 zibpay/page/charge-card.php。
*
* 结构:列表(过滤/搜索/排序/批量删除/分页) + 添加卡密(自动生成 / 导入) + 导出卡密(text / xls)。
* 卡密类型与参考主题一致:余额充值 / 会员兑换 / 积分兑换 / 自定义卡密;
* 本主题内部类型映射:balance / vip / credits / custom。
* 差异点:AKSRT 无商城发货(shipped)子系统,故不含「已售」标记与链接;删除取代参考主题的批量操作语义一致。
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
class PayChargeCards {
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_charge_cards';
public const NONCE = 'aksrt_card_pass_page';
public const EXPORT_AJAX_ACTION = 'aksrt_card_pass_export';
private static $instance = null;
/** @var string 当前页面 URL */
private $page_url = '';
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('wp_ajax_' . self::EXPORT_AJAX_ACTION, [$this, 'handle_export']);
}
public function render_charge_cards_page(): void {
$this->ensure_cap();
$this->page_url = admin_url('admin.php?page=' . self::PAGE_SLUG);
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
echo '<div class="wrap"><h1>' . esc_html__('卡密管理', 'aksrt') . '</h1>';
echo '<div class="notice notice-error"><p>' . esc_html__('卡密模块未初始化。', 'aksrt') . '</p></div></div>';
return;
}
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : '';
$s = isset($_REQUEST['s']) ? sanitize_text_field((string) wp_unslash($_REQUEST['s'])) : '';
$page_title = __('卡密管理', 'aksrt');
$head_but = '';
// 页面级动作(参考主题在渲染前处理 add / delete)。
$action = isset($_REQUEST['action']) ? sanitize_key((string) wp_unslash($_REQUEST['action'])) : '';
if ($action === 'add') {
$this->handle_add();
} elseif ($action === 'delete') {
$this->handle_delete();
}
// 标题右侧按钮是页面头部的一部分,只在此处输出一次(列表页同样如此,由 render_list 负责列表本体)。
switch ($tab) {
case 'add':
$page_title = __('添加卡密', 'aksrt');
$head_but = '<a href="' . esc_url($this->page_url) . '" class="page-title-action">' . esc_html__('返回列表', 'aksrt') . '</a>';
break;
case 'export':
$page_title = __('导出卡密', 'aksrt');
$head_but = '<a href="' . esc_url($this->page_url) . '" class="page-title-action">' . esc_html__('返回列表', 'aksrt') . '</a>';
break;
default:
$head_but = '<a href="' . esc_url(add_query_arg('tab', 'add', $this->page_url)) . '" class="page-title-action">' . esc_html__('添加卡密', 'aksrt') . '</a>';
$head_but .= '<a href="' . esc_url(add_query_arg('tab', 'export', $this->page_url)) . '" class="page-title-action">' . esc_html__('导出卡密', 'aksrt') . '</a>';
if ($s !== '') {
$head_but .= '<div><div class="update-nag" style="margin: 10px 0 0;">' . sprintf(esc_html__('搜索 “%s” 的内容', 'aksrt'), esc_html($s)) . ' </div></div>';
}
break;
}
echo '<div class="wrap">';
$this->print_assets();
echo '<h1 class="wp-heading-inline">' . esc_html($page_title) . '</h1>';
echo $head_but; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
if ($tab === 'add') {
$this->render_add_form();
} elseif ($tab === 'export') {
$this->render_export_form();
} else {
$this->render_list($s);
}
echo '</div>';
}
/* ------------------------------------------------------------------
* 列表
* ------------------------------------------------------------------ */
/**
* @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));
$per_page = max(1, min(100, (int) ($_REQUEST['ice_perpage'] ?? 30)));
// 参考主题: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>';
echo '<div class="order-header"><ul class="subsubsub">' . $but_html . '</ul>' . $search . '</div>';
$result = $svc->get_list([
'status' => $status,
'type' => $type,
'other' => $other,
'search' => $s,
'orderby' => $orderby,
'order' => $order,
'limit' => $per_page,
'offset' => ($paged - 1) * $per_page,
]);
$items = (array) ($result['items'] ?? []);
$count_all = (int) ($result['total'] ?? 0);
if (!$items) {
echo '<div class="clear"></div>';
echo '<table class="widefat fixed striped posts table table-bordered" style="min-width: 1000px;">';
echo '<thead><tr><td style="color: #ff4a4a;text-align: center;">' . esc_html__('未找到对应内容,或暂无内容', 'aksrt') . '</td></tr></thead>';
echo '</table>';
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 '<div class="clear"></div>';
echo '<form class="" method="post">';
wp_nonce_field(self::NONCE);
echo '<div class="bulkactions" style="margin: 10px 0;">
<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>';
echo '<div style="overflow-y: auto;width: 100%;">';
echo '<table class="widefat fixed striped posts table table-bordered" style="min-width: 1000px;">';
echo '<thead><tr>' . $thead_th . '</tr></thead>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
echo '<tbody>' . $tbody . '</tbody>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
echo '</table>';
echo '</div>';
echo '</form>';
echo '<div class="clear"></div>';
$this->render_pagenavi($count_all, $per_page);
}
/* ------------------------------------------------------------------
* 添加卡密
* ------------------------------------------------------------------ */
private function render_add_form(): void {
$type_labels = $this->type_labels();
?>
<p><b><?php esc_html_e('在此添加卡密', 'aksrt'); ?></b></p>
<ul style="list-style:disc;margin:0 0 12px 18px;line-height:1.9">
<li><?php esc_html_e('如果您已经准备好了卡密资料,请选择导入的方式添加', 'aksrt'); ?></li>
<li><?php esc_html_e('您也可以采用系统生成的方式,自动批量添加卡密', 'aksrt'); ?></li>
</ul>
<form class="aksrt-cardpass-form" method="post">
<?php wp_nonce_field(self::NONCE); ?>
<input type="hidden" name="action" value="add" />
<table class="form-table" role="presentation">
<?php
$this->field_row(
__('卡密类型', 'aksrt'),
$this->button_set('type', $type_labels, 'balance'),
[]
);
$this->field_row(
__('添加方式', 'aksrt'),
$this->button_set('add_type', [
'auto' => __('系统自动生成', 'aksrt'),
'import' => __('导入卡密', 'aksrt'),
], 'auto'),
[]
);
// 自动生成
$this->field_row(
__('生成数量', 'aksrt'),
$this->spinner('auto_num', 20, 1, 1000, 10, __('张', 'aksrt')),
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']],
__('需要生成多少张卡密(单次生成数量太多可能会对服务器性能造成影响)', 'aksrt')
);
$this->field_row(
__('标识', 'aksrt'),
'<input type="text" class="regular-text" name="auto_remarks" value="' . esc_attr('cardpass_' . current_time('YmdHis')) . '" />',
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']],
__('对生成的卡密做标记标识,方便后期查找管理', 'aksrt') . '<br>' . sprintf(esc_html__('注意:标识中不能有%1$s字符,系统已占用', 'aksrt'), '<code>shipped</code>')
);
$this->field_row(
__('面额', 'aksrt'),
'<input type="number" step="0.01" min="0" name="auto_price" value="0" />',
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'],
],
__('单张卡密的面额', 'aksrt')
);
$this->field_row(
__('兑换会员', 'aksrt'),
__('会员等级', 'aksrt') . ' ' . $this->button_set('vip_exchange[level]', [
'1' => $this->vip_level_name(1),
'2' => $this->vip_level_name(2),
], '1'),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'type', 'op' => '==', 'val' => 'vip'],
]
);
$this->field_row(
' ',
__('兑换会员时长', 'aksrt') . '<br><input type="text" name="vip_exchange[time]" value="" />',
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'type', 'op' => '==', 'val' => 'vip'],
],
__('填', 'aksrt') . '<code>Permanent</code>' . __('为永久', 'aksrt')
);
$this->field_row(
' ',
__('时长单位', 'aksrt') . ' ' . $this->button_set('vip_exchange[unit]', [
'day' => __('天', 'aksrt'),
'month' => __('个月', 'aksrt'),
], 'month'),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'type', 'op' => '==', 'val' => 'vip'],
['key' => 'vip_exchange[time]', 'op' => '!=', 'val' => 'Permanent'],
]
);
$this->field_row(
__('自定义显示名称', 'aksrt'),
__('卡号显示名称', 'aksrt') . '<br><input type="text" class="regular-text" name="custom_name[card_name]" value="" /><br>' .
__('卡密显示名称', 'aksrt') . '<br><input type="text" class="regular-text" name="custom_name[password_name]" value="" />',
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'type', 'op' => '==', 'val' => 'custom'],
]
);
$this->field_row(
__('高级选项', 'aksrt'),
'<input type="checkbox" name="auto_top_s" value="1" />',
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']]
);
$this->field_row(
__('自定义卡号位数', 'aksrt'),
$this->spinner('auto_rand_number_limit', 20, 1, 50, 5, __('位数', 'aksrt')),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'auto_top_s', 'op' => 'checked'],
]
);
$this->field_row(
__('自定义密码位数', 'aksrt'),
$this->spinner('auto_rand_password_limit', 35, 1, 50, 5, __('位数', 'aksrt')),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'auto_top_s', 'op' => 'checked'],
],
__('自定义自动生成的长度(不能太短,太短可能会出现重复)如果启用了单密码模式,可以将卡号位数设置为0', 'aksrt')
);
// 导入卡密(与参考主题一致的提示文案与示例)。
$this->submessage_row(
[
['key' => 'add_type', 'op' => '!=', 'val' => 'auto'],
['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'],
],
'<p><b>' . esc_html__('导入卡密', 'aksrt') . '</b></p>
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 面额 标识', 'aksrt') . '</code></li>
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '</li>
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA 50 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('50' . $this->currency_unit())) . '</div>
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq 100 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('100' . $this->currency_unit())) . '</div>'
);
$this->submessage_row(
[
['key' => 'add_type', 'op' => '!=', 'val' => 'auto'],
['key' => 'type', 'op' => '==', 'val' => 'vip'],
],
'<p><b>' . esc_html__('导入会员兑换卡密', 'aksrt') . '</b></p>
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 会员参数 标识', 'aksrt') . '</code></li>
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
<li>' . esc_html__('会员参数格式为:', 'aksrt') . '<code>level=1&time=5&unit=day</code></li>
<li>' . esc_html__('参数说明:level为等级,只能为1或2。time为时长,填Permanent或数字。unit为时长单位,只能填day(天)或者month(月)', 'aksrt') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '</li>
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA level=1&time=5&unit=day ' . esc_html__('5天一级会员', 'aksrt') . '</div>
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq level=2&time=Permanent ' . esc_html__('永久2级会员', 'aksrt') . '</div>
'
);
$this->field_row(
__('单密码模式', 'aksrt'),
'<input type="checkbox" name="only_pass_s" value="1" />',
[
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
['key' => 'type', 'op' => '==', 'val' => 'custom'],
]
);
$this->submessage_row(
[
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
['key' => 'type', 'op' => '==', 'val' => 'custom'],
['key' => 'only_pass_s', 'op' => 'unchecked'],
],
'<p><b>' . esc_html__('自定义卡密导入', 'aksrt') . '</b></p>
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 标识 自定义参数', 'aksrt') . '</code></li>
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
<li>' . esc_html__('自定义参数格式为:', 'aksrt') . '<code>card_name=授权号&password_name=授权密码</code>,' . esc_html__('card_name为卡号的显示名称,password_name为卡密的显示名称,可以留空', 'aksrt') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li>
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '</div>
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '</div>
<div>111111@qq.com 11assssddsds ' . esc_html__('某某账号', 'aksrt') . ' card_name=' . esc_html__('账号', 'aksrt') . '&password_name=' . esc_html__('密码', 'aksrt') . '</div>'
);
$this->submessage_row(
[
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
['key' => 'type', 'op' => '==', 'val' => 'custom'],
['key' => 'only_pass_s', 'op' => 'checked'],
],
'<p><b>' . esc_html__('自定义卡密导入(单密码模式)', 'aksrt') . '</b></p>
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('密码 标识 自定义参数', 'aksrt') . '</code></li>
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li>
<li>' . esc_html__('自定义参数格式为:', 'aksrt') . '<code>password_name=授权密码</code>,' . esc_html__('password_name为卡密的显示名称,可以留空', 'aksrt') . '</li>
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
<div>3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '</div>
<div>SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '</div>'
);
$this->field_row(
__('卡密数据', 'aksrt'),
'<textarea name="import_data" rows="10" class="large-text code" style="resize: both;max-width: none;"></textarea>',
[['key' => 'add_type', 'op' => '==', 'val' => 'import']]
);
$this->field_row(
__('自定义分隔符号', 'aksrt'),
'<input type="text" class="mini-input" style="width:80px;" name="import_division" value=" " />',
[['key' => 'add_type', 'op' => '==', 'val' => 'import']],
__('卡号和密码之间分割符号(默认为空格分割)', 'aksrt')
);
?>
</table>
<?php submit_button(__('确认提交', 'aksrt'), 'primary', 'submit', true); ?>
</form>
<?php
}
/* ------------------------------------------------------------------
* 导出卡密
* ------------------------------------------------------------------ */
private function render_export_form(): void {
?>
<form class="aksrt-cardpass-form" method="post" action="<?php echo esc_url(admin_url('admin-ajax.php')); ?>">
<input type="hidden" name="action" value="<?php echo esc_attr(self::EXPORT_AJAX_ACTION); ?>" />
<table class="form-table" role="presentation">
<?php
$this->field_row(
__('卡密类型', 'aksrt'),
$this->button_set('type', $this->type_labels(), 'balance'),
[]
);
$this->field_row(
__('选择状态', 'aksrt'),
$this->button_set('status', [
'all' => __('全部', 'aksrt'),
'0' => __('未使用', 'aksrt'),
'used' => __('已使用', 'aksrt'),
], 'all'),
[['key' => 'type', 'op' => '!=', 'val' => 'custom']]
);
$this->field_row(
__('导出格式', 'aksrt'),
$this->button_set('export_format', [
'text' => __('文本文档', 'aksrt'),
'xls' => __('Excel表格', 'aksrt'),
], 'xls'),
[]
);
$this->field_row(
' ',
__('分隔符号', 'aksrt') . '<br><input type="text" class="mini-input" style="width:80px;" name="text_division" value=" " />',
[['key' => 'export_format', 'op' => '==', 'val' => 'text']],
__('卡号和密码之间分割符号(默认为空格分割)', 'aksrt')
);
?>
</table>
<?php submit_button(__('确认提交', 'aksrt'), 'primary', 'submit', true); ?>
</form>
<?php
}
/**
* 导出处理(参考主题 zibpay_ajax_card_pass_export 同款行为)。
*/
public function handle_export(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('暂无此权限', 'aksrt'));
}
@set_time_limit(0);
$export_format = isset($_REQUEST['export_format']) && sanitize_key((string) wp_unslash($_REQUEST['export_format'])) === 'text' ? 'text' : 'xls';
$type = isset($_REQUEST['type']) ? sanitize_key((string) wp_unslash($_REQUEST['type'])) : '';
$status_raw = isset($_REQUEST['status']) ? sanitize_key((string) wp_unslash($_REQUEST['status'])) : 'all';
$status = '';
if ($status_raw !== 'all' && $status_raw !== '') {
// 参考主题:0=未使用。
$status = $status_raw === '0' ? 'active' : $status_raw;
}
$rows = \AKSRT\Payment\ChargeCard::get_instance()->get_export_rows($type, $status);
if (!$rows) {
wp_die(esc_html__('暂无可导出的内容', 'aksrt'));
}
$filename = $type . '_' . gmdate('d_m_Y');
// 组装行数据(与参考主题字段一致)。
$title = [];
$data = [];
foreach ($rows as $row) {
$meta = [];
if (!empty($row['meta'])) {
$decoded = json_decode((string) $row['meta'], true);
$meta = is_array($decoded) ? $decoded : [];
}
$status_text = $row['status'] === 'used'
? __('已使用', 'aksrt')
: ($row['status'] === 'expired' ? __('已过期', 'aksrt') : ($row['status'] === 'disabled' ? __('已禁用', 'aksrt') : __('未使用', 'aksrt')));
switch ($type) {
case 'credits': // 积分兑换
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('面额', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
$data[] = [(string) $row['card'], (string) $row['password'], (string) (int) ($meta['points'] ?? 0), (string) $row['note'], $status_text];
break;
case 'vip': // 会员兑换
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('兑换会员', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
$data[] = [(string) $row['card'], (string) $row['password'], $this->vip_meta_query($meta), (string) $row['note'], $status_text];
break;
case 'custom': // 自定义
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
$data[] = [(string) $row['card'], (string) $row['password'], (string) $row['note'], ''];
break;
default: // 余额充值
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('面额', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
$data[] = [(string) $row['card'], (string) $row['password'], $this->balance_amount_text($meta), (string) $row['note'], $status_text];
}
}
if ($export_format === 'text') {
$division = isset($_REQUEST['text_division']) ? (string) wp_unslash($_REQUEST['text_division']) : ' ';
header('Content-type:application/octet-stream');
header('Accept-Ranges:bytes');
header('Content-Disposition:attachment;filename=' . $filename . '.txt');
header('Pragma: no-cache');
header('Pragma: public');
header('Expires: 0');
$_data = [];
foreach ($data as $val) {
$_data[] = implode($division, $val);
}
echo implode("\n", $_data); // phpcs:ignore WordPress.Security.EscapeOutput
} else {
$this->export_excel($data, $title, $filename);
}
exit;
}
/**
* 导出 xls(参考主题 zib_export_excel 同款行为,GB2312 编码保证 Excel 兼容)。
*
* @param array<int,array<int,string>> $data
* @param array<int,string> $title
*/
private function export_excel(array $data, array $title, string $filename): void {
header('Content-type:application/octet-stream');
header('Accept-Ranges:bytes');
header('Content-type:application/vnd.ms-excel');
header('Content-Disposition:attachment;filename=' . $filename . '.xls');
header('Pragma: no-cache');
header('Expires: 0');
$convert = static function (string $value): string {
return function_exists('mb_convert_encoding')
? (string) @mb_convert_encoding($value, 'GB2312', 'UTF-8')
: $value;
};
if (!empty($title)) {
$title = array_map($convert, $title);
echo implode("\t", $title) . "\n";
}
if (!empty($data)) {
$_data = [];
foreach ($data as $val) {
$val = array_map($convert, array_map('strval', $val));
$_data[] = implode("\t", $val);
}
echo implode("\n", $_data);
}
}
/* ------------------------------------------------------------------
* 动作处理
* ------------------------------------------------------------------ */
private function handle_add(): void {
check_admin_referer(self::NONCE);
$add_type = isset($_REQUEST['add_type']) ? sanitize_key((string) wp_unslash($_REQUEST['add_type'])) : 'auto';
$pass_type = isset($_REQUEST['type']) ? sanitize_key((string) wp_unslash($_REQUEST['type'])) : '';
if (!in_array($pass_type, ['balance', 'vip', 'credits', 'custom'], true)) {
$this->page_notice(__('错误!', 'aksrt'), __('参数传入错误', 'aksrt'), 'error');
return;
}
if ($add_type === 'import') {
$this->handle_import($pass_type);
return;
}
// 自动生成。
$auto_num = isset($_REQUEST['auto_num']) ? (int) $_REQUEST['auto_num'] : 0;
if (!$auto_num) {
$this->page_notice(__('错误!', 'aksrt'), __('请输入需要生成的数量', 'aksrt'), 'error');
return;
}
$pass_meta = [];
switch ($pass_type) {
case 'balance':
case 'credits':
$auto_price = isset($_REQUEST['auto_price']) ? floatval(round((float) $_REQUEST['auto_price'], 2)) : 0;
if (!$auto_price) {
$this->page_notice(__('错误!', 'aksrt'), __('请输入卡密的面额', 'aksrt'), 'error');
return;
}
$pass_meta = $pass_type === 'credits'
? ['points' => (int) $auto_price]
: ['amount' => number_format($auto_price, 2, '.', '')];
break;
case 'vip':
$vip = isset($_REQUEST['vip_exchange']) && is_array($_REQUEST['vip_exchange']) ? wp_unslash($_REQUEST['vip_exchange']) : [];
if (empty($vip['time'])) {
$this->page_notice(__('错误!', 'aksrt'), __('请填写兑换会员参数', 'aksrt'), 'error');
return;
}
$pass_meta = $this->vip_meta($vip);
break;
}
// 自定义显示名称(参考主题:所有类型均可附加)。
$custom_name = isset($_REQUEST['custom_name']) && is_array($_REQUEST['custom_name']) ? wp_unslash($_REQUEST['custom_name']) : [];
if (!empty($custom_name['card_name'])) {
$pass_meta['custom_name']['card_name'] = trim(sanitize_text_field((string) $custom_name['card_name']));
}
if (!empty($custom_name['password_name'])) {
$pass_meta['custom_name']['password_name'] = trim(sanitize_text_field((string) $custom_name['password_name']));
}
$rand_number = 20;
$rand_password = 35;
if (!empty($_REQUEST['auto_top_s'])) {
$rand_number = isset($_REQUEST['auto_rand_number_limit']) ? (int) $_REQUEST['auto_rand_number_limit'] : 20;
$rand_password = isset($_REQUEST['auto_rand_password_limit']) ? (int) $_REQUEST['auto_rand_password_limit'] : 35;
}
$remarks = isset($_REQUEST['auto_remarks']) ? sanitize_text_field((string) wp_unslash($_REQUEST['auto_remarks'])) : '';
$created = \AKSRT\Payment\ChargeCard::get_instance()->generate_cards($pass_type, $auto_num, $pass_meta, $rand_number, $rand_password, $remarks);
$this->page_notice(__('完成!', 'aksrt'), sprintf(__('已自动生成%s个卡密', 'aksrt'), (string) $created));
}
private function handle_import(string $pass_type): void {
$import_data = isset($_REQUEST['import_data']) ? (string) wp_unslash($_REQUEST['import_data']) : '';
if (!$import_data) {
$this->page_notice(__('错误!', 'aksrt'), __('请粘贴您需要导入的数据', 'aksrt'), 'error');
return;
}
$import_data_array = explode("\r\n", $import_data);
$import_division = isset($_REQUEST['import_division']) ? (string) wp_unslash($_REQUEST['import_division']) : ' ';
if ($import_division === '') {
$import_division = ' ';
}
$success_i = 0;
$error_i = 0;
$svc = \AKSRT\Payment\ChargeCard::get_instance();
foreach ($import_data_array as $line) {
$v = explode($import_division, $line);
$pass_meta = [];
$_card = isset($v[0]) ? trim((string) $v[0]) : '';
$_pass = isset($v[1]) ? trim((string) $v[1]) : '';
$_other = isset($v[3]) ? trim((string) $v[3]) : '';
switch ($pass_type) {
case 'balance':
case 'credits':
$val = isset($v[2]) && $v[2] !== '' ? (float) $v[2] : 0;
if ($pass_type === 'credits') {
$pass_meta = $val ? ['points' => (int) $val] : [];
} else {
$pass_meta = $val ? ['amount' => number_format($val, 2, '.', '')] : [];
}
if (!$pass_meta) {
$error_i++;
continue 2;
}
break;
case 'vip':
$vip_pass_data = isset($v[2]) && $v[2] !== '' ? wp_parse_args(trim((string) $v[2])) : [];
if (!empty($vip_pass_data['level']) && !empty($vip_pass_data['time'])) {
$pass_meta = $this->vip_meta(is_array($vip_pass_data) ? $vip_pass_data : []);
}
if (!$pass_meta) {
$error_i++;
continue 2;
}
break;
case 'custom':
$pass_meta = [];
$only_pass_s = !empty($_REQUEST['only_pass_s']);
if ($only_pass_s) {
// 单密码模式:密码 标识 自定义参数
$_card = '';
$_pass = isset($v[0]) ? trim((string) $v[0]) : '';
$_other = isset($v[1]) ? trim((string) $v[1]) : '';
$custom_name = isset($v[2]) && $v[2] !== '' ? wp_parse_args(trim((string) $v[2])) : [];
} else {
$_other = isset($v[2]) ? trim((string) $v[2]) : '';
$custom_name = isset($v[3]) && $v[3] !== '' ? wp_parse_args(trim((string) $v[3])) : [];
}
if (!empty($custom_name['card_name'])) {
$pass_meta['custom_name']['card_name'] = trim(sanitize_text_field((string) $custom_name['card_name']));
}
if (!empty($custom_name['password_name'])) {
$pass_meta['custom_name']['password_name'] = trim(sanitize_text_field((string) $custom_name['password_name']));
}
break;
}
if ($_pass !== '') {
$id = $svc->create([
'card' => $_card,
'password' => $_pass,
'type' => $pass_type,
'meta' => $pass_meta,
'note' => $_other,
]);
if ($id > 0) {
$success_i++;
} else {
$error_i++;
}
} else {
$error_i++;
}
}
if ($success_i) {
$this->page_notice(
__('导入完成', 'aksrt'),
sprintf(__('成功导入%s个卡密%s', 'aksrt'), (string) $success_i, $error_i ? sprintf(__(',%s个导入失败', 'aksrt'), (string) $error_i) : '')
);
} else {
$this->page_notice(__('导入失败', 'aksrt'), sprintf(__('%s个导入失败,数据格式错误', 'aksrt'), (string) $error_i), 'error');
}
}
private function handle_delete(): void {
check_admin_referer(self::NONCE);
$delete_ids = isset($_REQUEST['action_id']) ? array_map('absint', (array) wp_unslash($_REQUEST['action_id'])) : [];
$delete_ids = array_filter($delete_ids);
if (!$delete_ids) {
$this->page_notice(__('错误!', 'aksrt'), __('未选择需要删除的内容', 'aksrt'), 'error');
return;
}
$delete_i = \AKSRT\Payment\ChargeCard::get_instance()->delete_by_ids($delete_ids);
$this->page_notice(__('删除完成', 'aksrt'), sprintf(__('已删除%s个卡密', 'aksrt'), (string) $delete_i));
}
/* ------------------------------------------------------------------
* 渲染辅助
* ------------------------------------------------------------------ */
/**
* @param array<int,array{key:string,op:string,val:string}> $deps
*/
private function field_row(string $title, string $html, array $deps = [], string $desc = ''): void {
$dep_attr = $deps ? ' data-dep="' . esc_attr((string) wp_json_encode($deps)) . '"' : '';
$hidden = ($deps && !$this->deps_visible($deps)) ? ' style="display:none;"' : '';
echo '<tr' . $dep_attr . $hidden . '><th scope="row">' . esc_html($title) . '</th><td>' . $html; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
if ($desc !== '') {
echo '<p class="description">' . wp_kses_post($desc) . '</p>';
}
echo '</td></tr>';
}
/**
* @param array<int,array{key:string,op:string,val:string}> $deps
*/
private function submessage_row(array $deps, string $content): void {
$hidden = $this->deps_visible($deps) ? '' : ' style="display:none;"';
echo '<tr data-dep="' . esc_attr((string) wp_json_encode($deps)) . '"' . $hidden . '><td colspan="2"><div class="aksrt-submsg notice notice-warning" style="margin:6px 0;padding:6px 12px;">' . $content . '</div></td></tr>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。
}
/**
* 服务端按表单默认值预判依赖可见性(求值规则与行内 JS 完全一致)。
* 仅在表单渲染默认值(添加方式=系统自动生成、类型=余额)下,把不满足依赖的行标记为初始隐藏,
* 避免四条导入提示首帧铺开。
*
* 注意:已知键必须覆盖 field_row/submessage_row 用到的所有依赖键,否则该键的判定会与 JS 不一致,
* 出现「服务端判可见、JS 判隐藏」之类的错配(例如自定义卡密的两条导入提示同时铺开)。
*
* @param array<int,array{key:string,op:string,val:string}> $deps
*/
private function deps_visible(array $deps): bool {
foreach ($deps as $dep) {
$key = (string) ($dep['key'] ?? '');
$val = (string) ($dep['val'] ?? '');
$op = (string) ($dep['op'] ?? '');
if (!$this->is_known_dep_key($key)) {
// 未登记的键:不做服务端判定,交 JS 处理。
continue;
}
$v = (string) $this->default_form_value($key);
switch ($op) {
case '==':
if ($v !== $val) {
return false;
}
break;
case '!=':
if ($v === $val) {
return false;
}
break;
case 'any':
if (!in_array($v, explode(',', $val), true)) {
return false;
}
break;
case 'checked':
if ($v === '') {
return false;
}
break;
case 'unchecked':
if ($v !== '') {
return false;
}
break;
}
}
return true;
}
/**
* 可在服务端求值的字段键(须完整覆盖表单依赖中使用的键,与 default_form_value 保持一致)。
*/
private function is_known_dep_key(string $key): bool {
static $known = [
'add_type',
'type',
'auto_top_s',
'only_pass_s',
'vip_exchange[time]',
'export_format',
'status',
];
return in_array($key, $known, true);
}
/**
* 表单字段的初始默认值(须与 field_row/button_set/spinner 渲染的默认值保持同步)。
*/
private function default_form_value(string $key): string {
switch ($key) {
case 'add_type':
return 'auto';
case 'type':
return 'balance';
case 'export_format':
return 'xls';
case 'status':
return 'all';
// 未勾选的 checkbox/switcher 与空文本,在依赖求值中均等价于空字符串。
case 'auto_top_s':
case 'only_pass_s':
case 'vip_exchange[time]':
return '';
default:
return '';
}
}
/**
* button_set:radio 按钮组(参考主题 CSF button_set 的原生替代)。
*
* @param array<string,string> $options
*/
private function button_set(string $name, array $options, string $current): string {
$html = '<div class="aksrt-btnset">';
foreach ($options as $value => $label) {
$html .= '<label class="aksrt-btnset-item"><input type="radio" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '"' . checked((string) $current, (string) $value, false) . '><span>' . esc_html((string) $label) . '</span></label>';
}
return $html . '</div>';
}
private function spinner(string $name, int $value, int $min, int $max, int $step, string $unit): string {
return '<input type="number" class="small-text" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '" min="' . esc_attr((string) $min) . '" max="' . esc_attr((string) $max) . '" step="' . esc_attr((string) $step) . '" /> <span class="description">' . esc_html($unit) . '</span>';
}
private function page_notice(string $title, string $msg, string $type = 'success'): void {
$html = '';
$html .= $title ? '<h3>' . esc_html($title) . '</h3>' : '';
$html .= $msg ? '<p>' . esc_html($msg) . '</p>' : '';
if ($html) {
echo '<div class="notice notice-' . esc_attr($type) . '">' . $html . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput
}
}
/**
* 分页(参考主题 zibpay_admin_pagenavi 同款结构与禁用逻辑)。
*/
private function render_pagenavi(int $total_count, int $number_per_page = 15): void {
if ($total_count <= 0 || $number_per_page <= 0) {
return;
}
$current_page = max(1, (int) ($_GET['paged'] ?? 1));
$total_pages = (int) ceil($total_count / $number_per_page);
$first_page_url = add_query_arg('paged', 1);
$last_page_url = add_query_arg('paged', $total_pages);
$prev_page_url = '#';
$next_page_url = '#';
if ($current_page > 1 && $current_page < $total_pages) {
$prev_page_url = add_query_arg('paged', $current_page - 1);
$next_page_url = add_query_arg('paged', $current_page + 1);
} elseif (1 == $current_page) {
$first_page_url = '#';
if ($total_pages > 1) {
$next_page_url = add_query_arg('paged', $current_page + 1);
} else {
$next_page_url = '#';
$last_page_url = '#';
}
} elseif ($current_page == $total_pages) {
$prev_page_url = add_query_arg('paged', $current_page - 1);
$next_page_url = '#';
$last_page_url = '#';
}
?>
<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 1 == $current_page ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往第一页', 'aksrt'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
<a class="prev-page button<?php echo 1 == $current_page ? ' 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 $current_page == $total_pages ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往下一页', 'aksrt'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
<a class="last-page button<?php echo $current_page == $total_pages ? ' 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; }
/* 排序箭头对齐:宿主做成零尺寸 inline-block —— 空 inline-block 的基线即其下边缘,
会与文字基线对齐,于是箭头可用 bottom 精确锚定到「基线以上 N em」。
箭头盒中心等于 bottom 值(translateY(50%) 抵消自身高度,与盒子高度无关):
两枚中心分别落在基线以上 .725em / .075em,对称于文字视觉中心(约 .4em)。
不要改回「无尺寸 inline 宿主 + 依赖静态位置 + 固定 -3px/+6px」的写法:
静态位置随表头字体/字号变化,会让整组箭头相对文字偏下(历史缺陷,已量测确认偏下 2px)。 */
.orderby-but { display: inline-block; position: relative; width: 0; height: 0; }
.orderby-but > a { opacity: .4; position: absolute; left: 0; bottom: .725em; transform: translateY(50%); transition: opacity .3s; padding-right: 1em; }
.orderby-but > a + a { bottom: .075em; }
.orderby-but:hover a { opacity: .6; }
.orderby-but > a:hover { opacity: 1; }
.aksrt-btnset { display: flex; flex-wrap: wrap; gap: 6px; }
.aksrt-btnset-item { display: inline-block; cursor: pointer; }
.aksrt-btnset-item input { position: absolute; opacity: 0; pointer-events: none; }
.aksrt-btnset-item span { display: inline-block; padding: 3px 12px; border: 1px solid #c3c4c7; border-radius: 3px; background: #fff; }
.aksrt-btnset-item input:checked + span { background: #2271b1; border-color: #2271b1; color: #fff; }
.aksrt-copy-tip { margin-left: 6px; color: #00a32a; font-size: 12px; white-space: nowrap; }
</style>
<script>
(function () {
// 按 name 属性逐元素匹配,避免把字段名拼进 CSS 选择器:
// 含方括号的字段名(如 vip_exchange[time])会让选择器抛异常并被 catch 吞掉,
// 从而把依赖行永久留在初始状态。
function fieldsByName(key) {
var all = document.querySelectorAll('.aksrt-cardpass-form [name]');
var out = [];
for (var i = 0; i < all.length; i++) {
if (all[i].getAttribute('name') === key) out.push(all[i]);
}
return out;
}
function formVal(key) {
var els = fieldsByName(key);
if (!els.length) return '';
if (els[0].type === 'radio') {
for (var i = 0; i < els.length; i++) {
if (els[i].checked) return els[i].value;
}
return '';
}
if (els[0].type === 'checkbox') return els[0].checked ? '1' : '';
return els[0].value;
}
function evalDeps() {
document.querySelectorAll('.aksrt-cardpass-form tr[data-dep]').forEach(function (tr) {
try {
var deps = JSON.parse(tr.getAttribute('data-dep'));
var show = deps.every(function (d) {
var v = formVal(d.key);
switch (d.op) {
case '==': return String(v) === String(d.val);
case '!=': return String(v) !== String(d.val);
case 'any': return String(d.val).split(',').indexOf(String(v)) !== -1;
case 'checked': return !!v;
case 'unchecked': return !v;
}
return true;
});
tr.style.display = show ? '' : 'none';
} catch (e) { /* noop */ }
});
}
document.addEventListener('change', function (e) {
if (e.target.closest('.aksrt-cardpass-form')) evalDeps();
});
// 唯一的初始求值入口:本脚本在表单之前输出,此刻解析尚未到达表单,
// 直接求值必然取不到任何行(空操作);因此必须等 DOM 就绪后按默认值校正一次。
// 服务端已按默认值预隐藏,这里只做兜底校正。
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', evalDeps);
} else {
evalDeps();
}
document.addEventListener('click', function (e) {
var el = e.target.closest('[data-clipboard-text]');
if (!el) return;
var text = el.getAttribute('data-clipboard-text') || '';
if (!text) return;
var done = function () {
var tip = document.createElement('span');
tip.className = 'aksrt-copy-tip';
var tag = el.getAttribute('data-clipboard-tag') || '';
tip.textContent = (tag ? tag + ' ' : '') + '<?php echo esc_js(__('已复制', 'aksrt')); ?>';
el.appendChild(tip);
setTimeout(function () { tip.remove(); }, 1200);
};
var legacy = function () {
var ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (err) { /* noop */ }
document.body.removeChild(ta);
done();
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, legacy);
} else {
legacy();
}
});
})();
</script>
<?php
}
/* ------------------------------------------------------------------
* 数据辅助
* ------------------------------------------------------------------ */
/**
* 卡密类型(key 即本主题内部类型,与参考主题语义一一对应)。
*
* @return array<string,string>
*/
private function type_labels(): array {
return [
'balance' => __('余额充值', 'aksrt'),
'vip' => __('会员兑换', 'aksrt'),
'credits' => __('积分兑换', 'aksrt'),
'custom' => __('自定义卡密', 'aksrt'),
];
}
private function vip_level_name(int $level): string {
$s = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$name = trim((string) ($s['pay_user_vip_' . max(1, min(2, $level)) . '_name'] ?? ''));
if ($name !== '') {
return $name;
}
return $level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt');
}
/**
* VIP 卡展示文本:Permanent 为永久;day 为天;month 为个月。
*
* @param array<string,mixed> $meta
*/
private function vip_time_text(array $meta): string {
$time = array_key_exists('time', $meta) ? (string) $meta['time'] : null;
if ($time !== null && $time !== '') {
if ($time === 'Permanent') {
return __('永久', 'aksrt');
}
$unit = (isset($meta['unit']) && $meta['unit'] === 'month') ? __('个月', 'aksrt') : __('天', 'aksrt');
return (string) ((int) $time) . $unit;
}
$days = (int) ($meta['days'] ?? 0);
return $days > 0 ? $days . __('天', 'aksrt') : __('永久', 'aksrt');
}
/**
* VIP 卡 meta(内部统一保存 level/days + 参考主题的 time/unit)。
*
* @param array<string,mixed> $vip
* @return array<string,mixed>
*/
private function vip_meta(array $vip): array {
$level = max(1, min(2, (int) ($vip['level'] ?? 1)));
$time_raw = trim((string) ($vip['time'] ?? ''));
$unit = (isset($vip['unit']) && $vip['unit'] === 'month') ? 'month' : 'day';
if ($time_raw === 'Permanent') {
$days = 0;
} else {
$time = (int) $time_raw;
$days = $unit === 'month' ? $time * 30 : $time;
}
return [
'level' => $level,
'days' => max(0, $days),
'time' => $time_raw,
'unit' => $unit,
];
}
/**
* VIP 卡导出参数串(与参考主题导出格式一致)。
*
* @param array<string,mixed> $meta
*/
private function vip_meta_query(array $meta): string {
if (isset($meta['time']) && (string) $meta['time'] !== '') {
return build_query([
'level' => (int) ($meta['level'] ?? 1),
'time' => (string) $meta['time'],
'unit' => (isset($meta['unit']) && $meta['unit'] === 'month') ? 'month' : 'day',
]);
}
$days = (int) ($meta['days'] ?? 0);
return build_query([
'level' => (int) ($meta['level'] ?? 1),
'time' => $days > 0 ? (string) $days : 'Permanent',
'unit' => 'day',
]);
}
/**
* 余额卡面额展示(参考主题为数值文本,不追加单位)。
*
* @param array<string,mixed> $meta
*/
private function balance_amount_text(array $meta): string {
$amount = (string) ($meta['amount'] ?? '');
if ($amount === '') {
return '0';
}
return (string) (float) $amount;
}
private function currency_unit(): string {
$s = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
return trim((string) ($s['pay_currency_unit'] ?? '')) !== '' ? trim((string) $s['pay_currency_unit']) : __('元', 'aksrt');
}
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
}
}
}