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>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-30 15:05:38 +08:00
1 parent aa692f3c9f
commit 18951b01d6
29 files changed
+2503 -1593

No files matched your search

+26 -15
View File
@@ -15,7 +15,8 @@ trait UsersMenusVip {
$this->ensure_cap();
$tab = $this->vip_tab();
$page = max(1, absint($_GET['paged'] ?? 1));
echo '<div class="wrap"><h1>' . esc_html__('用户会员', 'aksrt') . '</h1>';
NativeUi::page_open();
NativeUi::header(__('用户会员', 'aksrt'), '', 'dashicons-awards');
$this->flash();
$this->render_vip_tabs($tab);
if ($tab === 'history') {
@@ -23,12 +24,17 @@ trait UsersMenusVip {
} else {
$filters = $this->vip_filters();
$result = $this->query_vip_users($filters, $page, 20);
NativeUi::card_open('', '', false, 'aksrt-native-toolbar');
$this->render_vip_filters($filters);
NativeUi::card_close();
$this->render_vip_bulk_form($filters);
NativeUi::table_card_open(__('会员列表', 'aksrt'));
$this->render_vip_table($result['items'], $filters);
NativeUi::table_wrap_close();
$this->pagination($result['pages'], $page);
NativeUi::card_close();
}
echo '</div>';
NativeUi::page_close();
}
public function handle_mutate_vip(): void {
@@ -165,37 +171,38 @@ trait UsersMenusVip {
private function render_vip_filters(array $filters): void {
$names = $this->vip_level_names();
echo '<form method="get" style="margin:12px 0;display:flex;gap:8px;align-items:end;flex-wrap:wrap"><input type="hidden" name="page" value="aksrt_user_vip">';
echo '<label>UID<br><input type="number" min="1" name="user_id" value="' . esc_attr((string) $filters['user_id']) . '"></label><label>' . esc_html__('档位', 'aksrt') . '<br><select name="level">';
echo '<form method="get" class="aksrt-native-filters"><input type="hidden" name="page" value="aksrt_user_vip">';
echo '<label>UID<input type="number" min="1" name="user_id" value="' . esc_attr((string) $filters['user_id']) . '"></label><label>' . esc_html__('档位', 'aksrt') . '<select name="level">';
foreach (['all' => __('全部', 'aksrt'), '0' => __('普通用户', 'aksrt'), '1' => $names[1], '2' => $names[2]] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($filters['level'], $key, false), esc_html($label));
}
echo '</select></label><label>' . esc_html__('状态', 'aksrt') . '<br><select name="state">';
echo '</select></label><label>' . esc_html__('状态', 'aksrt') . '<select name="state">';
foreach (['all' => __('全部', 'aksrt'), 'active' => __('有效', 'aksrt'), 'permanent' => __('永久', 'aksrt'), 'expired' => __('已过期', 'aksrt'), 'none' => __('无会员', 'aksrt')] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($filters['state'], $key, false), esc_html($label));
}
echo '</select></label><label>' . esc_html__('到期从', 'aksrt') . '<br><input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '"></label><label>' . esc_html__('到期至', 'aksrt') . '<br><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '"></label>';
echo '</select></label><label>' . esc_html__('到期从', 'aksrt') . '<input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '"></label><label>' . esc_html__('到期至', 'aksrt') . '<input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '"></label>';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
}
private function render_vip_bulk_form(array $filters): void {
$names = $this->vip_level_names();
echo '<form id="aksrt-vip-bulk" method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:14px 0;padding:12px;background:#fff;border:1px solid #ccd0d4;display:flex;align-items:center;gap:8px;flex-wrap:wrap">';
NativeUi::card_open(__('批量操作', 'aksrt'));
echo '<form id="aksrt-vip-bulk" method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline">';
echo '<input type="hidden" name="action" value="aksrt_users_mutate_vip">';
wp_nonce_field('aksrt_users_mutate_vip');
$this->render_vip_return_fields($filters);
echo '<strong>' . esc_html__('批量操作', 'aksrt') . '</strong> ';
$this->render_vip_operation_fields($names);
echo ' <label style="display:inline-flex;align-items:center;gap:4px;min-width:0;flex:1 1 240px">' . esc_html__('原因', 'aksrt') . ' <input required name="reason" class="regular-text" style="width:100%;max-width:360px;min-width:0;box-sizing:border-box"></label> ';
echo ' <label>' . esc_html__('原因', 'aksrt') . ' <input required name="reason" class="regular-text"></label> ';
submit_button(__('应用到选中用户', 'aksrt'), 'primary', '', false);
echo '</form>';
NativeUi::card_close();
}
private function render_vip_table(array $items, array $filters): void {
echo '<table class="wp-list-table widefat fixed striped"><thead><tr><th><input type="checkbox" aria-label="' . esc_attr__('选择全部用户', 'aksrt') . '" onclick="document.querySelectorAll(\'.aksrt-vip-check\').forEach((element) => element.checked = this.checked)"></th><th>UID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('当前会员', 'aksrt') . '</th><th>' . esc_html__('到期时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th></tr></thead><tbody>';
echo '<table class="wp-list-table widefat fixed striped"><thead><tr><td class="manage-column column-cb check-column"><input type="checkbox" aria-label="' . esc_attr__('选择全部用户', 'aksrt') . '" onclick="document.querySelectorAll(\'.aksrt-vip-check\').forEach((element) => element.checked = this.checked)"></td><th>UID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('当前会员', 'aksrt') . '</th><th>' . esc_html__('到期时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th></tr></thead><tbody>';
if ($items === []) {
echo '<tr><td colspan="6">' . esc_html__('暂无用户', 'aksrt') . '</td></tr>';
echo NativeUi::empty_row(6, __('暂无用户', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
}
foreach ($items as $row) {
$this->render_vip_row($row, $filters);
@@ -215,11 +222,12 @@ trait UsersMenusVip {
if ((int) $row['raw_level'] !== $level || (int) $row['raw_expires'] !== $expires) {
$label .= ' (' . __('基础档位恢复后', 'aksrt') . ')';
}
echo '<tr><td><input class="aksrt-vip-check" form="aksrt-vip-bulk" type="checkbox" name="user_ids[]" value="' . $id . '"></td><td>' . $id . '</td><td><a href="' . esc_url(get_edit_user_link($id)) . '">' . esc_html((string) $row['display_name']) . '</a><div class="description">@' . esc_html((string) $row['user_login']) . '</div></td><td>' . esc_html($label) . '</td><td>' . esc_html($expiry) . '</td><td><details><summary>' . esc_html__('调整', 'aksrt') . '</summary><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-top:8px"><input type="hidden" name="action" value="aksrt_users_mutate_vip"><input type="hidden" name="user_ids[]" value="' . $id . '">';
$tone = $level > 0 ? ((int) $row['raw_level'] !== $level || (int) $row['raw_expires'] !== $expires ? 'warning' : 'success') : 'neutral';
echo '<tr><td class="check-column"><input class="aksrt-vip-check" form="aksrt-vip-bulk" type="checkbox" name="user_ids[]" value="' . $id . '"></td><td>' . $id . '</td><td>' . NativeUi::user_cell($id, (string) $row['display_name']) . '<div class="aksrt-native-muted">@' . esc_html((string) $row['user_login']) . '</div></td><td>' . NativeUi::badge($label, $tone) . '</td><td>' . esc_html($expiry) . '</td><td><details><summary class="button button-small">' . esc_html__('调整', 'aksrt') . '</summary><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-form-foot"><input type="hidden" name="action" value="aksrt_users_mutate_vip"><input type="hidden" name="user_ids[]" value="' . $id . '">'; // phpcs:ignore WordPress.Security.EscapeOutput -- builders escape.
wp_nonce_field('aksrt_users_mutate_vip');
$this->render_vip_return_fields($filters);
$this->render_vip_operation_fields($names);
echo '<p style="margin-bottom:8px"><label style="display:block">' . esc_html__('原因', 'aksrt') . '<br><input required name="reason" class="regular-text" style="width:100%;max-width:360px;box-sizing:border-box"></label></p>';
echo '<label class="aksrt-native-field is-wide">' . esc_html__('原因', 'aksrt') . '<input required name="reason" class="regular-text"></label>';
submit_button(__('保存', 'aksrt'), 'secondary', '', false);
echo '</form></details></td></tr>';
}
@@ -228,7 +236,7 @@ trait UsersMenusVip {
echo '<select class="aksrt-vip-operation" name="vip_operation"><option value="set">' . esc_html__('设置会员', 'aksrt') . '</option><option value="extend">' . esc_html__('延长有效期', 'aksrt') . '</option><option value="restore">' . esc_html__('取消限时升级/恢复回退会员', 'aksrt') . '</option><option value="disable">' . esc_html__('停用会员', 'aksrt') . '</option></select> ';
echo '<span class="aksrt-vip-set-fields"><label>' . esc_html__('档位', 'aksrt') . ' <select name="level"><option value="1">' . esc_html($names[1]) . '</option><option value="2">' . esc_html($names[2]) . '</option></select></label> ';
echo '<label><input type="checkbox" name="permanent" value="1">' . esc_html__('永久', 'aksrt') . '</label> <label>' . esc_html__('绝对到期日', 'aksrt') . ' <input type="date" name="expires_date"></label></span> ';
echo '<span class="aksrt-vip-extend-fields"><label>' . esc_html__('延长', 'aksrt') . ' <input class="aksrt-vip-amount" type="number" name="amount" min="1" max="3650" value="1" style="width:72px"><select name="unit"><option value="day">' . esc_html__('天', 'aksrt') . '</option><option value="month">' . esc_html__('月', 'aksrt') . '</option></select></label></span>';
echo '<span class="aksrt-vip-extend-fields"><label>' . esc_html__('延长', 'aksrt') . ' <input class="aksrt-vip-amount small-text" type="number" name="amount" min="1" max="3650" value="1"><select name="unit"><option value="day">' . esc_html__('天', 'aksrt') . '</option><option value="month">' . esc_html__('月', 'aksrt') . '</option></select></label></span>';
echo '<script>document.querySelectorAll(".aksrt-vip-operation").forEach(function(select){function sync(){var form=select.closest("form");if(!form)return;var set=form.querySelector(".aksrt-vip-set-fields"),extend=form.querySelector(".aksrt-vip-extend-fields"),danger=select.value==="disable"||select.value==="restore";if(set)set.hidden=select.value!=="set";if(extend)extend.hidden=select.value!=="extend";if(danger&&!form.dataset.confirmBound){form.addEventListener("submit",function(e){if((select.value==="disable"&&!window.confirm("停用后将立即撤销该用户的前端会员权益,并清理恢复状态,确定继续吗?"))||(select.value==="restore"&&!window.confirm("将恢复回退会员并取消当前限时升级,确定继续吗?")))e.preventDefault();});form.dataset.confirmBound="1";}}sync();select.addEventListener("change",sync);});</script>';
}
@@ -262,16 +270,19 @@ trait UsersMenusVip {
private function render_vip_history(int $page): void {
$result = (new \AKSRT\Content\VipMembershipEventRepository())->query($page, 20);
NativeUi::table_card_open(__('变更记录', 'aksrt'));
echo '<table class="wp-list-table widefat fixed striped"><thead><tr><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th><th>' . esc_html__('操作人', 'aksrt') . '</th><th>' . esc_html__('原因', 'aksrt') . '</th><th>' . esc_html__('结果', 'aksrt') . '</th></tr></thead><tbody>';
if ($result['items'] === []) {
echo '<tr><td colspan="6">' . esc_html__('暂无变更记录', 'aksrt') . '</td></tr>';
echo NativeUi::empty_row(6, __('暂无变更记录', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
}
foreach ($result['items'] as $event) {
$user = (string) ($event['display_name'] ?: $event['user_login']);
echo '<tr><td>' . esc_html((string) $event['created_at']) . '</td><td>' . esc_html($user) . ' (#' . (int) $event['user_id'] . ')</td><td>' . esc_html((string) $event['operation']) . '</td><td>' . esc_html((string) ($event['actor_name'] ?: '—')) . '</td><td>' . esc_html((string) $event['reason']) . '</td><td>' . esc_html((string) $event['status']) . '</td></tr>';
}
echo '</tbody></table>';
NativeUi::table_wrap_close();
$this->pagination($result['pages'], $page);
NativeUi::card_close();
}
/** @return array<int,string> */