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>
437 lines
24 KiB
PHP
437 lines
24 KiB
PHP
<?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
|
||
}
|
||
}
|
||
}
|