feat(admin): upgrade message management and broadcast interactions

Add message details, per-row actions, selection-aware bulk controls and
confirmation dialogs. Merge filters into the list card and link overview
statistics to the matching views with site-timezone today filtering.

Improve the compose dialog with Unicode character counters, recipient mode
segments, role counts and explicit broadcast confirmation verified against
the resolved role and recipient count on submission. Split rendering into
focused sibling traits and cover rendering, handlers and UI interactions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-30 17:24:54 +08:00
1 parent 6195508481
commit fe3ef82718
9 files changed
+1242 -422

No files matched your search

@@ -0,0 +1,109 @@
<?php
/**
* 消息管理弹窗:发送表单、只读详情、删除与角色群发共用的二次确认。
*
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
use AKSRT\Messages\AdminBroadcast;
defined('ABSPATH') || exit;
trait UsersMenusMessagesCompose {
private function render_message_compose(): void {
$open = sanitize_key((string) wp_unslash($_GET['aksrt_msg'] ?? '')) === 'send_failed';
printf(
'<dialog class="aksrt-msg-dialog" id="%s" aria-labelledby="%s-title"%s>',
esc_attr(self::MESSAGE_COMPOSE_ID), esc_attr(self::MESSAGE_COMPOSE_ID), $open ? ' data-aksrt-dialog-autoopen' : ''
);
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-msg-dialog__frame aksrt-messages-admin__compose" data-aksrt-msg-compose';
$strings = [
'count-error' => __('最多输入 {count} 字。', 'aksrt'),
'role-hint' => __('将发送给 {count} 位用户;提交前需再次确认。', 'aksrt'),
'role-title' => __('确认按角色群发', 'aksrt'),
'role-confirm' => __('将向「{role}」的 {count} 位用户发送系统消息,并按通知配置触发提醒。请确认收件人范围与消息内容。', 'aksrt'),
'role-send' => __('确认发送', 'aksrt'),
];
foreach ($strings as $key => $text) {
echo ' data-i18n-' . esc_attr($key) . '="' . esc_attr($text) . '"';
}
echo '>';
echo '<input type="hidden" name="action" value="aksrt_users_send_message" />';
echo '<input type="hidden" name="confirmed_role" value="" /><input type="hidden" name="confirmed_count" value="" />';
wp_nonce_field('aksrt_users_send_message');
$this->render_message_dialog_head(self::MESSAGE_COMPOSE_ID, __('发送系统消息', 'aksrt'), 'dashicons-megaphone');
echo '<div class="aksrt-msg-dialog__body">';
if ($open) {
$error = sanitize_text_field((string) wp_unslash($_GET['aksrt_msg_error'] ?? __('消息发送失败,请检查后重试。', 'aksrt')));
echo '<p class="aksrt-native-callout is-danger">' . esc_html($error) . '</p>';
}
echo '<fieldset class="aksrt-msg-modes"><legend class="aksrt-native-field__label">' . esc_html__('收件人', 'aksrt') . '</legend>';
foreach (['users' => [__('指定用户', 'aksrt'), 'dashicons-admin-users'], 'role' => [__('按角色群发', 'aksrt'), 'dashicons-groups']] as $value => [$label, $icon]) {
echo '<label class="aksrt-msg-mode"><input type="radio" name="recipient_mode" value="' . esc_attr($value) . '"' . checked($value, 'users', false) . ' />';
echo '<span><span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>' . esc_html($label) . '</span></label>';
}
echo '</fieldset><div class="aksrt-native-form-grid">';
printf(
'<label class="aksrt-native-field is-wide" data-aksrt-msg-pane="users"><span class="aksrt-native-field__label">%s</span><textarea name="users" rows="2" required placeholder="%s"></textarea><small class="aksrt-native-field__hint">%s</small></label>',
esc_html__('用户', 'aksrt'), esc_attr__('用户 ID / 用户名 / 邮箱,逗号或换行分隔', 'aksrt'), esc_html(sprintf(__('单次最多 %d 位。', 'aksrt'), AdminBroadcast::MAX_USERS))
);
echo '<label class="aksrt-native-field is-wide" data-aksrt-msg-pane="role" hidden><span class="aksrt-native-field__label">' . esc_html__('角色', 'aksrt') . '</span>';
echo '<select name="role" required disabled><option value="" selected disabled>' . esc_html__('请选择角色', 'aksrt') . '</option>';
$counts = count_users('time');
foreach (wp_roles()->get_names() as $role => $name) {
$count = (int) ($counts['avail_roles'][$role] ?? 0);
$label = translate_user_role($name);
$disabled = $count === 0 || $count > AdminBroadcast::MAX_ROLE_USERS;
$suffix = $count > AdminBroadcast::MAX_ROLE_USERS ? __('(超过发送上限)', 'aksrt') : ($count === 0 ? __('(暂无用户)', 'aksrt') : '');
printf(
'<option value="%s" data-count="%d" data-label="%s"%s>%s</option>',
esc_attr($role), $count, esc_attr($label), $disabled ? ' disabled' : '',
esc_html(sprintf(__('%1$s · %2$s 人', 'aksrt'), $label, number_format_i18n($count)) . $suffix)
);
}
echo '</select><small class="aksrt-native-field__hint">' . esc_html(sprintf(__('单个角色最多 %d 人;空角色和超限角色不可选。', 'aksrt'), AdminBroadcast::MAX_ROLE_USERS)) . '</small>';
echo '<small class="aksrt-native-field__hint" data-aksrt-msg-role-hint aria-live="polite"></small></label>';
printf(
'<label class="aksrt-native-field"><span class="aksrt-native-field__label">%s</span><input type="text" name="title" data-count-limit="%d" aria-describedby="aksrt-msg-title-count" placeholder="%s" /><small class="aksrt-msg-counter" id="aksrt-msg-title-count" data-count-for="title"></small></label>',
esc_html__('标题', 'aksrt'), AdminBroadcast::TITLE_MAX, esc_attr__('留空则取内容开头', 'aksrt')
);
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('跳转链接(可选)', 'aksrt') . '</span><input type="url" name="target_url" placeholder="https://" /></label>';
printf(
'<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">%s</span><textarea name="content" rows="5" data-count-limit="%d" required aria-describedby="aksrt-msg-content-count"></textarea><small class="aksrt-msg-counter" id="aksrt-msg-content-count" data-count-for="content"></small></label>',
esc_html__('内容', 'aksrt'), AdminBroadcast::CONTENT_MAX
);
echo '</div><p class="aksrt-native-muted aksrt-msg-dialog__note">' . esc_html__('以「系统通知」类型逐人投递;邮件 / 公众号提醒按通知配置同步触发。', 'aksrt') . '</p></div>';
echo '<footer class="aksrt-msg-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close>' . esc_html__('取消', 'aksrt') . '</button>';
echo '<button type="submit" class="button button-primary" data-aksrt-msg-send>' . esc_html__('发送', 'aksrt') . '</button></footer></form></dialog>';
}
private function render_message_dialogs(): void {
echo '<dialog class="aksrt-msg-dialog" id="aksrt-msg-detail" aria-labelledby="aksrt-msg-detail-title"><div class="aksrt-msg-dialog__frame">';
$this->render_message_dialog_head('aksrt-msg-detail', __('消息详情', 'aksrt'), 'dashicons-email-alt');
echo '<div class="aksrt-msg-dialog__body" data-aksrt-msg-detail-body></div>';
echo '<footer class="aksrt-msg-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close>' . esc_html__('关闭', 'aksrt') . '</button></footer></div></dialog>';
echo '<dialog class="aksrt-msg-dialog aksrt-msg-dialog--confirm" id="aksrt-msg-confirm" aria-labelledby="aksrt-msg-confirm-title" aria-describedby="aksrt-msg-confirm-text"';
foreach ([
'delete-title' => __('删除消息', 'aksrt'),
'delete-text' => __('确定删除 {count} 条消息?删除后用户收件箱中也将不再显示,此操作不可撤销。', 'aksrt'),
'delete-send' => __('确认删除', 'aksrt'),
] as $key => $text) {
echo ' data-i18n-' . esc_attr($key) . '="' . esc_attr($text) . '"';
}
echo '><div class="aksrt-msg-dialog__frame">';
$this->render_message_dialog_head('aksrt-msg-confirm', __('确认操作', 'aksrt'), 'dashicons-warning');
echo '<div class="aksrt-msg-dialog__body"><p id="aksrt-msg-confirm-text"></p></div>';
echo '<footer class="aksrt-msg-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close autofocus>' . esc_html__('取消', 'aksrt') . '</button>';
echo '<button type="button" class="button button-primary" data-aksrt-msg-confirm>' . esc_html__('确认', 'aksrt') . '</button></footer></div></dialog>';
}
private function render_message_dialog_head(string $id, string $title, string $icon): void {
echo '<header class="aksrt-msg-dialog__head"><h2 id="' . esc_attr($id) . '-title" class="aksrt-msg-dialog__title">';
echo '<span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span><span data-aksrt-dialog-title>' . esc_html($title) . '</span></h2>';
echo '<button type="button" class="aksrt-msg-dialog__close" data-aksrt-dialog-close aria-label="' . esc_attr__('关闭', 'aksrt') . '"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>';
}
}