feat(admin): upgrade invite code management with dialogs
Replace standalone add and export views with accessible dialogs, refine list interactions, and remove obsolete paths and unused counts. Fix filtered totals and reject oversized imports without truncation, with regression coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1cd19434d1
commit
a6776ffe0a
8 files changed
+884
-400
No files matched your search
@@ -1,9 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Native invite-code operations page.
|
||||
*
|
||||
* 视图结构(列表 / 添加 / 导出)对齐参考主题的运营后台交互:
|
||||
* 服务端渲染 + 表单提交 + 结果回跳提示,无 AJAX 依赖。
|
||||
* Native invite-code list and page assets.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
@@ -13,16 +10,16 @@ namespace AKSRT\Admin\Native;
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusInviteCode {
|
||||
use UsersMenusInviteForms;
|
||||
|
||||
/** 页面专属样式(排序箭头 / 按钮组 / 字段行 / 复制提示),只在本页入队。 */
|
||||
public function enqueue_invite_assets(string $hook): void {
|
||||
if ($hook !== 'users_page_aksrt_invite_codes') {
|
||||
return;
|
||||
}
|
||||
$css = '/assets/css/admin/invite-codes.css';
|
||||
if (is_file(AKSRT_DIR . $css)) {
|
||||
wp_enqueue_style('aksrt-admin-invite-codes', AKSRT_URI . $css, [], (string) filemtime(AKSRT_DIR . $css));
|
||||
}
|
||||
$js = '/assets/js/admin-invite-codes.js';
|
||||
wp_enqueue_style('aksrt-admin-invite-codes', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
|
||||
wp_enqueue_script('aksrt-admin-invite-codes', AKSRT_URI . $js, [], (string) filemtime(AKSRT_DIR . $js), true);
|
||||
}
|
||||
|
||||
public function render_invite_codes_page(): void {
|
||||
@@ -32,353 +29,150 @@ trait UsersMenusInviteCode {
|
||||
|
||||
$service = \AKSRT\Payment\InviteCode::get_instance();
|
||||
$this_url = admin_url('users.php?page=aksrt_invite_codes');
|
||||
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : '';
|
||||
if (!in_array($tab, ['add', 'export'], true)) {
|
||||
$tab = '';
|
||||
}
|
||||
$nonce = wp_create_nonce('aksrt_invite_admin');
|
||||
|
||||
NativeUi::page_open('aksrt-inv-page');
|
||||
NativeUi::header(
|
||||
__('邀请码管理', 'aksrt'),
|
||||
__('管理注册邀请码与使用奖励,点击邀请码即可复制。', 'aksrt'),
|
||||
'dashicons-tickets-alt',
|
||||
[
|
||||
['label' => __('添加邀请码', 'aksrt'), 'dialog' => 'aksrt-inv-add', 'primary' => true, 'icon' => 'dashicons-plus-alt2'],
|
||||
['label' => __('导出邀请码', 'aksrt'), 'dialog' => 'aksrt-inv-export', 'icon' => 'dashicons-download'],
|
||||
]
|
||||
);
|
||||
|
||||
// 功能未启用提示(置顶橙色提醒)。
|
||||
$settings = $service->registration_settings();
|
||||
if (empty($settings['enabled'])) {
|
||||
$settings_url = admin_url('admin.php?page=aksrt-settings');
|
||||
echo '<div class="notice notice-warning"><p>'
|
||||
. esc_html__('注意:邀请码功能暂未启用。您可以在', 'aksrt')
|
||||
. ' <a href="' . esc_url($settings_url) . '">' . esc_html__('登录注册设置', 'aksrt') . '</a> '
|
||||
. esc_html__('中启用此功能', 'aksrt')
|
||||
. '</p></div>';
|
||||
$settings_url = admin_url('admin.php?page=aksrt-settings#/users/auth');
|
||||
echo '<div class="notice notice-warning"><p>' . esc_html__('邀请码注册尚未启用,已添加的邀请码暂不能用于注册。', 'aksrt')
|
||||
. ' <a href="' . esc_url($settings_url) . '">' . esc_html__('前往登录注册设置', 'aksrt') . '</a></p></div>';
|
||||
}
|
||||
|
||||
// 操作结果回跳提示:标题 + 说明,成功 / 错误两种形态。
|
||||
$inv_title = isset($_GET['aksrt_inv_title']) ? sanitize_text_field(rawurldecode((string) wp_unslash($_GET['aksrt_inv_title']))) : '';
|
||||
$inv_msg = isset($_GET['aksrt_inv_msg']) ? sanitize_text_field(rawurldecode((string) wp_unslash($_GET['aksrt_inv_msg']))) : '';
|
||||
$inv_type = isset($_GET['aksrt_inv_type']) && $_GET['aksrt_inv_type'] === 'error' ? 'error' : 'success';
|
||||
if ($inv_title !== '') {
|
||||
echo '<div class="notice notice-' . esc_attr($inv_type) . ' is-dismissible"><h3>' . esc_html($inv_title) . '</h3>'
|
||||
. ($inv_msg !== '' ? '<p>' . esc_html($inv_msg) . '</p>' : '') . '</div>';
|
||||
}
|
||||
|
||||
if ($tab === '') {
|
||||
$this->render_invite_codes_list($service, $this_url, $nonce);
|
||||
} elseif ($tab === 'add') {
|
||||
$this->render_invite_codes_add($this_url, $nonce);
|
||||
} else {
|
||||
$this->render_invite_codes_export($this_url, $nonce);
|
||||
$title = isset($_GET['aksrt_inv_title']) ? sanitize_text_field(rawurldecode((string) wp_unslash($_GET['aksrt_inv_title']))) : '';
|
||||
$message = isset($_GET['aksrt_inv_msg']) ? sanitize_text_field(rawurldecode((string) wp_unslash($_GET['aksrt_inv_msg']))) : '';
|
||||
$type = ($_GET['aksrt_inv_type'] ?? '') === 'error' ? 'error' : 'success';
|
||||
if ($title !== '') {
|
||||
echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p><strong>' . esc_html($title) . '</strong>'
|
||||
. ($message !== '' ? ' ' . esc_html($message) : '') . '</p></div>';
|
||||
}
|
||||
|
||||
$this->render_invite_codes_list($service, $this_url, $nonce);
|
||||
$this->render_invite_codes_add($nonce);
|
||||
$this->render_invite_codes_export($nonce);
|
||||
echo '<div class="aksrt-inv-toast" role="status" aria-live="polite" data-aksrt-inv-toast'
|
||||
. ' data-copied="' . esc_attr__('邀请码已复制', 'aksrt') . '"'
|
||||
. ' data-copy-failed="' . esc_attr__('自动复制失败,请选中邀请码后手动复制。', 'aksrt') . '"'
|
||||
. ' data-exported="' . esc_attr__('下载请求已提交,请查看浏览器下载列表。', 'aksrt') . '"></div>';
|
||||
NativeUi::page_close();
|
||||
?>
|
||||
<div class="aksrt-inv-toast" id="aksrt-inv-toast"><?php esc_html_e('复制成功', 'aksrt'); ?></div>
|
||||
<script>
|
||||
(() => {
|
||||
// 依赖切换:添加方式 / 高级选项 / VIP 时长。
|
||||
const sync = () => {
|
||||
document.querySelectorAll('[data-aksrt-inv-dep]').forEach((el) => {
|
||||
const [name, value] = el.dataset.aksrtInvDep.split('=');
|
||||
const input = document.querySelector(`[name="${name}"]:checked`) || document.querySelector(`[name="${name}"]`);
|
||||
const show = input ? String(input.type === 'checkbox' ? (input.checked ? '1' : '') : input.value) === value : false;
|
||||
el.style.display = show ? '' : 'none';
|
||||
});
|
||||
document.querySelectorAll('.aksrt-inv-btnset').forEach((group) => {
|
||||
group.querySelectorAll('label').forEach((label) => label.classList.toggle('on', label.querySelector('input').checked));
|
||||
});
|
||||
};
|
||||
document.addEventListener('change', sync);
|
||||
// 点击复制邀请码,悬浮提示结果。
|
||||
const toast = document.getElementById('aksrt-inv-toast');
|
||||
document.addEventListener('click', async (event) => {
|
||||
const target = event.target.closest('[data-clipboard-text]');
|
||||
if (!target) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(target.dataset.clipboardText);
|
||||
if (toast) { toast.style.display = 'block'; setTimeout(() => { toast.style.display = 'none'; }, 1200); }
|
||||
} catch (error) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
const selection = getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
});
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', sync, { once: true });
|
||||
} else {
|
||||
sync();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/** 列表视图:状态筛选、搜索、批量删除、排序、分页。 */
|
||||
private function render_invite_codes_list(\AKSRT\Payment\InviteCode $service, string $this_url, string $nonce): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- 只读列表查询。
|
||||
$status = in_array(($_GET['status'] ?? 'all'), ['all', 'unused', 'used'], true) ? (string) $_GET['status'] : 'all';
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- 只读列表查询。
|
||||
$s = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- 只读列表查询。
|
||||
$remark = isset($_GET['remark']) ? sanitize_text_field((string) wp_unslash($_GET['remark'])) : '';
|
||||
$data = $service->list_codes([
|
||||
'page' => isset($_GET['paged']) ? max(1, (int) $_GET['paged']) : 1, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
'page' => isset($_GET['paged']) ? max(1, (int) $_GET['paged']) : 1,
|
||||
'status' => $status,
|
||||
's' => $s,
|
||||
'remark' => $remark,
|
||||
'orderby' => isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
'desc' => isset($_GET['desc']) ? sanitize_key((string) wp_unslash($_GET['desc'])) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
'orderby' => isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : '',
|
||||
'desc' => isset($_GET['desc']) ? sanitize_key((string) wp_unslash($_GET['desc'])) : '',
|
||||
]);
|
||||
$filters = ['status' => $status, 's' => $s, 'remark' => $remark, 'orderby' => $data['orderby'], 'desc' => $data['desc']];
|
||||
$filter_url = add_query_arg($filters, $this_url);
|
||||
|
||||
// 页头 + 三视图切换(WP 原生 page-title-action:列表 / 添加 / 导出)。
|
||||
NativeUi::header(
|
||||
__('邀请码管理', 'aksrt'),
|
||||
'',
|
||||
'dashicons-tickets-alt'
|
||||
);
|
||||
printf(
|
||||
'<div class="aksrt-inv-switch-views"><a href="%s" class="page-title-action">%s</a> <a href="%s" class="page-title-action">%s</a></div>',
|
||||
esc_url(add_query_arg('tab', 'add', $this_url)),
|
||||
esc_html__('添加邀请码', 'aksrt'),
|
||||
esc_url(add_query_arg('tab', 'export', $this_url)),
|
||||
esc_html__('导出邀请码', 'aksrt')
|
||||
);
|
||||
|
||||
// 顶部筛选行:左侧状态链接(搜索时改为提示语),右侧搜索表单。
|
||||
NativeUi::card_open('', '', false, 'aksrt-native-toolbar');
|
||||
if ($s !== '') {
|
||||
echo '<ul class="subsubsub"><li>' . esc_html(sprintf(__('搜索 “%s” 的内容', 'aksrt'), $s)) . '</li></ul>';
|
||||
} else {
|
||||
$links = [
|
||||
'all' => __('全部', 'aksrt'),
|
||||
'unused' => __('未使用', 'aksrt'),
|
||||
'used' => __('已使用', 'aksrt'),
|
||||
];
|
||||
echo '<ul class="subsubsub">';
|
||||
foreach ($links as $key => $label) {
|
||||
$url = $key === 'all' ? $this_url : add_query_arg('status', $key, $this_url);
|
||||
echo '<li><a href="' . esc_url($url) . '"' . ($key === $status ? ' class="current"' : '') . '>' . esc_html($label) . '</a></li>';
|
||||
NativeUi::card_open('', '', false, 'aksrt-inv-toolbar');
|
||||
echo '<div class="aksrt-native-toolbar"><ul class="subsubsub" aria-label="' . esc_attr__('使用状态筛选', 'aksrt') . '">';
|
||||
foreach (['all' => __('全部', 'aksrt'), 'unused' => __('未使用', 'aksrt'), 'used' => __('已使用', 'aksrt')] as $key => $label) {
|
||||
$url = add_query_arg('status', $key, $filter_url);
|
||||
echo '<li><a href="' . esc_url($url) . '"' . ($key === $status ? ' class="current" aria-current="page"' : '') . '>' . esc_html($label) . '</a></li>';
|
||||
}
|
||||
echo '</ul><form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_invite_codes">';
|
||||
foreach (array_diff_key($filters, ['s' => '']) as $key => $value) {
|
||||
echo '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr($value) . '">';
|
||||
}
|
||||
echo '<label class="screen-reader-text" for="aksrt-inv-search">' . esc_html__('搜索邀请码或标识', 'aksrt') . '</label>';
|
||||
echo '<input type="search" id="aksrt-inv-search" name="s" value="' . esc_attr($s) . '" placeholder="' . esc_attr__('搜索邀请码或标识', 'aksrt') . '">';
|
||||
echo '<button type="submit" class="button">' . esc_html__('搜索', 'aksrt') . '</button></form></div>';
|
||||
if ($s !== '' || $remark !== '' || $status !== 'all') {
|
||||
echo '<div class="aksrt-inv-filter-summary">';
|
||||
if ($s !== '') {
|
||||
echo '<span>' . esc_html(sprintf(__('关键词:%s', 'aksrt'), $s)) . '</span>';
|
||||
}
|
||||
echo '</ul>';
|
||||
if ($remark !== '') {
|
||||
echo '<span>' . esc_html(sprintf(__('标识:%s', 'aksrt'), $remark)) . '</span>';
|
||||
}
|
||||
echo '<a href="' . esc_url($this_url) . '">' . esc_html__('清除筛选', 'aksrt') . '</a></div>';
|
||||
}
|
||||
echo '<form method="get" class="aksrt-native-filters"><input type="hidden" name="page" value="aksrt_invite_codes">';
|
||||
echo '<input type="search" name="s" value="' . esc_attr($s) . '" placeholder="' . esc_attr__('搜索邀请码', 'aksrt') . '">';
|
||||
echo '<button type="submit" class="button">' . esc_html__('提交', 'aksrt') . '</button></form>';
|
||||
NativeUi::card_close();
|
||||
|
||||
// 可排序列:code / created_at / updated_at / used_count / remark。
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" data-aksrt-inv-list'
|
||||
. ' data-confirm="' . esc_attr__('确定删除选中的 %d 个邀请码及其使用记录?此操作不可撤销。', 'aksrt') . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_invite_delete"><input type="hidden" name="aksrt_nonce" value="' . esc_attr($nonce) . '">';
|
||||
$bulk = '<div class="aksrt-native-inline"><span class="aksrt-inv-selection" data-aksrt-inv-selection data-label="' . esc_attr__('已选 %d 项', 'aksrt') . '" aria-live="polite"></span>'
|
||||
. '<label class="screen-reader-text" for="bulk-action-selector-top">' . esc_html__('批量操作', 'aksrt') . '</label>'
|
||||
. '<select name="bulk_action" id="bulk-action-selector-top"><option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option><option value="delete">' . esc_html__('删除', 'aksrt') . '</option></select>'
|
||||
. '<button type="submit" class="button" data-aksrt-inv-bulk disabled>' . esc_html__('应用', 'aksrt') . '</button></div>';
|
||||
NativeUi::table_card_open(__('邀请码列表', 'aksrt'), $bulk);
|
||||
echo '<table class="widefat fixed striped aksrt-inv-table"><thead><tr><td class="manage-column column-cb check-column">';
|
||||
echo '<input type="checkbox" data-aksrt-inv-check-all aria-label="' . esc_attr__('全选本页邀请码', 'aksrt') . '"></td>';
|
||||
$columns = [
|
||||
['width' => '6%', 'key' => 'code', 'name' => __('邀请码', 'aksrt')],
|
||||
['width' => '10%', 'key' => '', 'name' => __('使用奖励', 'aksrt')],
|
||||
['width' => '10%', 'key' => 'created_at', 'name' => __('创建时间', 'aksrt')],
|
||||
['width' => '10%', 'key' => 'updated_at', 'name' => __('更新时间', 'aksrt')],
|
||||
['width' => '6%', 'key' => 'used_count', 'name' => __('状态', 'aksrt')],
|
||||
['width' => '12%', 'key' => 'remark', 'name' => __('标识', 'aksrt')],
|
||||
'code' => __('邀请码', 'aksrt'), 'rewards' => __('使用奖励', 'aksrt'),
|
||||
'created_at' => __('创建时间', 'aksrt'), 'updated_at' => __('更新时间', 'aksrt'),
|
||||
'used_count' => __('使用状态', 'aksrt'), 'remark' => __('标识', 'aksrt'),
|
||||
];
|
||||
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_invite_delete">';
|
||||
echo '<input type="hidden" name="aksrt_nonce" value="' . esc_attr($nonce) . '">';
|
||||
|
||||
$bulk = '<div class="bulkactions aksrt-native-inline">
|
||||
<select name="bulk_action" id="bulk-action-selector-top">
|
||||
<option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option>
|
||||
<option value="delete">' . esc_html__('删除', 'aksrt') . '</option>
|
||||
</select>
|
||||
<button type="submit" class="button action" onclick="return this.form.bulk_action.value===\'delete\'||confirm(' . esc_attr(wp_json_encode(__('请先选择“删除”操作', 'aksrt'))) . ');">' . esc_html__('应用', 'aksrt') . '</button>
|
||||
</div>';
|
||||
|
||||
NativeUi::table_card_open(__('邀请码列表', 'aksrt'), $bulk); // phpcs:ignore WordPress.Security.EscapeOutput -- actions pre-escaped.
|
||||
echo '<table class="widefat fixed striped"><thead><tr>';
|
||||
echo '<td id="cb" class="manage-column column-cb check-column"><input id="cb-select-all-1" type="checkbox" onclick="document.querySelectorAll(\'tbody [name="action_id[]"]\').forEach(c=>c.checked=this.checked)"></td>';
|
||||
foreach ($columns as $column) {
|
||||
$arrows = '';
|
||||
if ($column['key'] !== '') {
|
||||
$up = add_query_arg(['orderby' => $column['key'], 'desc' => 'ASC'], $this_url);
|
||||
$down = add_query_arg(['orderby' => $column['key'], 'desc' => 'DESC'], $this_url);
|
||||
$arrows = '<span class="orderby-but"><a title="' . esc_attr__('降序', 'aksrt') . '" href="' . esc_url($down) . '"><span class="dashicons dashicons-arrow-up"></span></a>'
|
||||
. '<a title="' . esc_attr__('升序', 'aksrt') . '" href="' . esc_url($up) . '"><span class="dashicons dashicons-arrow-down"></span></a></span>';
|
||||
foreach ($columns as $key => $label) {
|
||||
$active = $key === $data['orderby'];
|
||||
$sort = $data['desc'] === 'ASC' ? 'ascending' : 'descending';
|
||||
echo '<th scope="col" class="column-' . esc_attr($key) . '"' . ($active ? ' aria-sort="' . esc_attr($sort) . '"' : '') . '>';
|
||||
if ($key === 'rewards') {
|
||||
echo esc_html($label);
|
||||
} else {
|
||||
$next = $active && $data['desc'] === 'ASC' ? 'DESC' : 'ASC';
|
||||
$url = add_query_arg(['orderby' => $key, 'desc' => $next], $filter_url);
|
||||
$icon = $active && $data['desc'] === 'ASC' ? 'dashicons-arrow-up-alt2' : 'dashicons-arrow-down-alt2';
|
||||
echo '<a class="orderby-but' . ($active ? ' is-active' : '') . '" href="' . esc_url($url) . '" title="' . esc_attr($next === 'ASC' ? __('按此列升序排列', 'aksrt') : __('按此列降序排列', 'aksrt')) . '">'
|
||||
. esc_html($label) . '<span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span></a>';
|
||||
}
|
||||
echo '<th>' . esc_html($column['name']) . $arrows . '</th>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 箭头为静态结构。
|
||||
echo '</th>';
|
||||
}
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if ($data['items'] === []) {
|
||||
echo NativeUi::empty_row(7, __('未找到对应内容,或暂无内容', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
} else {
|
||||
foreach ($data['items'] as $item) {
|
||||
$used = (int) $item['used_count'];
|
||||
$max_uses = (int) $item['max_uses'];
|
||||
echo '<tr>';
|
||||
echo '<th scope="row" class="check-column"><input type="checkbox" name="action_id[]" value="' . esc_attr((string) $item['id']) . '"></th>';
|
||||
echo '<td><span class="aksrt-inv-code aksrt-native-mono" data-clipboard-text="' . esc_attr($item['code']) . '" title="' . esc_attr__('点击复制', 'aksrt') . '">' . esc_html($item['code']) . '</span></td>';
|
||||
echo '<td>' . wp_kses((string) $item['reward_text'], ['br' => []]) . '</td>';
|
||||
echo '<td>' . esc_html($item['created_at']) . '</td>';
|
||||
echo '<td>' . esc_html($item['updated_at']) . '</td>';
|
||||
if ($used === 0) {
|
||||
echo '<td>' . NativeUi::badge(__('未使用', 'aksrt'), 'info') . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
} else {
|
||||
$label = $max_uses > 0 && $used < $max_uses ? sprintf(__('已使用 %1$d/%2$d', 'aksrt'), $used, $max_uses) : __('已使用', 'aksrt');
|
||||
echo '<td>' . NativeUi::badge($label, 'danger'); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
foreach ((array) $item['recent_users'] as $user) {
|
||||
$user_link = get_edit_user_link((int) $user['user_id']);
|
||||
echo '<div class="aksrt-native-muted"><a target="_blank" href="' . esc_url($user_link) . '">' . esc_html($user['name']) . '</a></div>';
|
||||
}
|
||||
echo '</td>';
|
||||
}
|
||||
echo '<td>';
|
||||
if ($item['remark'] !== '') {
|
||||
echo '<a href="' . esc_url(add_query_arg('remark', $item['remark'], $this_url)) . '">' . esc_html($item['remark']) . '</a>';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
echo NativeUi::empty_row(7, __('暂无符合条件的邀请码', 'aksrt'), __('调整筛选条件,或点击“添加邀请码”生成、导入新的邀请码。', 'aksrt'));
|
||||
}
|
||||
foreach ($data['items'] as $item) {
|
||||
$used = (int) $item['used_count'];
|
||||
$max_uses = (int) $item['max_uses'];
|
||||
echo '<tr><th scope="row" class="check-column"><input type="checkbox" name="action_id[]" value="' . esc_attr((string) $item['id']) . '" aria-label="' . esc_attr(sprintf(__('选择邀请码 %s', 'aksrt'), $item['code'])) . '"></th>';
|
||||
echo '<td><button type="button" class="aksrt-inv-code aksrt-native-mono" data-clipboard-text="' . esc_attr($item['code']) . '" aria-label="' . esc_attr(sprintf(__('复制邀请码 %s', 'aksrt'), $item['code'])) . '">' . esc_html($item['code']) . '<span class="dashicons dashicons-admin-page" aria-hidden="true"></span></button></td>';
|
||||
echo '<td>' . wp_kses((string) $item['reward_text'], ['br' => []]) . '</td>';
|
||||
echo '<td class="aksrt-inv-date">' . esc_html($item['created_at']) . '</td><td class="aksrt-inv-date">' . esc_html($item['updated_at']) . '</td><td>';
|
||||
$label = $used > 0 && $max_uses > 0 && $used < $max_uses ? sprintf(__('已使用 %1$d/%2$d', 'aksrt'), $used, $max_uses) : ($used > 0 ? __('已使用', 'aksrt') : __('未使用', 'aksrt'));
|
||||
echo NativeUi::badge($label, $used > 0 ? 'neutral' : 'success');
|
||||
foreach ((array) $item['recent_users'] as $user) {
|
||||
echo '<div class="aksrt-inv-user"><a href="' . esc_url(get_edit_user_link((int) $user['user_id'])) . '">' . esc_html($user['name']) . '</a></div>';
|
||||
}
|
||||
echo '</td><td>';
|
||||
if ($item['remark'] !== '') {
|
||||
echo '<a href="' . esc_url(add_query_arg('remark', $item['remark'], $this_url)) . '">' . esc_html($item['remark']) . '</a>';
|
||||
} else {
|
||||
echo '<span class="aksrt-native-muted">—</span>';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
NativeUi::table_wrap_close();
|
||||
|
||||
// 分页条:总数 + 页码链接。
|
||||
$total = (int) $data['total'];
|
||||
$max_pages = (int) $data['max_pages'];
|
||||
echo '<div class="tablenav bottom"><div class="tablenav-pages">';
|
||||
echo '<span class="displaying-num">' . esc_html(sprintf(__('共 %s 项', 'aksrt'), number_format_i18n($total))) . '</span>';
|
||||
if ($max_pages > 1) {
|
||||
echo '<div class="tablenav bottom"><div class="tablenav-pages"><span class="displaying-num">' . esc_html(sprintf(__('共 %s 项', 'aksrt'), number_format_i18n((int) $data['total']))) . '</span>';
|
||||
if ((int) $data['max_pages'] > 1) {
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'current' => (int) $data['page'],
|
||||
'total' => $max_pages,
|
||||
'type' => 'plain',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
]); // phpcs:ignore WordPress.Security.EscapeOutput -- paginate_links 自行转义。
|
||||
'base' => add_query_arg('paged', '%#%', $filter_url), 'format' => '',
|
||||
'current' => (int) $data['page'], 'total' => (int) $data['max_pages'],
|
||||
'type' => 'plain', 'prev_text' => '«', 'next_text' => '»',
|
||||
]);
|
||||
}
|
||||
echo '</div></div>';
|
||||
NativeUi::card_close();
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
/** 添加视图:系统自动生成 / 文本导入 二选一。 */
|
||||
private function render_invite_codes_add(string $this_url, string $nonce): void {
|
||||
NativeUi::header(
|
||||
__('添加邀请码', 'aksrt'),
|
||||
'',
|
||||
'dashicons-tickets-alt'
|
||||
);
|
||||
printf(
|
||||
'<div class="aksrt-inv-switch-views"><a href="%s" class="page-title-action"><span class="dashicons dashicons-arrow-left-alt" aria-hidden="true"></span>%s</a></div>',
|
||||
esc_url($this_url),
|
||||
esc_html__('返回列表', 'aksrt')
|
||||
);
|
||||
|
||||
NativeUi::card_open(__('添加邀请码', 'aksrt'));
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_invite_add">';
|
||||
echo '<input type="hidden" name="aksrt_nonce" value="' . esc_attr($nonce) . '">';
|
||||
|
||||
echo '<div class="aksrt-native-callout"><p><b>' . esc_html__('在此添加邀请码', 'aksrt') . '</b></p><ul>'
|
||||
. '<li>' . esc_html__('如果您已经准备好了邀请码资料,请选择导入的方式添加', 'aksrt') . '</li>'
|
||||
. '<li>' . esc_html__('您也可以采用系统生成的方式,自动批量添加邀请码', 'aksrt') . '</li></ul></div>';
|
||||
|
||||
// 添加方式(按钮组二选一)。
|
||||
echo '<div class="aksrt-inv-field"><label>' . esc_html__('添加方式', 'aksrt') . '</label><div class="aksrt-inv-control"><div class="aksrt-inv-btnset">'
|
||||
. '<label><input type="radio" name="add_type" value="auto" checked>' . esc_html__('系统自动生成', 'aksrt') . '</label>'
|
||||
. '<label><input type="radio" name="add_type" value="import">' . esc_html__('导入邀请码', 'aksrt') . '</label>'
|
||||
. '</div></div></div>';
|
||||
|
||||
// 自动生成区。
|
||||
echo '<div data-aksrt-inv-dep="add_type=auto">';
|
||||
echo '<div class="aksrt-inv-field"><label for="aksrt-inv-num">' . esc_html__('生成数量', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<input type="number" id="aksrt-inv-num" name="auto_num" value="20" min="1" max="1000" step="10" class="small-text"><span class="aksrt-inv-unit">' . esc_html__('个', 'aksrt') . '</span>'
|
||||
. '<span class="description">' . esc_html__('需要生成多少个邀请码(单次生成数量太多可能会对服务器性能造成影响)', 'aksrt') . '</span></div></div>';
|
||||
echo '<div class="aksrt-inv-field"><label for="aksrt-inv-remarks">' . esc_html__('标识', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<input type="text" id="aksrt-inv-remarks" name="auto_remarks" value="' . esc_attr('invita_' . current_time('YmdHis')) . '" class="regular-text">'
|
||||
. '<span class="description">' . esc_html__('对生成的邀请码做标记标识,方便后期查找管理', 'aksrt') . '</span></div></div>';
|
||||
$this->render_invite_reward_fields();
|
||||
echo '<div class="aksrt-inv-field"><label for="aksrt-inv-top">' . esc_html__('高级选项', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<label class="aksrt-inv-switch"><input type="checkbox" id="aksrt-inv-top" name="auto_top_s" value="1"><span>' . esc_html__('开启', 'aksrt') . '</span></label></div></div>';
|
||||
echo '<div data-aksrt-inv-dep="auto_top_s=1" style="display:none;">';
|
||||
echo '<div class="aksrt-inv-field"><label for="aksrt-inv-len">' . esc_html__('自定义位数', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<input type="number" id="aksrt-inv-len" name="auto_rand_password_limit" value="8" min="1" max="50" step="2" class="small-text"><span class="aksrt-inv-unit">' . esc_html__('位数', 'aksrt') . '</span>'
|
||||
. '<span class="description">' . esc_html__('自定义自动生成的长度(不能太短,太短可能会出现重复)', 'aksrt') . '</span></div></div>';
|
||||
echo '</div></div>';
|
||||
|
||||
// 导入区。
|
||||
echo '<div data-aksrt-inv-dep="add_type=import" style="display:none;">';
|
||||
echo '<div class="aksrt-native-callout"><p><b>' . esc_html__('导入邀请码', 'aksrt') . '</b></p><ul>'
|
||||
. '<li>' . esc_html__('一行一个邀请码,单行格式为:', 'aksrt') . '<code>' . esc_html__('邀请码 奖励 标识', 'aksrt') . '</code></li>'
|
||||
. '<li>' . esc_html__('邀请码、奖励、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>'
|
||||
. '<li>' . esc_html__('奖励格式为:', 'aksrt') . '<code>level_integral=10&points=20&balance=30&vip=1&vip_time=5</code>,' . esc_html__('无需奖励填', 'aksrt') . ' <code>' . esc_html__('无', 'aksrt') . '</code></li>'
|
||||
. '<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li></ul>'
|
||||
. '<p><b>' . esc_html__('数据示例', 'aksrt') . '</b><br>'
|
||||
. '<code>93FW0XAY ' . esc_html__('无', 'aksrt') . '</code> ' . esc_html__('没有奖励', 'aksrt') . '<br>'
|
||||
. '<code>P86NPWKI level_integral=100</code> ' . esc_html__('奖励100经验值', 'aksrt') . '<br>'
|
||||
. '<code>P86NPWKI points=10</code> ' . esc_html__('奖励10积分', 'aksrt') . '<br>'
|
||||
. '<code>auF9D2B4 balance=30&vip=1&vip_time=5</code> ' . esc_html__('奖励余额30和5天1级会员', 'aksrt') . '</p></div>';
|
||||
echo '<div class="aksrt-inv-field"><label for="aksrt-import-data">' . esc_html__('邀请码数据', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<textarea id="aksrt-import-data" name="import_data" rows="10" class="large-text code aksrt-inv-textarea"></textarea></div></div>';
|
||||
echo '<div class="aksrt-inv-field"><label for="aksrt-import-division">' . esc_html__('自定义分隔符号', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<input type="text" id="aksrt-import-division" name="import_division" value=" " maxlength="4" class="aksrt-inv-mini">'
|
||||
. '<span class="description">' . esc_html__('邀请码和奖励之间分割符号(默认为空格分割)', 'aksrt') . '</span></div></div>';
|
||||
echo '</div>';
|
||||
|
||||
echo '<div class="aksrt-inv-field"><label></label><div class="aksrt-inv-control">'
|
||||
. '<button type="submit" class="button button-primary">' . esc_html__('确认提交', 'aksrt') . '</button></div></div>';
|
||||
echo '</form>';
|
||||
NativeUi::card_close();
|
||||
}
|
||||
/** 使用奖励字段集:经验值 / 积分 / 余额 / VIP 会员与赠送时长。 */
|
||||
private function render_invite_reward_fields(): void {
|
||||
echo '<div class="aksrt-inv-field"><label>' . esc_html__('使用奖励', 'aksrt') . '</label><div class="aksrt-inv-control">';
|
||||
echo '<div class="aksrt-inv-rewards">'
|
||||
. '<span><label>' . esc_html__('经验值', 'aksrt') . ' <input type="number" name="level_integral" value="0" min="0" class="small-text"></label></span>'
|
||||
. '<span><label>' . esc_html__('积分', 'aksrt') . ' <input type="number" name="points" value="0" min="0" class="small-text"></label></span>'
|
||||
. '<span><label>' . esc_html__('余额', 'aksrt') . ' <input type="number" name="balance" value="0" min="0" step="0.01" class="small-text"> ' . esc_html__('元', 'aksrt') . '</label></span>'
|
||||
. '</div>';
|
||||
echo '<div class="aksrt-inv-vip"><label>' . esc_html__('VIP会员', 'aksrt') . ' '
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="vip" value="" checked>' . esc_html__('无', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="vip" value="1">' . esc_html__('一级', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="vip" value="2">' . esc_html__('二级', 'aksrt') . '</label>'
|
||||
. '</label></div>';
|
||||
echo '<div class="aksrt-inv-vip" data-aksrt-inv-dep="vip=1" style="display:none;"><label>' . esc_html__('会员赠送时长', 'aksrt')
|
||||
. ' <input type="number" name="vip_time" value="0" min="0" class="small-text"> <span class="aksrt-inv-unit">' . esc_html__('天', 'aksrt') . '</span></label>'
|
||||
. '<span class="description">' . esc_html__('仅在上方选择 VIP 会员等级后生效', 'aksrt') . '</span></div>';
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
/** 导出视图:状态 / 格式 选择后提交下载。 */
|
||||
private function render_invite_codes_export(string $this_url, string $nonce): void {
|
||||
NativeUi::header(
|
||||
__('导出邀请码', 'aksrt'),
|
||||
'',
|
||||
'dashicons-download'
|
||||
);
|
||||
printf(
|
||||
'<div class="aksrt-inv-switch-views"><a href="%s" class="page-title-action"><span class="dashicons dashicons-arrow-left-alt" aria-hidden="true"></span>%s</a></div>',
|
||||
esc_url($this_url),
|
||||
esc_html__('返回列表', 'aksrt')
|
||||
);
|
||||
|
||||
NativeUi::card_open();
|
||||
echo '<form method="get" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_invite_export">';
|
||||
echo '<input type="hidden" name="aksrt_nonce" value="' . esc_attr($nonce) . '">';
|
||||
|
||||
echo '<div class="aksrt-inv-field"><label>' . esc_html__('选择状态', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="status" value="all" checked>' . esc_html__('全部', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="status" value="unused">' . esc_html__('未使用', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="status" value="used">' . esc_html__('已使用', 'aksrt') . '</label>'
|
||||
. '</div></div>';
|
||||
echo '<div class="aksrt-inv-field"><label>' . esc_html__('导出格式', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="export_format" value="text">' . esc_html__('文本文档', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="export_format" value="xls" checked>' . esc_html__('Excel表格', 'aksrt') . '</label>'
|
||||
. '</div></div>';
|
||||
echo '<div data-aksrt-inv-dep="export_format=text" style="display:none;">';
|
||||
echo '<div class="aksrt-inv-field"><label>' . esc_html__('分隔符号', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<input type="text" name="text_division" value=" " maxlength="4" class="aksrt-inv-mini">'
|
||||
. '<span class="description">' . esc_html__('邀请码和奖励之间分割符号(默认为空格分割)', 'aksrt') . '</span></div></div>';
|
||||
echo '</div>';
|
||||
echo '<div class="aksrt-inv-field"><label></label><div class="aksrt-inv-control">'
|
||||
. '<button type="submit" class="button button-primary">' . esc_html__('确认提交', 'aksrt') . '</button></div></div>';
|
||||
echo '</form>';
|
||||
NativeUi::card_close();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user