Files
wordpress-theme-aksrt/src/Admin/Native/PayChargeCardsForms.php
T
kate522andClaude Fable 5 18951b01d6 feat(admin): unify native admin pages on a shared modern UI
Add NativeUi helper + native-ui.css (tokens mirror the SPA Naive UI theme)
and move the pay center, orders, products, coupons, charge cards, income,
withdrawals, tickets, invite codes, VIP, verify, messages and report/ban
pages onto shared header, stat cards, cards, filters, badges and empty
states. Remove inline styles and dead helpers; split PayChargeCards into
sibling traits to stay under the file size limit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-30 15:05:38 +08:00

437 lines
24 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
*
* PayChargeCards 的「添加卡密 / 导出卡密」表单渲染与依赖显隐求值。
* 表单结构、字段依赖(field_row / submessage_row / deps_visible)、按钮组与数字输入
* 均与参考主题 1:1 对齐,由 charge-cards-dependency / charge-cards-form-constraints 两组契约锁定。
*
* 作为 flat sibling trait 被 PayChargeCards 使用(PSR-4:trait 必须与宿主类同目录)。
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
trait PayChargeCardsForms {
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, __('张', 'aksrt')),
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']],
__('需要生成多少张卡密(1 - 1000 的整数,单次生成数量太多可能会对服务器性能造成影响)', '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="any" 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, __('位数', 'aksrt')),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'auto_top_s', 'op' => 'checked'],
]
);
$this->field_row(
__('自定义密码位数', 'aksrt'),
$this->spinner('auto_rand_password_limit', 35, __('位数', 'aksrt')),
[
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
['key' => 'auto_top_s', 'op' => 'checked'],
],
__('自定义自动生成的长度(不能太短,太短可能会出现重复)如果启用了单密码模式,可以将卡号位数设置为0', 'aksrt')
);
// 导入提示统一放在「卡密数据」正上方:切换卡密类型/添加方式时,
// 命中的那一条紧邻示例数据与输入框,便于对照填写。
$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' => 'auto'],
['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'],
],
'<p><b>' . esc_html__('导入卡密', 'aksrt') . '</b></p>
<ul class="aksrt-submsg-list">
<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>
</ul>
<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>
<ul class="aksrt-submsg-list">
<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>
</ul>
<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->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>
<ul class="aksrt-submsg-list">
<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>
</ul>
<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>
<ul class="aksrt-submsg-list">
<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>
</ul>
<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
}
/* ------------------------------------------------------------------
* 字段渲染 / 依赖显隐
* ------------------------------------------------------------------ */
/**
* @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 {
$dep_attr = ' data-dep="' . esc_attr((string) wp_json_encode($deps)) . '"';
$hidden = $this->deps_visible($deps) ? '' : ' style="display:none;"';
// 提示块**不要**挂 WP 的 .notice / .notice-warning:wp-admin/js/common.js 在 DOM
// 就绪时会执行
// $('div.updated, div.error, div.notice').not('.inline, .below-h2').insertAfter($headerEnd)
// 把页面上所有「非 inline」的 notice 整块搬到 .wp-header-end(不存在时取 .wrap 内首个
// h1/h2)之后。提示块一旦带上 notice,就会被搬到页面顶部、脱离「添加方式—卡密数据」
// 之间的字段区,而且搬迁发生在求值之后,行内的显隐逻辑无从纠正。
// 外观改由 .aksrt-submsg 自带(见 print_assets),与参考主题 CSF content 字段一致。
// data-dep 与初始隐藏状态同时挂在 <tr> 和内层提示块上:提示块若脱离所在行,仍能自行显隐。
echo '<tr' . $dep_attr . $hidden . '><td colspan="2"><div' . $dep_attr . ' class="aksrt-submsg"' . $hidden . '>' . $content . '</div></td></tr>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。
}
/**
* 服务端按表单默认值预判依赖可见性(求值规则与行内 JS 完全一致)。
* 仅在表单渲染默认值(添加方式=系统自动生成、类型=余额)下,把不满足依赖的行标记为初始隐藏,
* 避免四条导入提示首帧铺开。
*
* 注意:已知键必须覆盖 field_row/submessage_row 用到的所有依赖键,否则该键的判定会与 JS 不一致,
* 出现「服务端判可见、JS 判隐藏」之类的错配(例如自定义卡密的两条导入提示同时铺开)。
*
* @param array<int,array{key:string,op:string,val:string}> $deps
*/
private function deps_visible(array $deps): bool {
foreach ($deps as $dep) {
$key = (string) ($dep['key'] ?? '');
$val = (string) ($dep['val'] ?? '');
$op = (string) ($dep['op'] ?? '');
if (!$this->is_known_dep_key($key)) {
// 未登记的键:不做服务端判定,交 JS 处理。
continue;
}
$v = (string) $this->default_form_value($key);
switch ($op) {
case '==':
if ($v !== $val) {
return false;
}
break;
case '!=':
if ($v === $val) {
return false;
}
break;
case 'any':
if (!in_array($v, explode(',', $val), true)) {
return false;
}
break;
case 'checked':
if ($v === '') {
return false;
}
break;
case 'unchecked':
if ($v !== '') {
return false;
}
break;
}
}
return true;
}
/**
* 可在服务端求值的字段键(须完整覆盖表单依赖中使用的键,与 default_form_value 保持一致)。
*/
private function is_known_dep_key(string $key): bool {
static $known = [
'add_type',
'type',
'auto_top_s',
'only_pass_s',
'vip_exchange[time]',
'export_format',
'status',
];
return in_array($key, $known, true);
}
/**
* 表单字段的初始默认值(须与 field_row/button_set/spinner 渲染的默认值保持同步)。
*/
private function default_form_value(string $key): string {
switch ($key) {
case 'add_type':
return 'auto';
case 'type':
return 'balance';
case 'export_format':
return 'xls';
case 'status':
return 'all';
// 未勾选的 checkbox/switcher 与空文本,在依赖求值中均等价于空字符串。
case 'auto_top_s':
case 'only_pass_s':
case 'vip_exchange[time]':
return '';
default:
return '';
}
}
/**
* button_set:radio 按钮组(参考主题 CSF button_set 的原生替代)。
*
* @param array<string,string> $options
*/
private function button_set(string $name, array $options, string $current): string {
$html = '<div class="aksrt-btnset">';
foreach ($options as $value => $label) {
$html .= '<label class="aksrt-btnset-item"><input type="radio" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '"' . checked((string) $current, (string) $value, false) . '><span>' . esc_html((string) $label) . '</span></label>';
}
return $html . '</div>';
}
/**
* 数字输入(参考主题 CSF spinner / number 字段的原生替代)。
*
* **一律不写原生 min / max / step 约束属性,只写 `step="any"`:**
* - 本表单的行是条件显隐的(`field_row()` 会给不满足依赖的 `<tr>` 打上 `display:none`)。
* HTML5 约束校验会检查**隐藏**控件,而隐藏控件无法聚焦、浏览器弹不出提示气泡,
* 于是直接**静默拒绝提交**(控制台只有一句 "An invalid form control ... is not focusable"),
* 页面毫无反应 —— 用户看到的就是「确认提交按钮不起作用」。
* - `step="any"` 不能省:`type="number"` 缺省 `step=1`,会退回整数网格,把非整数输入判为非法,
* 同样触发上面的静默拒绝。
*
* 取值范围不在标记里表达,由服务端单点约束:生成数量见 `handle_add()` 的 (0, 1000] 判定,
* 卡号 / 密码位数见 `ChargeCard::generate_cards()`。
*/
private function spinner(string $name, int $value, string $unit): string {
return '<input type="number" class="small-text" step="any" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '" />'
. ' <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
}
}
}