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:
1 parent
6195508481
commit
fe3ef82718
9 files changed
+1242
-422
No files matched your search
@@ -1,10 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* 消息管理后台页(users.php → aksrt_messages)。
|
||||
*
|
||||
* 单页承载:概览卡片 + 代发系统消息(页头按钮打开弹窗,按用户列表或角色投递)+ 已读状态视图
|
||||
* + 类型 / 收件人 / 关键词筛选 + 全站通知列表(批量标记已读 / 删除)+ 共享分页条。
|
||||
* 查询与写操作在 AKSRT\Messages\NotificationAdminQuery / AdminBroadcast,这里只做页面与路由。
|
||||
* 消息管理后台页:页面编排、带权限 / nonce 的写操作、筛选与反馈。
|
||||
* 展示由同目录的 MessagesList / MessagesCompose traits 承载;查询与投递下沉消息服务。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
@@ -12,17 +9,18 @@
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
use AKSRT\Messages\AdminBroadcast;
|
||||
use AKSRT\Messages\MessageTypes;
|
||||
use AKSRT\Messages\NotificationAdminQuery;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusMessages {
|
||||
|
||||
use UsersMenusMessagesList;
|
||||
use UsersMenusMessagesCompose;
|
||||
|
||||
private const MESSAGE_PAGE_SLUG = 'aksrt_messages';
|
||||
private const MESSAGE_COMPOSE_ID = 'aksrt-msg-compose';
|
||||
|
||||
/** 页面专属样式与弹窗脚本,只在本页入队。 */
|
||||
public function enqueue_message_assets(string $hook): void {
|
||||
if ($hook !== 'users_page_' . self::MESSAGE_PAGE_SLUG) {
|
||||
return;
|
||||
@@ -71,18 +69,14 @@ trait UsersMenusMessages {
|
||||
|
||||
$query = $this->message_query_filters($filters);
|
||||
$result = NotificationAdminQuery::paginate($query, $paged);
|
||||
|
||||
$this->render_message_overview();
|
||||
$this->render_message_compose();
|
||||
|
||||
NativeUi::card_open('', '', false, 'aksrt-native-toolbar');
|
||||
NativeUi::card_open(__('站内通知', 'aksrt'), '', true, 'aksrt-messages-admin__card');
|
||||
echo '<div class="aksrt-messages-admin__toolbar">';
|
||||
echo $this->message_views_html($filters, NotificationAdminQuery::status_counts($query)); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
$this->render_message_filter_form($filters);
|
||||
NativeUi::card_close();
|
||||
|
||||
NativeUi::table_card_open(__('站内通知', 'aksrt'));
|
||||
echo '</div>';
|
||||
$this->render_message_table($result['items'], $filters, $result['page'], $query['user_id'] < 0);
|
||||
NativeUi::table_wrap_close();
|
||||
AdminListPagination::render(
|
||||
self::MESSAGE_PAGE_SLUG,
|
||||
$result['total'],
|
||||
@@ -92,13 +86,12 @@ trait UsersMenusMessages {
|
||||
'users.php'
|
||||
);
|
||||
NativeUi::card_close();
|
||||
|
||||
$this->render_message_compose();
|
||||
$this->render_message_dialogs();
|
||||
NativeUi::page_close();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 写操作
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
public function handle_send_message(): void {
|
||||
$this->ensure_cap();
|
||||
check_admin_referer('aksrt_users_send_message');
|
||||
@@ -108,17 +101,24 @@ trait UsersMenusMessages {
|
||||
$content = trim(sanitize_textarea_field((string) wp_unslash($_POST['content'] ?? '')));
|
||||
$target_url = esc_url_raw(trim((string) wp_unslash($_POST['target_url'] ?? '')));
|
||||
|
||||
if (!in_array($mode, ['users', 'role'], true)) {
|
||||
$this->message_fail(__('请选择有效的收件人模式。', 'aksrt'));
|
||||
}
|
||||
if ($content === '') {
|
||||
$this->message_fail(__('请填写消息内容。', 'aksrt'));
|
||||
}
|
||||
if (mb_strlen($content) > AdminBroadcast::CONTENT_MAX) {
|
||||
$this->message_fail(sprintf(__('消息内容不能超过 %d 字。', 'aksrt'), AdminBroadcast::CONTENT_MAX));
|
||||
}
|
||||
if (mb_strlen($title) > AdminBroadcast::TITLE_MAX) {
|
||||
$this->message_fail(sprintf(__('消息标题不能超过 %d 字。', 'aksrt'), AdminBroadcast::TITLE_MAX));
|
||||
}
|
||||
$title = mb_substr($title !== '' ? $title : $content, 0, AdminBroadcast::TITLE_MAX);
|
||||
|
||||
$unknown = 0;
|
||||
if ($mode === 'role') {
|
||||
$ids = AdminBroadcast::resolve_role(sanitize_key((string) wp_unslash($_POST['role'] ?? '')));
|
||||
$role = sanitize_key((string) wp_unslash($_POST['role'] ?? ''));
|
||||
$ids = AdminBroadcast::resolve_role($role);
|
||||
} else {
|
||||
$resolved = AdminBroadcast::resolve_users(sanitize_textarea_field((string) wp_unslash($_POST['users'] ?? '')));
|
||||
$ids = is_wp_error($resolved) ? $resolved : $resolved['ids'];
|
||||
@@ -130,6 +130,13 @@ trait UsersMenusMessages {
|
||||
if ($ids === []) {
|
||||
$this->message_fail(__('没有匹配到任何有效收件人。', 'aksrt'));
|
||||
}
|
||||
// 确认绑定角色与人数;页面加载后人数变化时拒绝投递,避免按过期预览群发。
|
||||
if ($mode === 'role' && (
|
||||
sanitize_key((string) wp_unslash($_POST['confirmed_role'] ?? '')) !== $role
|
||||
|| absint($_POST['confirmed_count'] ?? 0) !== count($ids)
|
||||
)) {
|
||||
$this->message_fail(__('收件人范围尚未确认或人数已变化,请重新确认后发送。', 'aksrt'));
|
||||
}
|
||||
|
||||
$sent = AdminBroadcast::send($ids, $title, $content, $target_url);
|
||||
$this->message_redirect([
|
||||
@@ -145,7 +152,8 @@ trait UsersMenusMessages {
|
||||
check_admin_referer('aksrt_users_bulk_messages');
|
||||
|
||||
$op = sanitize_key((string) wp_unslash($_POST['bulk_action'] ?? ''));
|
||||
$ids = isset($_POST['ids']) && is_array($_POST['ids']) ? array_map('absint', wp_unslash($_POST['ids'])) : [];
|
||||
$single_id = absint($_POST['message_id'] ?? 0);
|
||||
$ids = $single_id > 0 ? [$single_id] : (isset($_POST['ids']) && is_array($_POST['ids']) ? array_map('absint', wp_unslash($_POST['ids'])) : []);
|
||||
$back = $this->message_filter_args($this->message_filters($_POST));
|
||||
$paged = absint($_POST['paged'] ?? 0);
|
||||
if ($paged > 1) {
|
||||
@@ -162,15 +170,8 @@ trait UsersMenusMessages {
|
||||
]));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 区块渲染
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
private function render_message_notices(): void {
|
||||
$code = sanitize_key((string) wp_unslash($_GET['aksrt_msg'] ?? ''));
|
||||
if ($code === '') {
|
||||
return;
|
||||
}
|
||||
$n = absint($_GET['aksrt_msg_n'] ?? 0);
|
||||
$error = false;
|
||||
switch ($code) {
|
||||
@@ -204,297 +205,24 @@ trait UsersMenusMessages {
|
||||
default:
|
||||
return;
|
||||
}
|
||||
printf(
|
||||
'<div class="notice %s is-dismissible"><p>%s</p></div>',
|
||||
$error ? 'notice-error' : 'notice-success',
|
||||
esc_html($text)
|
||||
);
|
||||
printf('<div class="notice %s is-dismissible"><p>%s</p></div>', $error ? 'notice-error' : 'notice-success', esc_html($text));
|
||||
}
|
||||
|
||||
private function render_message_overview(): void {
|
||||
$stats = NotificationAdminQuery::overview();
|
||||
NativeUi::stats([
|
||||
['label' => __('消息总数', 'aksrt'), 'value' => number_format_i18n($stats['total']), 'icon' => 'dashicons-email-alt', 'tone' => 'info'],
|
||||
['label' => __('未读', 'aksrt'), 'value' => number_format_i18n($stats['unread']), 'icon' => 'dashicons-marker', 'tone' => 'danger'],
|
||||
['label' => __('今日新增', 'aksrt'), 'value' => number_format_i18n($stats['today']), 'icon' => 'dashicons-calendar-alt', 'tone' => 'success'],
|
||||
['label' => __('涉及用户', 'aksrt'), 'value' => number_format_i18n($stats['recipients']), 'icon' => 'dashicons-groups', 'tone' => 'primary'],
|
||||
]);
|
||||
}
|
||||
|
||||
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__form aksrt-messages-admin__compose">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_send_message" />';
|
||||
wp_nonce_field('aksrt_users_send_message');
|
||||
|
||||
printf(
|
||||
'<header class="aksrt-msg-dialog__head"><h2 id="%s-title" class="aksrt-msg-dialog__title"><span class="dashicons dashicons-megaphone" aria-hidden="true"></span>%s</h2>'
|
||||
. '<button type="button" class="aksrt-msg-dialog__close" data-aksrt-dialog-close aria-label="%s"><span class="dashicons dashicons-no-alt" aria-hidden="true"></span></button></header>',
|
||||
esc_attr(self::MESSAGE_COMPOSE_ID),
|
||||
esc_html__('发送系统消息', 'aksrt'),
|
||||
esc_attr__('关闭', 'aksrt')
|
||||
);
|
||||
echo '<div class="aksrt-msg-dialog__body">';
|
||||
|
||||
echo '<fieldset class="aksrt-messages-admin__modes"><legend>' . esc_html__('收件人', 'aksrt') . '</legend>';
|
||||
printf(
|
||||
'<label><input type="radio" name="recipient_mode" value="users" checked /> %s</label>',
|
||||
esc_html__('指定用户', 'aksrt')
|
||||
);
|
||||
printf(
|
||||
'<label><input type="radio" name="recipient_mode" value="role" /> %s</label>',
|
||||
esc_html__('按角色', 'aksrt')
|
||||
);
|
||||
echo '</fieldset>';
|
||||
|
||||
echo '<div class="aksrt-native-form-grid">';
|
||||
printf(
|
||||
'<label class="aksrt-native-field is-wide is-users"><span class="aksrt-native-field__label">%s</span><textarea name="users" rows="2" class="large-text" 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 is-role"><span class="aksrt-native-field__label">' . esc_html__('角色', 'aksrt') . '</span><select name="role">';
|
||||
foreach (wp_roles()->get_names() as $role => $name) {
|
||||
printf('<option value="%s">%s</option>', esc_attr($role), esc_html(translate_user_role($name)));
|
||||
}
|
||||
echo '</select><small class="aksrt-native-field__hint">' . esc_html(sprintf(__('角色用户超过 %d 人时拒绝发送。', 'aksrt'), AdminBroadcast::MAX_ROLE_USERS)) . '</small></label>';
|
||||
printf(
|
||||
'<label class="aksrt-native-field"><span class="aksrt-native-field__label">%s</span><input type="text" name="title" maxlength="%d" class="regular-text" placeholder="%s" /></label>',
|
||||
esc_html__('标题', 'aksrt'),
|
||||
(int) AdminBroadcast::TITLE_MAX,
|
||||
esc_attr__('留空则取内容开头', 'aksrt')
|
||||
);
|
||||
printf(
|
||||
'<label class="aksrt-native-field"><span class="aksrt-native-field__label">%s</span><input type="url" name="target_url" class="regular-text" placeholder="https://" /></label>',
|
||||
esc_html__('跳转链接(可选)', 'aksrt')
|
||||
);
|
||||
printf(
|
||||
'<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">%s</span><textarea name="content" rows="4" maxlength="%d" required class="large-text"></textarea></label>',
|
||||
esc_html__('内容', 'aksrt'),
|
||||
(int) AdminBroadcast::CONTENT_MAX
|
||||
);
|
||||
echo '</div>';
|
||||
echo '<p class="aksrt-native-muted aksrt-msg-dialog__note">' . esc_html__('以「系统通知」类型逐人投递;邮件 / 公众号提醒按通知配置同步触发。', 'aksrt') . '</p>';
|
||||
echo '</div>';
|
||||
printf(
|
||||
'<footer class="aksrt-msg-dialog__foot"><button type="button" class="button" data-aksrt-dialog-close>%s</button><button type="submit" class="button button-primary" data-aksrt-dialog-submit>%s</button></footer>',
|
||||
esc_html__('取消', 'aksrt'),
|
||||
esc_html__('发送', 'aksrt')
|
||||
);
|
||||
echo '</form></dialog>';
|
||||
}
|
||||
|
||||
/** @param array{all:int,unread:int,read:int} $counts */
|
||||
private function message_views_html(array $filters, array $counts): string {
|
||||
$views = [
|
||||
'' => [__('全部', 'aksrt'), $counts['all']],
|
||||
'unread' => [__('未读', 'aksrt'), $counts['unread']],
|
||||
'read' => [__('已读', 'aksrt'), $counts['read']],
|
||||
];
|
||||
$args = $this->message_filter_args($filters);
|
||||
unset($args['status']);
|
||||
|
||||
$html = '<ul class="subsubsub">';
|
||||
$last = array_key_last($views);
|
||||
foreach ($views as $key => [$label, $count]) {
|
||||
$html .= sprintf(
|
||||
'<li><a href="%s"%s>%s <span class="count">(%s)</span></a>%s</li>',
|
||||
esc_url($this->message_page_url($key === '' ? $args : array_merge($args, ['status' => $key]))),
|
||||
$filters['status'] === $key ? ' class="current" aria-current="page"' : '',
|
||||
esc_html($label),
|
||||
esc_html(number_format_i18n($count)),
|
||||
$key === $last ? '' : ' |'
|
||||
);
|
||||
}
|
||||
return $html . '</ul>';
|
||||
}
|
||||
|
||||
private function render_message_filter_form(array $filters): void {
|
||||
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::MESSAGE_PAGE_SLUG) . '" />';
|
||||
if ($filters['status'] !== '') {
|
||||
echo '<input type="hidden" name="status" value="' . esc_attr($filters['status']) . '" />';
|
||||
}
|
||||
echo '<label class="screen-reader-text" for="aksrt-msg-filter-type">' . esc_html__('按类型筛选', 'aksrt') . '</label>';
|
||||
echo '<select name="type" id="aksrt-msg-filter-type"><option value="">' . esc_html__('全部类型', 'aksrt') . '</option>';
|
||||
foreach (NotificationAdminQuery::types() as $type) {
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($type),
|
||||
selected($filters['type'], $type, false),
|
||||
esc_html(MessageTypes::label($type))
|
||||
);
|
||||
}
|
||||
echo '</select>';
|
||||
printf(
|
||||
'<label class="screen-reader-text" for="aksrt-msg-filter-user">%s</label><input type="text" id="aksrt-msg-filter-user" name="user" value="%s" placeholder="%s" />',
|
||||
esc_html__('按收件人筛选', 'aksrt'),
|
||||
esc_attr($filters['user']),
|
||||
esc_attr__('收件人 ID / 用户名 / 邮箱', 'aksrt')
|
||||
);
|
||||
printf(
|
||||
'<label class="screen-reader-text" for="aksrt-msg-filter-s">%s</label><input type="search" id="aksrt-msg-filter-s" name="s" value="%s" placeholder="%s" />',
|
||||
esc_html__('搜索消息', 'aksrt'),
|
||||
esc_attr($filters['s']),
|
||||
esc_attr__('标题 / 内容', 'aksrt')
|
||||
);
|
||||
submit_button(__('筛选', 'aksrt'), '', '', false);
|
||||
if ($filters['type'] !== '' || $filters['user'] !== '' || $filters['s'] !== '') {
|
||||
printf(
|
||||
'<a class="button-link" href="%s">%s</a>',
|
||||
esc_url($this->message_page_url($filters['status'] === '' ? [] : ['status' => $filters['status']])),
|
||||
esc_html__('清除筛选', 'aksrt')
|
||||
);
|
||||
}
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
/** @param list<array<string,mixed>> $items */
|
||||
private function render_message_table(array $items, array $filters, int $page, bool $user_missing): void {
|
||||
$user_ids = array_values(array_unique(array_filter(array_map(static fn($row): int => (int) ($row['user_id'] ?? 0), $items))));
|
||||
if ($user_ids !== []) {
|
||||
cache_users($user_ids);
|
||||
}
|
||||
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-messages-admin__list">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_bulk_messages" />';
|
||||
wp_nonce_field('aksrt_users_bulk_messages');
|
||||
foreach ($this->message_filter_args($filters) as $key => $value) {
|
||||
printf('<input type="hidden" name="%s" value="%s" />', esc_attr($key), esc_attr((string) $value));
|
||||
}
|
||||
echo '<input type="hidden" name="paged" value="' . esc_attr((string) $page) . '" />';
|
||||
|
||||
echo '<div class="tablenav top"><div class="alignleft actions bulkactions">';
|
||||
printf(
|
||||
'<button type="submit" name="bulk_action" value="mark_read" class="button">%s</button> ',
|
||||
esc_html__('标记已读', 'aksrt')
|
||||
);
|
||||
printf(
|
||||
'<button type="submit" name="bulk_action" value="delete" class="button aksrt-messages-admin__danger" onclick="return confirm(\'%s\');">%s</button>',
|
||||
esc_js(__('确定删除所选消息?删除后用户收件箱中也将不再显示。', 'aksrt')),
|
||||
esc_html__('删除', 'aksrt')
|
||||
);
|
||||
echo '</div><br class="clear"></div>';
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped aksrt-messages-admin__table"><thead><tr>';
|
||||
echo '<td class="manage-column column-cb check-column"><label class="screen-reader-text" for="cb-select-all-1">' . esc_html__('全选', 'aksrt') . '</label><input id="cb-select-all-1" type="checkbox" /></td>';
|
||||
echo '<th class="column-title">' . esc_html__('消息', 'aksrt') . '</th>';
|
||||
echo '<th class="column-type">' . esc_html__('类型', 'aksrt') . '</th>';
|
||||
echo '<th class="column-user">' . esc_html__('收件人', 'aksrt') . '</th>';
|
||||
echo '<th class="column-read">' . esc_html__('状态', 'aksrt') . '</th>';
|
||||
echo '<th class="column-time">' . esc_html__('发送时间', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if ($items === []) {
|
||||
$hint = $user_missing
|
||||
? __('没有找到该收件人。', 'aksrt')
|
||||
: ($this->message_filter_args($filters) === [] ? __('站内还没有任何通知。', 'aksrt') : __('当前筛选条件下没有消息。', 'aksrt'));
|
||||
NativeUi::empty_row(6, __('暂无消息', 'aksrt'), $hint);
|
||||
}
|
||||
foreach ($items as $row) {
|
||||
$this->render_message_row($row, $filters);
|
||||
}
|
||||
echo '</tbody></table></form>';
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $row */
|
||||
private function render_message_row(array $row, array $filters): void {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$type = (string) ($row['type'] ?? '');
|
||||
$title = (string) ($row['title'] ?? '');
|
||||
$desc = (string) ($row['description'] ?? '');
|
||||
$url = (string) ($row['target_url'] ?? '');
|
||||
$unread = empty($row['is_read']);
|
||||
|
||||
echo '<tr class="' . ($unread ? 'is-unread' : 'is-read') . '">';
|
||||
printf(
|
||||
'<th scope="row" class="check-column"><label class="screen-reader-text" for="aksrt-msg-%1$d">%2$s</label><input id="aksrt-msg-%1$d" type="checkbox" name="ids[]" value="%1$d" /></th>',
|
||||
$id,
|
||||
esc_html(sprintf(__('选择消息 #%d', 'aksrt'), $id))
|
||||
);
|
||||
|
||||
echo '<td class="column-title"><strong class="row-title">' . esc_html($title) . '</strong>';
|
||||
if ($desc !== '' && $desc !== $title) {
|
||||
echo '<div class="aksrt-messages-admin__desc">' . esc_html(mb_strimwidth($desc, 0, 160, '…')) . '</div>';
|
||||
}
|
||||
echo '<div class="aksrt-messages-admin__sub"><code>#' . esc_html((string) $id) . '</code>';
|
||||
if ($url !== '') {
|
||||
printf(
|
||||
' <a href="%s" target="_blank" rel="noopener noreferrer">%s<span class="screen-reader-text">%s</span></a>',
|
||||
esc_url($url),
|
||||
esc_html__('跳转链接', 'aksrt'),
|
||||
esc_html__('(在新窗口打开)', 'aksrt')
|
||||
);
|
||||
}
|
||||
echo '</div></td>';
|
||||
|
||||
printf(
|
||||
'<td class="column-type"><a class="aksrt-msg-badge is-%s" href="%s">%s</a></td>',
|
||||
esc_attr(MessageTypes::tone($type)),
|
||||
esc_url($this->message_page_url(array_merge($this->message_filter_args($filters), ['type' => $type]))),
|
||||
esc_html(MessageTypes::label($type))
|
||||
);
|
||||
|
||||
echo '<td class="column-user">' . $this->message_user_cell((int) ($row['user_id'] ?? 0), $filters) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in builder.
|
||||
|
||||
$read_at = (string) ($row['read_at'] ?? '');
|
||||
printf(
|
||||
'<td class="column-read"><span class="aksrt-msg-state %s"%s>%s</span></td>',
|
||||
$unread ? 'is-unread' : 'is-read',
|
||||
!$unread && $read_at !== '' ? ' title="' . esc_attr(sprintf(__('阅读于 %s', 'aksrt'), $this->message_time($read_at))) . '"' : '',
|
||||
$unread ? esc_html__('未读', 'aksrt') : esc_html__('已读', 'aksrt')
|
||||
);
|
||||
|
||||
$created = (string) ($row['created_at'] ?? '');
|
||||
$ts = $created !== '' ? strtotime($created . ' UTC') : false;
|
||||
printf(
|
||||
'<td class="column-time"><time datetime="%s">%s</time>%s</td>',
|
||||
esc_attr($ts ? gmdate('c', $ts) : ''),
|
||||
esc_html($this->message_time($created)),
|
||||
$ts ? '<div class="aksrt-messages-admin__sub">' . esc_html(sprintf(__('%s前', 'aksrt'), human_time_diff($ts))) . '</div>' : ''
|
||||
);
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
private function message_user_cell(int $user_id, array $filters): string {
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : false;
|
||||
if (!$user) {
|
||||
return '<span class="aksrt-messages-admin__muted">' . esc_html(sprintf(__('已删除用户 #%d', 'aksrt'), $user_id)) . '</span>';
|
||||
}
|
||||
$avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user_id, 48) : get_avatar_url($user_id, ['size' => 48]);
|
||||
$filter_url = $this->message_page_url(array_merge($this->message_filter_args($filters), ['user' => (string) $user_id]));
|
||||
return '<a class="aksrt-msg-user" href="' . esc_url($filter_url) . '" title="' . esc_attr__('只看该用户', 'aksrt') . '">'
|
||||
. '<img src="' . esc_url($avatar) . '" width="24" height="24" alt="" />'
|
||||
. '<span>' . esc_html($user->display_name) . '</span></a>'
|
||||
. '<div class="aksrt-messages-admin__sub">#' . esc_html((string) $user_id) . ' · <a href="' . esc_url(get_edit_user_link($user_id)) . '">' . esc_html__('编辑', 'aksrt') . '</a></div>';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 参数与 URL
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/** @return array{status:string,type:string,user:string,s:string} */
|
||||
/** @return array{status:string,type:string,user:string,s:string,period:string} */
|
||||
private function message_filters(?array $source = null): array {
|
||||
$source = $source ?? $_GET;
|
||||
$status = sanitize_key((string) wp_unslash($source['status'] ?? ''));
|
||||
$period = sanitize_key((string) wp_unslash($source['period'] ?? ''));
|
||||
return [
|
||||
'status' => in_array($status, NotificationAdminQuery::STATUSES, true) ? $status : '',
|
||||
'type' => sanitize_key((string) wp_unslash($source['type'] ?? '')),
|
||||
'user' => trim(sanitize_text_field((string) wp_unslash($source['user'] ?? ''))),
|
||||
's' => trim(sanitize_text_field((string) wp_unslash($source['s'] ?? ''))),
|
||||
'period' => $period === 'today' ? 'today' : '',
|
||||
];
|
||||
}
|
||||
|
||||
/** 收件人筛选解析为 user_id:0=不限,-1=未找到(查询结果为空)。 */
|
||||
/** 收件人筛选解析为 user_id:0=不限,-1=未找到。 */
|
||||
private function message_query_filters(array $filters): array {
|
||||
$user_id = 0;
|
||||
$needle = $filters['user'];
|
||||
@@ -513,10 +241,10 @@ trait UsersMenusMessages {
|
||||
'type' => $filters['type'],
|
||||
'user_id' => $user_id,
|
||||
's' => $filters['s'],
|
||||
'period' => $filters['period'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,string> 非空筛选参数(用于 URL / 隐藏域) */
|
||||
private function message_filter_args(array $filters): array {
|
||||
return array_filter($filters, static fn($value): bool => $value !== '');
|
||||
}
|
||||
@@ -525,7 +253,6 @@ trait UsersMenusMessages {
|
||||
return add_query_arg(array_merge(['page' => self::MESSAGE_PAGE_SLUG], $args), admin_url('users.php'));
|
||||
}
|
||||
|
||||
/** 通知表时间统一存 GMT,按站点时区展示。 */
|
||||
private function message_time(string $gmt): string {
|
||||
return $gmt !== '' ? get_date_from_gmt($gmt, 'Y-m-d H:i') : '';
|
||||
}
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
/**
|
||||
* 消息管理展示:概览、筛选、列表与按行预渲染的只读详情。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
use AKSRT\Messages\MessageTypes;
|
||||
use AKSRT\Messages\NotificationAdminQuery;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusMessagesList {
|
||||
|
||||
private function render_message_overview(): void {
|
||||
$stats = NotificationAdminQuery::overview();
|
||||
NativeUi::stats([
|
||||
['label' => __('消息总数', 'aksrt'), 'value' => number_format_i18n($stats['total']), 'icon' => 'dashicons-email-alt', 'tone' => 'info', 'url' => $this->message_page_url()],
|
||||
['label' => __('未读', 'aksrt'), 'value' => number_format_i18n($stats['unread']), 'icon' => 'dashicons-marker', 'tone' => 'danger', 'url' => $this->message_page_url(['status' => 'unread'])],
|
||||
['label' => __('今日新增', 'aksrt'), 'value' => number_format_i18n($stats['today']), 'icon' => 'dashicons-calendar-alt', 'tone' => 'success', 'url' => $this->message_page_url(['period' => 'today'])],
|
||||
['label' => __('涉及用户', 'aksrt'), 'value' => number_format_i18n($stats['recipients']), 'icon' => 'dashicons-groups', 'tone' => 'primary', 'url' => admin_url('users.php'), 'hint' => __('前往用户管理', 'aksrt')],
|
||||
]);
|
||||
}
|
||||
|
||||
private function message_views_html(array $filters, array $counts): string {
|
||||
$views = [
|
||||
'' => [__('全部', 'aksrt'), $counts['all']],
|
||||
'unread' => [__('未读', 'aksrt'), $counts['unread']],
|
||||
'read' => [__('已读', 'aksrt'), $counts['read']],
|
||||
];
|
||||
$args = $this->message_filter_args($filters);
|
||||
unset($args['status']);
|
||||
$html = '<ul class="subsubsub" aria-label="' . esc_attr__('消息状态', 'aksrt') . '">';
|
||||
foreach ($views as $key => [$label, $count]) {
|
||||
$html .= sprintf(
|
||||
'<li><a href="%s"%s>%s <span class="count">(%s)</span></a></li>',
|
||||
esc_url($this->message_page_url($key === '' ? $args : array_merge($args, ['status' => $key]))),
|
||||
$filters['status'] === $key ? ' class="current" aria-current="page"' : '',
|
||||
esc_html($label),
|
||||
esc_html(number_format_i18n($count))
|
||||
);
|
||||
}
|
||||
return $html . '</ul>';
|
||||
}
|
||||
|
||||
private function render_message_filter_form(array $filters): void {
|
||||
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::MESSAGE_PAGE_SLUG) . '" />';
|
||||
if ($filters['status'] !== '') {
|
||||
echo '<input type="hidden" name="status" value="' . esc_attr($filters['status']) . '" />';
|
||||
}
|
||||
echo '<label class="screen-reader-text" for="aksrt-msg-filter-type">' . esc_html__('按类型筛选', 'aksrt') . '</label>';
|
||||
echo '<select name="type" id="aksrt-msg-filter-type"><option value="">' . esc_html__('全部类型', 'aksrt') . '</option>';
|
||||
foreach (NotificationAdminQuery::types() as $type) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($type), selected($filters['type'], $type, false), esc_html(MessageTypes::label($type)));
|
||||
}
|
||||
echo '</select>';
|
||||
echo '<label class="screen-reader-text" for="aksrt-msg-filter-period">' . esc_html__('按发送时间筛选', 'aksrt') . '</label>';
|
||||
echo '<select name="period" id="aksrt-msg-filter-period"><option value="">' . esc_html__('全部时间', 'aksrt') . '</option>';
|
||||
echo '<option value="today"' . selected($filters['period'], 'today', false) . '>' . esc_html__('今日新增', 'aksrt') . '</option></select>';
|
||||
printf(
|
||||
'<label class="screen-reader-text" for="aksrt-msg-filter-user">%s</label><input type="text" id="aksrt-msg-filter-user" name="user" value="%s" placeholder="%s" />',
|
||||
esc_html__('按收件人筛选', 'aksrt'), esc_attr($filters['user']), esc_attr__('收件人 ID / 用户名 / 邮箱', 'aksrt')
|
||||
);
|
||||
printf(
|
||||
'<label class="screen-reader-text" for="aksrt-msg-filter-s">%s</label><input type="search" id="aksrt-msg-filter-s" name="s" value="%s" placeholder="%s" />',
|
||||
esc_html__('搜索消息', 'aksrt'), esc_attr($filters['s']), esc_attr__('标题 / 内容', 'aksrt')
|
||||
);
|
||||
submit_button(__('筛选', 'aksrt'), '', '', false);
|
||||
$other = $filters;
|
||||
unset($other['status']);
|
||||
if ($this->message_filter_args($other) !== []) {
|
||||
echo '<a class="button-link" href="' . esc_url($this->message_page_url($filters['status'] === '' ? [] : ['status' => $filters['status']])) . '">' . esc_html__('清除筛选', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
private function render_message_table(array $items, array $filters, int $page, bool $user_missing): void {
|
||||
$user_ids = array_values(array_unique(array_filter(array_map(static fn($row): int => (int) ($row['user_id'] ?? 0), $items))));
|
||||
if ($user_ids !== []) {
|
||||
cache_users($user_ids);
|
||||
}
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-messages-admin__list" data-aksrt-msg-list>';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_bulk_messages" />';
|
||||
wp_nonce_field('aksrt_users_bulk_messages');
|
||||
foreach ($this->message_filter_args($filters) as $key => $value) {
|
||||
printf('<input type="hidden" name="%s" value="%s" />', esc_attr($key), esc_attr((string) $value));
|
||||
}
|
||||
echo '<input type="hidden" name="paged" value="' . esc_attr((string) $page) . '" />';
|
||||
echo '<input type="hidden" name="bulk_action" value="" /><input type="hidden" name="message_id" value="" />';
|
||||
echo '<div class="aksrt-messages-admin__bulk">';
|
||||
echo '<span class="aksrt-messages-admin__selection" data-aksrt-msg-selection role="status" aria-live="polite" data-label="' . esc_attr__('已选 %d 条', 'aksrt') . '"></span>';
|
||||
echo '<button type="button" class="button" data-aksrt-msg-action="mark_read" disabled>' . esc_html__('标记已读', 'aksrt') . '</button>';
|
||||
echo '<button type="button" class="button aksrt-messages-admin__danger" data-aksrt-msg-action="delete" disabled>' . esc_html__('删除', 'aksrt') . '</button>';
|
||||
echo '</div><div class="aksrt-native-table-wrap">';
|
||||
echo '<table class="wp-list-table widefat fixed striped aksrt-messages-admin__table"><thead><tr>';
|
||||
echo '<td class="manage-column column-cb check-column"><label class="screen-reader-text" for="aksrt-msg-check-all">' . esc_html__('全选本页消息', 'aksrt') . '</label><input id="aksrt-msg-check-all" type="checkbox" data-aksrt-msg-check-all /></td>';
|
||||
foreach (['title' => __('消息', 'aksrt'), 'type' => __('类型', 'aksrt'), 'user' => __('收件人', 'aksrt'), 'read' => __('状态', 'aksrt'), 'time' => __('发送时间', 'aksrt')] as $column => $label) {
|
||||
echo '<th scope="col" class="column-' . esc_attr($column) . '">' . esc_html($label) . '</th>';
|
||||
}
|
||||
echo '</tr></thead><tbody>';
|
||||
if ($items === []) {
|
||||
$hint = $user_missing ? __('没有找到该收件人。', 'aksrt')
|
||||
: ($this->message_filter_args($filters) === [] ? __('站内还没有任何通知。', 'aksrt') : __('当前筛选条件下没有消息。', 'aksrt'));
|
||||
echo NativeUi::empty_row(6, __('暂无消息', 'aksrt'), $hint); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
}
|
||||
foreach ($items as $row) {
|
||||
$this->render_message_row($row, $filters);
|
||||
}
|
||||
echo '</tbody></table></div></form>';
|
||||
}
|
||||
|
||||
private function render_message_row(array $row, array $filters): void {
|
||||
$id = (int) $row['id'];
|
||||
$type = (string) $row['type'];
|
||||
$title = (string) $row['title'];
|
||||
$desc = (string) $row['description'];
|
||||
$unread = empty($row['is_read']);
|
||||
echo '<tr class="' . ($unread ? 'is-unread' : 'is-read') . '">';
|
||||
printf(
|
||||
'<th scope="row" class="check-column"><label class="screen-reader-text" for="aksrt-msg-%1$d">%2$s</label><input id="aksrt-msg-%1$d" type="checkbox" name="ids[]" value="%1$d" data-aksrt-msg-check /></th>',
|
||||
$id, esc_html(sprintf(__('选择消息 #%d', 'aksrt'), $id))
|
||||
);
|
||||
echo '<td class="column-title"><button type="button" class="button-link row-title" data-aksrt-msg-view="' . esc_attr((string) $id) . '" aria-haspopup="dialog" aria-controls="aksrt-msg-detail">' . esc_html($title) . '</button>';
|
||||
if ($desc !== '' && $desc !== $title) {
|
||||
echo '<div class="aksrt-messages-admin__desc">' . esc_html(mb_strimwidth($desc, 0, 160, '…')) . '</div>';
|
||||
}
|
||||
echo '<div class="aksrt-messages-admin__sub"><code>#' . esc_html((string) $id) . '</code></div>';
|
||||
echo '<div class="aksrt-msg-row-actions">';
|
||||
printf('<button type="button" class="button-link" data-aksrt-msg-view="%d" aria-haspopup="dialog" aria-controls="aksrt-msg-detail">%s</button>', $id, esc_html__('查看', 'aksrt'));
|
||||
if ($unread) {
|
||||
printf('<button type="button" class="button-link" data-aksrt-msg-action="mark_read" data-id="%d">%s</button>', $id, esc_html__('标记已读', 'aksrt'));
|
||||
}
|
||||
printf('<button type="button" class="button-link button-link-delete" data-aksrt-msg-action="delete" data-id="%d">%s</button>', $id, esc_html__('删除', 'aksrt'));
|
||||
echo '</div>';
|
||||
$this->render_message_detail_template($row);
|
||||
echo '</td>';
|
||||
printf(
|
||||
'<td class="column-type"><a class="aksrt-msg-badge is-%s" href="%s">%s</a></td>',
|
||||
esc_attr(MessageTypes::tone($type)), esc_url($this->message_page_url(array_merge($this->message_filter_args($filters), ['type' => $type]))), esc_html(MessageTypes::label($type))
|
||||
);
|
||||
echo '<td class="column-user">' . $this->message_user_cell((int) $row['user_id'], $filters) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
$read_at = (string) ($row['read_at'] ?? '');
|
||||
printf(
|
||||
'<td class="column-read"><span class="aksrt-msg-state %s"%s>%s</span></td>',
|
||||
$unread ? 'is-unread' : 'is-read',
|
||||
!$unread && $read_at !== '' ? ' title="' . esc_attr(sprintf(__('阅读于 %s', 'aksrt'), $this->message_time($read_at))) . '"' : '',
|
||||
$unread ? esc_html__('未读', 'aksrt') : esc_html__('已读', 'aksrt')
|
||||
);
|
||||
$created = (string) $row['created_at'];
|
||||
$ts = $created !== '' ? strtotime($created . ' UTC') : false;
|
||||
printf(
|
||||
'<td class="column-time"><time datetime="%s">%s</time>%s</td>',
|
||||
esc_attr($ts ? gmdate('c', $ts) : ''), esc_html($this->message_time($created)),
|
||||
$ts ? '<div class="aksrt-messages-admin__sub">' . esc_html(sprintf(__('%s前', 'aksrt'), human_time_diff($ts))) . '</div>' : ''
|
||||
);
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
private function render_message_detail_template(array $row): void {
|
||||
$user = get_userdata((int) $row['user_id']);
|
||||
$recipient = $user ? $user->display_name : __('已删除用户', 'aksrt');
|
||||
echo '<template id="aksrt-msg-detail-' . esc_attr((string) $row['id']) . '">';
|
||||
echo '<h3 class="aksrt-msg-detail__title">' . esc_html((string) $row['title']) . '</h3><dl class="aksrt-msg-detail__meta">';
|
||||
$meta = [
|
||||
__('消息编号', 'aksrt') => '#' . $row['id'],
|
||||
__('收件人', 'aksrt') => $recipient . ' · #' . $row['user_id'],
|
||||
__('消息类型', 'aksrt') => MessageTypes::label((string) $row['type']),
|
||||
__('发送时间', 'aksrt') => $this->message_time((string) $row['created_at']),
|
||||
__('阅读状态', 'aksrt') => empty($row['is_read']) ? __('未读', 'aksrt') : __('已读', 'aksrt'),
|
||||
__('阅读时间', 'aksrt') => empty($row['is_read']) || empty($row['read_at']) ? '—' : $this->message_time((string) $row['read_at']),
|
||||
];
|
||||
foreach ($meta as $label => $value) {
|
||||
echo '<div><dt>' . esc_html($label) . '</dt><dd>' . esc_html($value) . '</dd></div>';
|
||||
}
|
||||
echo '</dl><h4>' . esc_html__('完整内容', 'aksrt') . '</h4><div class="aksrt-msg-detail__content">' . esc_html((string) $row['description']) . '</div>';
|
||||
if (!empty($row['target_url'])) {
|
||||
printf('<a class="aksrt-msg-detail__link" href="%s" target="_blank" rel="noopener noreferrer">%s<span class="screen-reader-text">%s</span></a>', esc_url((string) $row['target_url']), esc_html((string) $row['target_url']), esc_html__('(在新窗口打开)', 'aksrt'));
|
||||
}
|
||||
echo '<p class="aksrt-native-field__hint">' . esc_html__('查看详情不会改变收件人的已读状态。', 'aksrt') . '</p></template>';
|
||||
}
|
||||
|
||||
private function message_user_cell(int $user_id, array $filters): string {
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : false;
|
||||
if (!$user) {
|
||||
return '<span class="aksrt-messages-admin__muted">' . esc_html(sprintf(__('已删除用户 #%d', 'aksrt'), $user_id)) . '</span>';
|
||||
}
|
||||
$avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user_id, 48) : get_avatar_url($user_id, ['size' => 48]);
|
||||
$filter_url = $this->message_page_url(array_merge($this->message_filter_args($filters), ['user' => (string) $user_id]));
|
||||
return '<a class="aksrt-msg-user" href="' . esc_url($filter_url) . '" title="' . esc_attr__('只看该用户', 'aksrt') . '">'
|
||||
. '<img src="' . esc_url($avatar) . '" width="24" height="24" alt="" />'
|
||||
. '<span>' . esc_html($user->display_name) . '</span></a>'
|
||||
. '<div class="aksrt-messages-admin__sub">#' . esc_html((string) $user_id) . ' · <a href="' . esc_url(get_edit_user_link($user_id)) . '">' . esc_html__('编辑', 'aksrt') . '</a></div>';
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ final class NotificationAdminQuery {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{status:string,type:string,user_id:int,s:string} $filters user_id:0=不限,<0=无匹配用户
|
||||
* @param array{status:string,type:string,user_id:int,s:string,period?:string} $filters user_id:0=不限,<0=无匹配用户
|
||||
* @return array{items:list<array<string,mixed>>,total:int,page:int}
|
||||
*/
|
||||
public static function paginate(array $filters, int $page, int $per_page = self::PER_PAGE): array {
|
||||
@@ -76,14 +76,15 @@ final class NotificationAdminQuery {
|
||||
}
|
||||
global $wpdb;
|
||||
// created_at 存 GMT;「今日」按站点时区零点换算。
|
||||
$today_gmt = get_gmt_from_date(wp_date('Y-m-d 00:00:00'));
|
||||
[$today_start, $today_end] = self::today_range();
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT COUNT(*) AS total,
|
||||
COALESCE(SUM(is_read = 0), 0) AS unread,
|
||||
COALESCE(SUM(created_at >= %s), 0) AS today,
|
||||
COALESCE(SUM(created_at >= %s AND created_at < %s), 0) AS today,
|
||||
COUNT(DISTINCT user_id) AS recipients
|
||||
FROM {$table}",
|
||||
$today_gmt
|
||||
$today_start,
|
||||
$today_end
|
||||
), ARRAY_A);
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
@@ -158,9 +159,22 @@ final class NotificationAdminQuery {
|
||||
$params[] = $like;
|
||||
$params[] = $like;
|
||||
}
|
||||
if (($filters['period'] ?? '') === 'today') {
|
||||
$where[] = 'created_at >= %s AND created_at < %s';
|
||||
array_push($params, ...self::today_range());
|
||||
}
|
||||
return [implode(' AND ', $where), $params];
|
||||
}
|
||||
|
||||
/** 先在站点时区计算自然日,再转换成 GMT;正确处理夏令时的 23 / 25 小时日。 */
|
||||
private static function today_range(): array {
|
||||
$start = current_datetime()->setTime(0, 0, 0);
|
||||
return [
|
||||
get_gmt_from_date($start->format('Y-m-d H:i:s')),
|
||||
get_gmt_from_date($start->modify('+1 day')->format('Y-m-d H:i:s')),
|
||||
];
|
||||
}
|
||||
|
||||
private static function prepare(string $sql, array $params): string {
|
||||
global $wpdb;
|
||||
return $params === [] ? $sql : (string) $wpdb->prepare($sql, $params);
|
||||
|
||||
Reference in new issue
Block a user