Files
wordpress-theme-aksrt/src/Admin/Native/PayChargeCards.php
T
kate522 ef9409cee6 fix(payment): 添加卡密页依赖字段初始状态按默认值隐藏
- 渲染期按表单默认值预判依赖可见性(deps_visible/default_form_value),不满足项直接输出 display:none,消除导入提醒首帧全部显示的问题
- 行内 JS 在 DOMContentLoaded 兜底执行一次依赖求值,交互前后状态与服务端一致
2026-09-16 22:40:35 +08:00

1205 lines
62 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'])) : '';
$page_title = __('卡密管理', 'aksrt');
$head_but = '<a href="' . esc_url(add_query_arg('tab', 'add', $this->page_url)) . '" class="page-title-action">' . esc_html__('添加卡密', 'aksrt') . '</a>';
// 页面级动作(参考主题在渲染前处理 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();
}
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;
}
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();
}
echo '</div>';
}
/* ------------------------------------------------------------------
* 列表
* ------------------------------------------------------------------ */
private function render_list(): void {
$svc = \AKSRT\Payment\ChargeCard::get_instance();
$s = isset($_REQUEST['s']) ? sanitize_text_field((string) wp_unslash($_REQUEST['s'])) : '';
$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 = '';
}
// 标题右侧按钮与过滤链接。
$head_but = '';
if ($s !== '') {
$head_but .= '<div><div class="update-nag" style="margin: 10px 0 0;">' . sprintf(esc_html__('搜索 “%s” 的内容', 'aksrt'), esc_html($s)) . ' </div></div>';
} else {
$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>';
}
$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 $head_but; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
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') {
$type_html = '<a style="color:#a42ade" href="' . esc_url(add_query_arg('type', 'custom', $this->page_url)) . '">' . esc_html__('自定义卡密', 'aksrt') . '</a>';
} else {
$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 一致)。
* 参考主题 CSF 的 dependency 在 DOM ready 后才隐藏不满足项,首帧会闪现全部字段;
* 此处在渲染期直接置 display:none,修正「导入提醒初始全部显示」的问题。
*
* @param array<int,array{key:string,op:string,val:string}> $deps
*/
private function deps_visible(array $deps): bool {
foreach ($deps as $dep) {
$v = (string) $this->default_form_value((string) ($dep['key'] ?? ''));
$val = (string) ($dep['val'] ?? '');
switch ((string) ($dep['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;
}
/**
* 表单字段的初始默认值(须与 field_row/button_set/spinner 渲染的默认值保持同步)。
* 未列出的键按空字符串处理,与行内 JS formVal 找不到元素时返回 '' 的行为一致。
*/
private function default_form_value(string $key): string {
switch ($key) {
case 'add_type':
return 'auto';
case 'type':
return 'balance';
case 'auto_top_s':
case 'only_pass_s':
return '';
case 'vip_exchange[time]':
return '';
case 'vip_exchange[unit]':
return 'month';
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; }
.orderby-but { position: relative; display: inline-block; margin-left: 4px; }
.orderby-but > a { opacity: .4; position: absolute; transform: translateY(-3px); transition: .3s; }
.orderby-but > a + a { transform: translateY(6px); }
.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 () {
function formVal(key) {
var els = document.querySelectorAll('[name="' + key + '"]');
if (!els.length) return '';
if (els[0].type === 'radio') {
var c = document.querySelector('[name="' + key + '"]:checked');
return c ? c.value : '';
}
if (els[0].type === 'checkbox') return els[0].checked ? '1' : '';
return els[0].value;
}
function evalDeps() {
document.querySelectorAll('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();
});
// 初始按默认值求值一次:服务端已预隐藏,这里兜底校正。
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();
}
});
evalDeps();
})();
</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'));
}
}
}