Files
wordpress-theme-aksrt/inc/classes/class-aksrt-admin.php
T
kate522 00d335ba16 feat(comments): AJAX fragment list/submit + load-more parity
Add REST comment/list + comment/submit HTML fragments, shared query
renderer for order/author/hottest, fix load-more policy and DOM
targets, and stop full-page scrapes for order/author/reload.
2026-07-12 08:59:50 +08:00

296 lines
11 KiB
PHP

<?php
/**
* AKSRT Theme Admin
*
* Admin SPA container + legacy REST compatibility façade.
* New domain routes live in inc/classes/admin/* controllers, while this class
* remains the orchestrator and keeps only shell/bootstrap and public AJAX
* helpers that are intentionally centralized.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Admin {
private static $instance = null;
private const PAGE_SLUG = 'aksrt-settings';
private const API_NAMESPACE = 'aksrt/v1';
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {}
public function init(): void {
add_action('admin_menu', [$this, 'add_admin_menu']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
add_filter('script_loader_tag', [$this, 'add_module_type_attribute'], 10, 2);
add_action('rest_api_init', [$this, 'register_rest_routes']);
add_action('wp_ajax_aksrt_load_more_posts', [$this, 'ajax_load_more_posts']);
add_action('wp_ajax_nopriv_aksrt_load_more_posts', [$this, 'ajax_load_more_posts']);
add_action('wp_ajax_aksrt_load_more_comments', [$this, 'ajax_load_more_comments']);
add_action('wp_ajax_nopriv_aksrt_load_more_comments', [$this, 'ajax_load_more_comments']);
}
public function add_admin_menu(): void {
add_menu_page(
'AKSRT 主题设置',
'AKSRT',
'manage_options',
self::PAGE_SLUG,
[$this, 'render_admin_page'],
'dashicons-admin-generic',
58
);
}
public function render_admin_page(): void {
echo '<div class="wrap aksrt-admin-wrap">';
echo '<div id="aksrt-admin-app"></div>';
echo '</div>';
}
public function enqueue_admin_assets($hook): void {
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen || $screen->base !== 'toplevel_page_' . self::PAGE_SLUG) {
return;
}
$build_dir = trailingslashit(AKSRT_DIR) . 'assets/admin/build/';
$build_uri = trailingslashit(AKSRT_URI) . 'assets/admin/build/';
$entry = $build_dir . 'index.js';
if (!is_file($entry)) {
add_action('admin_notices', static function (): void {
if (!current_user_can('manage_options')) {
return;
}
echo '<div class="notice notice-error"><p>';
echo esc_html__('AKSRT 管理后台构建缺失:请在 assets/admin 执行 npm run build,并完整部署 build/ 目录。', 'aksrt');
echo '</p></div>';
});
return;
}
$missing_chunks = self::find_missing_admin_chunks($build_dir, $entry);
if ($missing_chunks !== []) {
add_action('admin_notices', static function () use ($missing_chunks): void {
if (!current_user_can('manage_options')) {
return;
}
echo '<div class="notice notice-error"><p>';
echo esc_html__('AKSRT 管理后台构建不完整(动态模块缺失),请重新完整部署 assets/admin/build/,不要只覆盖 index.js。缺失:', 'aksrt');
echo ' ' . esc_html(implode(', ', array_slice($missing_chunks, 0, 8)));
if (count($missing_chunks) > 8) {
echo '…';
}
echo '</p></div>';
});
// Still enqueue entry so console shows the real import error if an admin insists;
// notice makes the deploy mistake explicit.
}
$css_files = [
'naive-ui.css',
'index.css',
'index2.css',
];
foreach ($css_files as $css_file) {
$path = $build_dir . $css_file;
if (file_exists($path)) {
wp_enqueue_style(
'aksrt-admin-' . sanitize_key(str_replace(['.', '/'], '-', $css_file)),
$build_uri . $css_file,
[],
(string) filemtime($path)
);
}
}
wp_enqueue_script(
'aksrt-admin-app',
$build_uri . 'index.js',
[],
(string) filemtime($entry),
true
);
wp_localize_script('aksrt-admin-app', 'aksrtAdminData', [
'apiUrl' => esc_url_raw(rest_url(self::API_NAMESPACE)),
'nonce' => wp_create_nonce('wp_rest'),
'siteUrl' => home_url('/'),
'adminUrl' => admin_url('/'),
'themeVersion' => wp_get_theme()->get('Version') ?: '3.4.0-dev',
'buildOk' => $missing_chunks === [],
]);
wp_enqueue_media();
}
/**
* Parse index.js dynamic imports and report missing chunk files.
*
* @return string[]
*/
private static function find_missing_admin_chunks(string $build_dir, string $entry_file): array {
$src = file_get_contents($entry_file);
if (!is_string($src) || $src === '') {
return ['index.js unreadable'];
}
if (!preg_match_all('/\\.\\/([A-Za-z0-9_.-]+\\.js)/', $src, $matches)) {
return [];
}
$missing = [];
foreach (array_unique($matches[1]) as $chunk) {
if (!is_file($build_dir . $chunk)) {
$missing[] = $chunk;
}
}
return $missing;
}
public function add_module_type_attribute(string $tag, string $handle): string {
if ('aksrt-admin-app' === $handle) {
return str_replace('<script ', '<script type="module" ', $tag);
}
return $tag;
}
public function register_rest_routes(): void {
$controllers = [
'AKSRT_Admin_Global_Controller',
'AKSRT_Admin_Page_Display_Controller',
'AKSRT_Admin_Post_List_Controller',
'AKSRT_Admin_Permissions_Controller',
'AKSRT_Admin_User_Controller',
'AKSRT_Admin_Payment_Controller',
'AKSRT_Admin_Tools_Controller',
'AKSRT_Admin_Import_Export_Controller',
'AKSRT_Admin_Shop_Controller',
'AKSRT_Admin_Forum_Controller',
'AKSRT_Admin_Extension_Controller',
'AKSRT_Admin_Comment_Controller',
'AKSRT_Admin_Message_Controller',
'AKSRT_Admin_AI_Controller',
];
foreach ($controllers as $class_name) {
if (!class_exists($class_name) || !method_exists($class_name, 'get_instance')) {
continue;
}
$controller = $class_name::get_instance();
if (method_exists($controller, 'register_routes')) {
$controller->register_routes();
}
}
}
public static function sanitize_public_query_vars(array $vars): array {
$allowed = [
'post_type', 'post_status', 'posts_per_page', 'paged', 'orderby', 'order',
'cat', 'tag', 'tag_id', 'category_name', 's', 'post__not_in', 'category__not_in',
];
$clean = [];
foreach ($allowed as $key) {
if (!array_key_exists($key, $vars)) {
continue;
}
$value = $vars[$key];
if (is_array($value)) {
$clean[$key] = array_map('sanitize_text_field', $value);
} else {
$clean[$key] = sanitize_text_field((string) $value);
}
}
if (isset($clean['posts_per_page'])) {
$clean['posts_per_page'] = max(1, min(50, (int) $clean['posts_per_page']));
}
if (isset($clean['paged'])) {
$clean['paged'] = max(1, (int) $clean['paged']);
}
return $clean;
}
public static function sanitize_public_render_config(array $config): array {
$clean = [];
if (isset($config['mode'])) {
$clean['mode'] = sanitize_key((string) $config['mode']);
}
if (isset($config['context'])) {
$clean['context'] = sanitize_key((string) $config['context']);
}
if (isset($config['query_context'])) {
$clean['query_context'] = sanitize_key((string) $config['query_context']);
}
return $clean;
}
public function ajax_load_more_posts(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$page = max(1, (int) ($_POST['page'] ?? 1));
$query_vars = json_decode(wp_unslash((string) ($_POST['query_vars'] ?? '{}')), true);
$render_config = json_decode(wp_unslash((string) ($_POST['render_config'] ?? '{}')), true);
$query_vars = is_array($query_vars) ? self::sanitize_public_query_vars($query_vars) : [];
$render_config = is_array($render_config) ? self::sanitize_public_render_config($render_config) : [];
$query_vars['paged'] = $page;
$query = new WP_Query($query_vars);
ob_start();
if (function_exists('aksrt_render_post_list')) {
aksrt_render_post_list($query, $render_config, true);
}
$html = ob_get_clean();
wp_send_json_success([
'html' => $html,
'has_more' => $page < (int) $query->max_num_pages,
'page' => $page,
]);
}
public function ajax_load_more_comments(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$post_id = absint($_POST['post_id'] ?? 0);
$page = max(1, (int) ($_POST['page'] ?? 1));
if ($post_id <= 0) {
wp_send_json_error(['message' => __('无效的文章 ID', 'aksrt')]);
}
$order = sanitize_key((string) ($_POST['comment_order'] ?? $_POST['order'] ?? 'newest'));
$author_only = !empty($_POST['author_only']) || (isset($_POST['aksrt_author_only']) && (int) $_POST['aksrt_author_only'] === 1);
if (!function_exists('aksrt_comments_query_for_post') || !function_exists('aksrt_comments_render_items_html')) {
wp_send_json_error(['message' => __('评论加载不可用', 'aksrt')]);
}
$result = aksrt_comments_query_for_post($post_id, [
'page' => $page,
'order' => $order,
'author_only' => $author_only,
]);
$html = aksrt_comments_render_items_html($result['comments'], $result['settings'], $result['order']);
wp_send_json_success([
'html' => $html,
'has_more' => $page < (int) $result['max_pages'],
'page' => $page,
'order' => $result['order'],
'total' => (int) $result['total'],
'max_pages'=> (int) $result['max_pages'],
]);
}
}