From fe3ef827184b09d4c4f8f6b571c266fddf1a6c52 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 30 Oct 2026 17:24:54 +0800 Subject: [PATCH] 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 --- assets/css/admin/messages.css | 158 ++++---- assets/js/admin-messages.js | 251 +++++++++++-- src/Admin/Native/UsersMenusMessages.php | 341 ++---------------- .../Native/UsersMenusMessagesCompose.php | 109 ++++++ src/Admin/Native/UsersMenusMessagesList.php | 197 ++++++++++ src/Messages/NotificationAdminQuery.php | 22 +- .../message-admin-contract.test.cjs | 102 +++++- tests/widget-system/message-admin-harness.php | 163 +++++++++ .../message-admin-runtime.test.cjs | 321 +++++++++++++++++ 9 files changed, 1242 insertions(+), 422 deletions(-) create mode 100644 src/Admin/Native/UsersMenusMessagesCompose.php create mode 100644 src/Admin/Native/UsersMenusMessagesList.php create mode 100644 tests/widget-system/message-admin-harness.php create mode 100644 tests/widget-system/message-admin-runtime.test.cjs diff --git a/assets/css/admin/messages.css b/assets/css/admin/messages.css index 7aa7f846..3510dc3b 100644 --- a/assets/css/admin/messages.css +++ b/assets/css/admin/messages.css @@ -1,79 +1,20 @@ -/* 消息管理(users.php?page=aksrt_messages)——仅本页入队。 - * 页面外壳 / 概览 / 表单栅格 / 筛选条 / 空状态均走 native-ui.css 的共享类, - * 本文件只保留本页专属样式:发送弹窗、收件人模式切换、类型徽章、读取状态、用户单元格、列宽。 */ - +/* 消息管理原生页。共享卡片 / 表单 / 筛选沿用 native-ui.css 的 --an-* 令牌。 */ +.aksrt-messages-admin [hidden] { display: none; } .aksrt-messages-admin__muted { color: var(--an-muted); } .aksrt-messages-admin__sub { margin-top: 3px; color: var(--an-muted); font-size: 12px; } .aksrt-messages-admin__sub code { padding: 0 4px; font-size: 12px; background: var(--an-bg-soft); } - -/* ---- 页头按钮:图标与文字居中对齐( 与 ', - esc_attr(self::MESSAGE_COMPOSE_ID), - esc_html__('发送系统消息', 'aksrt'), - esc_attr__('关闭', 'aksrt') - ); - echo '
'; - - echo '
' . esc_html__('收件人', 'aksrt') . ''; - printf( - '', - esc_html__('指定用户', 'aksrt') - ); - printf( - '', - esc_html__('按角色', 'aksrt') - ); - echo '
'; - - echo '
'; - printf( - '', - esc_html__('用户', 'aksrt'), - esc_attr__('用户 ID / 用户名 / 邮箱,逗号或换行分隔', 'aksrt'), - esc_html(sprintf(__('单次最多 %d 位。', 'aksrt'), AdminBroadcast::MAX_USERS)) - ); - echo ''; - printf( - '', - esc_html__('标题', 'aksrt'), - (int) AdminBroadcast::TITLE_MAX, - esc_attr__('留空则取内容开头', 'aksrt') - ); - printf( - '', - esc_html__('跳转链接(可选)', 'aksrt') - ); - printf( - '', - esc_html__('内容', 'aksrt'), - (int) AdminBroadcast::CONTENT_MAX - ); - echo '
'; - echo '

' . esc_html__('以「系统通知」类型逐人投递;邮件 / 公众号提醒按通知配置同步触发。', 'aksrt') . '

'; - echo '
'; - printf( - '
', - esc_html__('取消', 'aksrt'), - esc_html__('发送', 'aksrt') - ); - echo ''; - } - - /** @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 = '
'; - } - - private function render_message_filter_form(array $filters): void { - echo '
'; - echo ''; - if ($filters['status'] !== '') { - echo ''; - } - echo ''; - echo ''; - printf( - '', - esc_html__('按收件人筛选', 'aksrt'), - esc_attr($filters['user']), - esc_attr__('收件人 ID / 用户名 / 邮箱', 'aksrt') - ); - printf( - '', - esc_html__('搜索消息', 'aksrt'), - esc_attr($filters['s']), - esc_attr__('标题 / 内容', 'aksrt') - ); - submit_button(__('筛选', 'aksrt'), '', '', false); - if ($filters['type'] !== '' || $filters['user'] !== '' || $filters['s'] !== '') { - printf( - '%s', - esc_url($this->message_page_url($filters['status'] === '' ? [] : ['status' => $filters['status']])), - esc_html__('清除筛选', 'aksrt') - ); - } - echo '
'; - } - - /** @param list> $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 '
'; - echo ''; - wp_nonce_field('aksrt_users_bulk_messages'); - foreach ($this->message_filter_args($filters) as $key => $value) { - printf('', esc_attr($key), esc_attr((string) $value)); - } - echo ''; - - echo '
'; - printf( - ' ', - esc_html__('标记已读', 'aksrt') - ); - printf( - '', - esc_js(__('确定删除所选消息?删除后用户收件箱中也将不再显示。', 'aksrt')), - esc_html__('删除', 'aksrt') - ); - echo '

'; - - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - - 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 '
' . esc_html__('消息', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('收件人', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('发送时间', 'aksrt') . '
'; - } - - /** @param array $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 ''; - printf( - '', - $id, - esc_html(sprintf(__('选择消息 #%d', 'aksrt'), $id)) - ); - - echo '' . esc_html($title) . ''; - if ($desc !== '' && $desc !== $title) { - echo '
' . esc_html(mb_strimwidth($desc, 0, 160, '…')) . '
'; - } - echo '
#' . esc_html((string) $id) . ''; - if ($url !== '') { - printf( - ' %s%s', - esc_url($url), - esc_html__('跳转链接', 'aksrt'), - esc_html__('(在新窗口打开)', 'aksrt') - ); - } - echo '
'; - - printf( - '%s', - 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 '' . $this->message_user_cell((int) ($row['user_id'] ?? 0), $filters) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in builder. - - $read_at = (string) ($row['read_at'] ?? ''); - printf( - '%s', - $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( - '%s', - esc_attr($ts ? gmdate('c', $ts) : ''), - esc_html($this->message_time($created)), - $ts ? '
' . esc_html(sprintf(__('%s前', 'aksrt'), human_time_diff($ts))) . '
' : '' - ); - echo ''; - } - - private function message_user_cell(int $user_id, array $filters): string { - $user = $user_id > 0 ? get_userdata($user_id) : false; - if (!$user) { - return '' . esc_html(sprintf(__('已删除用户 #%d', 'aksrt'), $user_id)) . ''; - } - $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 '' - . '' - . '' . esc_html($user->display_name) . '' - . '
#' . esc_html((string) $user_id) . ' · ' . esc_html__('编辑', 'aksrt') . '
'; - } - - /* ------------------------------------------------------------------ - * 参数与 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 非空筛选参数(用于 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') : ''; } diff --git a/src/Admin/Native/UsersMenusMessagesCompose.php b/src/Admin/Native/UsersMenusMessagesCompose.php new file mode 100644 index 00000000..c5e6238f --- /dev/null +++ b/src/Admin/Native/UsersMenusMessagesCompose.php @@ -0,0 +1,109 @@ +', + esc_attr(self::MESSAGE_COMPOSE_ID), esc_attr(self::MESSAGE_COMPOSE_ID), $open ? ' data-aksrt-dialog-autoopen' : '' + ); + echo '
__('最多输入 {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 ''; + echo ''; + wp_nonce_field('aksrt_users_send_message'); + $this->render_message_dialog_head(self::MESSAGE_COMPOSE_ID, __('发送系统消息', 'aksrt'), 'dashicons-megaphone'); + echo '
'; + if ($open) { + $error = sanitize_text_field((string) wp_unslash($_GET['aksrt_msg_error'] ?? __('消息发送失败,请检查后重试。', 'aksrt'))); + echo '

' . esc_html($error) . '

'; + } + echo '
' . esc_html__('收件人', 'aksrt') . ''; + foreach (['users' => [__('指定用户', 'aksrt'), 'dashicons-admin-users'], 'role' => [__('按角色群发', 'aksrt'), 'dashicons-groups']] as $value => [$label, $icon]) { + echo ''; + } + echo '
'; + printf( + '', + esc_html__('用户', 'aksrt'), esc_attr__('用户 ID / 用户名 / 邮箱,逗号或换行分隔', 'aksrt'), esc_html(sprintf(__('单次最多 %d 位。', 'aksrt'), AdminBroadcast::MAX_USERS)) + ); + echo ''; + printf( + '', + esc_html__('标题', 'aksrt'), AdminBroadcast::TITLE_MAX, esc_attr__('留空则取内容开头', 'aksrt') + ); + echo ''; + printf( + '', + esc_html__('内容', 'aksrt'), AdminBroadcast::CONTENT_MAX + ); + echo '

' . esc_html__('以「系统通知」类型逐人投递;邮件 / 公众号提醒按通知配置同步触发。', 'aksrt') . '

'; + echo '
'; + echo '
'; + } + + private function render_message_dialogs(): void { + echo '
'; + $this->render_message_dialog_head('aksrt-msg-detail', __('消息详情', 'aksrt'), 'dashicons-email-alt'); + echo '
'; + echo '
'; + + echo ' __('删除消息', 'aksrt'), + 'delete-text' => __('确定删除 {count} 条消息?删除后用户收件箱中也将不再显示,此操作不可撤销。', 'aksrt'), + 'delete-send' => __('确认删除', 'aksrt'), + ] as $key => $text) { + echo ' data-i18n-' . esc_attr($key) . '="' . esc_attr($text) . '"'; + } + echo '>
'; + $this->render_message_dialog_head('aksrt-msg-confirm', __('确认操作', 'aksrt'), 'dashicons-warning'); + echo '

'; + echo '
'; + echo '
'; + } + + private function render_message_dialog_head(string $id, string $title, string $icon): void { + echo '

'; + echo '' . esc_html($title) . '

'; + echo '
'; + } +} diff --git a/src/Admin/Native/UsersMenusMessagesList.php b/src/Admin/Native/UsersMenusMessagesList.php new file mode 100644 index 00000000..f58cf6b7 --- /dev/null +++ b/src/Admin/Native/UsersMenusMessagesList.php @@ -0,0 +1,197 @@ + __('消息总数', '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 = '
    '; + foreach ($views as $key => [$label, $count]) { + $html .= sprintf( + '
  • %s (%s)
  • ', + 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 . '
'; + } + + private function render_message_filter_form(array $filters): void { + echo '
'; + echo ''; + if ($filters['status'] !== '') { + echo ''; + } + echo ''; + echo ''; + echo ''; + echo ''; + printf( + '', + esc_html__('按收件人筛选', 'aksrt'), esc_attr($filters['user']), esc_attr__('收件人 ID / 用户名 / 邮箱', 'aksrt') + ); + printf( + '', + 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 '' . esc_html__('清除筛选', 'aksrt') . ''; + } + echo '
'; + } + + 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 '
'; + echo ''; + wp_nonce_field('aksrt_users_bulk_messages'); + foreach ($this->message_filter_args($filters) as $key => $value) { + printf('', esc_attr($key), esc_attr((string) $value)); + } + echo ''; + echo ''; + echo '
'; + echo ''; + echo ''; + echo ''; + echo '
'; + echo ''; + echo ''; + foreach (['title' => __('消息', 'aksrt'), 'type' => __('类型', 'aksrt'), 'user' => __('收件人', 'aksrt'), 'read' => __('状态', 'aksrt'), 'time' => __('发送时间', 'aksrt')] as $column => $label) { + echo ''; + } + echo ''; + 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 '
' . esc_html($label) . '
'; + } + + 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 ''; + printf( + '', + $id, esc_html(sprintf(__('选择消息 #%d', 'aksrt'), $id)) + ); + echo ''; + if ($desc !== '' && $desc !== $title) { + echo '
' . esc_html(mb_strimwidth($desc, 0, 160, '…')) . '
'; + } + echo '
#' . esc_html((string) $id) . '
'; + echo '
'; + printf('', $id, esc_html__('查看', 'aksrt')); + if ($unread) { + printf('', $id, esc_html__('标记已读', 'aksrt')); + } + printf('', $id, esc_html__('删除', 'aksrt')); + echo '
'; + $this->render_message_detail_template($row); + echo ''; + printf( + '%s', + 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 '' . $this->message_user_cell((int) $row['user_id'], $filters) . ''; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes. + $read_at = (string) ($row['read_at'] ?? ''); + printf( + '%s', + $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( + '%s', + esc_attr($ts ? gmdate('c', $ts) : ''), esc_html($this->message_time($created)), + $ts ? '
' . esc_html(sprintf(__('%s前', 'aksrt'), human_time_diff($ts))) . '
' : '' + ); + echo ''; + } + + private function render_message_detail_template(array $row): void { + $user = get_userdata((int) $row['user_id']); + $recipient = $user ? $user->display_name : __('已删除用户', 'aksrt'); + echo ''; + } + + private function message_user_cell(int $user_id, array $filters): string { + $user = $user_id > 0 ? get_userdata($user_id) : false; + if (!$user) { + return '' . esc_html(sprintf(__('已删除用户 #%d', 'aksrt'), $user_id)) . ''; + } + $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 '' + . '' + . '' . esc_html($user->display_name) . '' + . '
#' . esc_html((string) $user_id) . ' · ' . esc_html__('编辑', 'aksrt') . '
'; + } +} diff --git a/src/Messages/NotificationAdminQuery.php b/src/Messages/NotificationAdminQuery.php index 0a531e68..3031eca4 100644 --- a/src/Messages/NotificationAdminQuery.php +++ b/src/Messages/NotificationAdminQuery.php @@ -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>,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); diff --git a/tests/widget-system/message-admin-contract.test.cjs b/tests/widget-system/message-admin-contract.test.cjs index dd24cbe5..03251053 100644 --- a/tests/widget-system/message-admin-contract.test.cjs +++ b/tests/widget-system/message-admin-contract.test.cjs @@ -33,10 +33,14 @@ test('write handlers check capability and nonce', () => { const page = read('src/Admin/Native/UsersMenusMessages.php'); assert.match(page, /function handle_send_message\(\): void \{\s*\$this->ensure_cap\(\);\s*check_admin_referer\('aksrt_users_send_message'\);/); assert.match(page, /function handle_bulk_messages\(\): void \{\s*\$this->ensure_cap\(\);\s*check_admin_referer\('aksrt_users_bulk_messages'\);/); - assert.ok(page.includes("wp_nonce_field('aksrt_users_send_message')")); - assert.ok(page.includes("wp_nonce_field('aksrt_users_bulk_messages')")); + const compose = read('src/Admin/Native/UsersMenusMessagesCompose.php'); + const list = read('src/Admin/Native/UsersMenusMessagesList.php'); + assert.ok(page.includes('use UsersMenusMessagesCompose;')); + assert.ok(page.includes('use UsersMenusMessagesList;')); + assert.ok(compose.includes("wp_nonce_field('aksrt_users_send_message')")); + assert.ok(list.includes("wp_nonce_field('aksrt_users_bulk_messages')")); // 页面层不直接碰数据库,分页走共享组件。 - assert.doesNotMatch(page, /\$wpdb/); + assert.doesNotMatch(page + compose + list, /\$wpdb/); assert.ok(page.includes('AdminListPagination::render(')); }); @@ -53,3 +57,95 @@ test('dead notification helpers are removed', () => { assert.doesNotMatch(read('src/Messages/Message.php'), /get_all_notifications/); assert.ok(fs.existsSync(path.join(root, 'assets/css/admin/messages.css'))); }); + +let rendered; +function harness() { + if (!rendered) { + const result = require('node:child_process').spawnSync('php', [path.join(__dirname, 'message-admin-harness.php')], { encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr + result.stdout); + rendered = JSON.parse(result.stdout); + } + return rendered; +} + +test('list toolbar is merged into the table card and inline confirmation is removed', () => { + const page = read('src/Admin/Native/UsersMenusMessages.php'); + const list = read('src/Admin/Native/UsersMenusMessagesList.php'); + assert.ok(page.indexOf("NativeUi::card_open(__('站内通知'") < page.indexOf('$this->render_message_filter_form')); + assert.doesNotMatch(page + list, /onclick=|confirm\(| { + const { table, cache_calls } = harness(); + assert.match(table, /