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, /