register_default_tabs(); add_action('rest_api_init', [$this, 'register_rest_routes']); } /** * Register public user-home panel fragment routes. */ public function register_rest_routes(): void { register_rest_route('aksrt/v1', '/user-home/panel/(?P\d+)/(?P[a-z0-9_-]+)', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [$this, 'rest_panel'], 'permission_callback' => '__return_true', 'args' => [ 'user_id' => [ 'required' => true, 'sanitize_callback' => 'absint', 'validate_callback' => static function ($value): bool { return (int) $value > 0; }, ], 'tab' => [ 'required' => true, 'sanitize_callback' => 'sanitize_key', 'validate_callback' => static function ($value): bool { return preg_match('/^[a-z0-9_-]+$/', (string) $value) === 1; }, ], 'page' => [ 'required' => false, 'default' => 1, 'sanitize_callback' => 'absint', ], 'sub' => [ 'required' => false, 'default' => 'followers', 'sanitize_callback' => 'sanitize_key', 'validate_callback' => static function ($value): bool { return in_array((string) $value, ['followers', 'following'], true); }, ], ], ]); } /** * Render a public user-home panel body fragment. */ public function rest_panel(\WP_REST_Request $request) { $user = get_userdata((int) $request->get_param('user_id')); if (!$user) { return new \WP_Error('aksrt_user_home_user', __('用户不存在', 'aksrt'), ['status' => 404]); } $tab = sanitize_key((string) $request->get_param('tab')); $page = max(1, (int) $request->get_param('page')); $sub = (string) $request->get_param('sub') === 'following' ? 'following' : 'followers'; if (!function_exists('aksrt_get_user_home_page_context')) { require_once AKSRT_DIR . '/inc/helpers/aksrt-user-home.php'; } $context = aksrt_get_user_home_page_context($user); if (!in_array($tab, (array) ($context['enabled_tabs'] ?? []), true)) { return new \WP_Error('aksrt_user_home_panel', __('用户主页面板不存在', 'aksrt'), ['status' => 404]); } $context['active_tab'] = $tab; $context['follow_sub'] = $sub; $page_keys = [ 'posts' => 'posts_page', 'comments' => 'comments_page', 'favorites' => 'favorites_page', 'follow' => 'follow_page', ]; if (isset($page_keys[$tab])) { $context[$page_keys[$tab]] = $page; } $html = aksrt_capture_user_home_panel($tab, $context); if ($html === '') { return new \WP_Error('aksrt_user_home_panel_empty', __('用户主页面板暂不可用', 'aksrt'), ['status' => 404]); } return rest_ensure_response([ 'success' => true, 'data' => [ 'tab' => $tab, 'html' => $html, ], ]); } /** * 注册默认公开 Tab */ private function register_default_tabs(): void { $this->tabs = [ 'posts' => [ 'label' => __('文章', 'aksrt'), 'icon' => 'file-alt', 'priority' => 10, ], 'comments' => [ 'label' => __('评论', 'aksrt'), 'icon' => 'comment', 'priority' => 20, ], 'favorites' => [ 'label' => __('收藏', 'aksrt'), 'icon' => 'star', 'priority' => 30, ], 'credits' => [ 'label' => class_exists('\AKSRT\Settings\Settings') ? sprintf(__('我的%s', 'aksrt'), \AKSRT\Settings\Settings::get_currency_name()) : __('我的积分', 'aksrt'), 'icon' => 'wallet', 'priority' => 40, ], 'follow' => [ 'label' => __('粉丝', 'aksrt'), 'icon' => 'user-plus', 'priority' => 80, ], ]; /** * 允许插件/子主题注册额外公开 Tab * * @param array $tabs Tab 注册表 */ $this->tabs = apply_filters('aksrt_user_home_tabs', $this->tabs); } /** * 获取 Tab 注册表 */ public function get_tabs(): array { return $this->tabs; } /** * 获取当前用户主页实际可见的公开 Tab。 */ public function get_visible_tabs(bool $is_own_profile = false): array { $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_user_home_settings() : [ 'tabs' => ['posts', 'comments', 'favorites'], ]; $visible = []; foreach ((array) ($settings['tabs'] ?? []) as $key) { if (!isset($this->tabs[$key])) { continue; } if ($key === 'credits' && (!$is_own_profile || !class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::credits_enabled())) { continue; } $visible[$key] = $this->tabs[$key]; } if (empty($visible)) { foreach (['posts', 'comments', 'favorites'] as $fallback) { if (isset($this->tabs[$fallback])) { $visible[$fallback] = $this->tabs[$fallback]; } } } return $visible; } /** * 获取公开展示的身份信号(认证 / 等级 / 徽章)。 * * @return array */ public function get_profile_signals(int $user_id): array { if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) { return []; } $settings = \AKSRT\Settings\Settings::get_user_identity_settings(); $signals = []; if (!empty($settings['verify']['enabled']) && !empty($settings['verify']['public_display']) && $this->is_user_verified($user_id, $settings['verify'])) { $resolved = $this->resolve_verified_type($user_id, $settings['verify']); $signals[] = [ 'type' => 'verify', 'label' => $resolved['label'], 'title' => $resolved['label'], 'icon' => 'check-circle', 'icon_url' => get_theme_file_uri('/assets/img/user/user-certification.svg'), 'tone' => 'success', ]; } if (!empty($settings['level']['enabled']) && !empty($settings['level']['public_display'])) { $level = $this->get_user_level($user_id, $settings['level']); if ($level > 0) { $signals[] = [ 'type' => 'level', 'label' => Level::get_level_name($level), 'title' => Level::get_level_name($level), 'icon' => 'layer-group', 'icon_url' => Level::get_level_icon_url($level), 'tone' => 'info', ]; } } if (!empty($settings['medal']['enabled']) && !empty($settings['medal']['public_display'])) { $limit = max(1, (int) ($settings['medal']['limit'] ?? 3)); if (class_exists('AKSRT\\User\\Badges')) { $list = Badges::list_for_user($user_id); $badge_items = []; $worn_item = null; foreach ((array) ($list['categories'] ?? []) as $cat) { foreach ((array) ($cat['items'] ?? []) as $item) { if (empty($item['owned'])) { continue; } if (!empty($item['wear'])) { $worn_item = $item; } $badge_items[] = $item; } } // Keep the worn badge visible even when it falls after the configured preview limit. if (is_array($worn_item)) { $signals[] = [ 'type' => 'badge', 'slug' => (string) ($worn_item['slug'] ?? ''), 'label' => (string) ($worn_item['label'] ?? ''), 'icon' => 'award', 'tone' => 'info', 'icon_url' => (string) ($worn_item['icon_url'] ?? ''), 'wear' => true, ]; } $badge_count = is_array($worn_item) ? 1 : 0; foreach ($badge_items as $item) { if (!empty($item['wear'])) { continue; } if ($badge_count >= $limit) { break; } $signals[] = [ 'type' => 'badge', 'slug' => (string) ($item['slug'] ?? ''), 'label' => (string) ($item['label'] ?? ''), 'icon' => 'award', 'tone' => 'info', 'icon_url' => (string) ($item['icon_url'] ?? ''), 'wear' => false, ]; $badge_count++; } } } return $signals; } /** * 解析"更多资料"弹窗用的认证三态标识。 * * @return array{state:string,label:string} state 为空串表示认证公开展示已关闭; * 否则为 none|personal|enterprise,label 为对应展示文案(与名字旁认证 chip 同源)。 */ public function get_verify_state(int $user_id): array { if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) { return ['state' => '', 'label' => '']; } $verify = \AKSRT\Settings\Settings::get_user_identity_settings()['verify'] ?? []; $verify = is_array($verify) ? $verify : []; if (empty($verify['enabled']) || empty($verify['public_display'])) { return ['state' => '', 'label' => '']; } if (!$this->is_user_verified($user_id, $verify)) { return ['state' => 'none', 'label' => __('未认证', 'aksrt')]; } $resolved = $this->resolve_verified_type($user_id, $verify); // 邮箱核验通过等无申请类型的场景与名字旁 chip 一致,回落到首个已配置类型。 $state = in_array($resolved['state'], ['personal', 'enterprise'], true) ? $resolved['state'] : 'personal'; return ['state' => $state, 'label' => $resolved['label']]; } /** * @return array{state:string,label:string} state 为申请类型 personal|enterprise(可为空串),label 为展示文案。 */ private function resolve_verified_type(int $user_id, array $verify): array { $info = class_exists('AKSRT\\User\\AuthApply') ? AuthApply::get_auth_info($user_id) : []; $application = class_exists('AKSRT\\User\\AuthApply') ? AuthApply::get_apply_payload($user_id) : []; $type = sanitize_key((string) ($info['type'] ?? $application['type'] ?? '')); $type_settings = is_array($verify['types'][$type] ?? null) ? $verify['types'][$type] : []; $label = trim((string) ($type_settings['badge_text'] ?? $type_settings['label'] ?? '')); if ($label === '') { foreach ((array) ($verify['types'] ?? []) as $configured_type) { if (is_array($configured_type) && !empty($configured_type['badge_text'])) { $label = trim((string) $configured_type['badge_text']); break; } } } return ['state' => $type, 'label' => $label]; } /** * 获取公开治理提示。 * * @return array{state:string,text:string}|array{} */ public function get_profile_notice(int $user_id): array { if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) { return []; } $settings = \AKSRT\Settings\Settings::get_user_identity_settings(); if (empty($settings['report']['enabled']) || empty($settings['report']['public_notice'])) { return []; } $state = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_ban_state', true)); if (!in_array($state, ['muted', 'restricted', 'banned'], true)) { return []; } return [ 'state' => $state, 'text' => (string) $settings['report']['ban_notice_text'], ]; } /** * 公开主页是否显示举报入口。 */ public function show_report_entry(): bool { if (!class_exists('\AKSRT\Settings\Settings')) { return false; } $settings = \AKSRT\Settings\Settings::get_user_identity_settings(); return !empty($settings['report']['enabled']) && !empty($settings['report']['public_entry']); } /** * @param array{accept_email_verified:bool} $verify_settings */ private function is_user_verified(int $user_id, array $verify_settings): bool { if (class_exists('\\AKSRT\\User\\AuthApply') && AuthApply::is_verified($user_id)) { return true; } if (!empty($verify_settings['accept_email_verified']) && class_exists('\AKSRT\Auth\Service')) { return \AKSRT\Auth\Service::user_is_email_verified($user_id); } return false; } /** * @param array{source?:string,thresholds?:array} $level_settings */ private function get_user_level(int $user_id, array $level_settings): int { unset($level_settings); return class_exists('AKSRT\\User\\Level') ? Level::get_level($user_id) : 0; } /** * 判断当前页面是否为公开用户主页 */ public static function is_page(): bool { return function_exists('aksrt_is_user_home_page') && aksrt_is_user_home_page(); } }