- 渲染期按表单默认值预判依赖可见性(deps_visible/default_form_value),不满足项直接输出 display:none,消除导入提醒首帧全部显示的问题 - 行内 JS 在 DOMContentLoaded 兜底执行一次依赖求值,交互前后状态与服务端一致
1205 lines
62 KiB
PHP
1205 lines
62 KiB
PHP
<?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'));
|
||
}
|
||
}
|
||
}
|