diff --git a/assets/css/frontend/single-post/author-actions.css b/assets/css/frontend/single-post/author-actions.css index e04a84b2..dbef9bf5 100644 --- a/assets/css/frontend/single-post/author-actions.css +++ b/assets/css/frontend/single-post/author-actions.css @@ -548,3 +548,17 @@ html.dark .post-drop-meta__item:hover { .article-tags .but.c-green { color: #5cb85c; } .article-tags .but.c-purple { color: #9c27b0; } .article-tags .but.c-red { color: #f44336; } + + +/* ===== Zibll user-card after-box ===== */ +.user-card.author.zib-widget { margin-bottom: 16px; overflow: hidden; } +.user-card.author .user-avatar { display: flex; justify-content: center; } +.user-card.author .user-avatar .avatar, +.user-card.author .user-avatar img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; } +.user-card.author .user-name { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 4px; } +.user-card.author .user-name .display-name { font-weight: 700; color: var(--key-color, inherit); } +.user-card.author .user-desc { text-align: center; } +.user-card.author .user-btns { text-align: center; } +.user-card.author .user-card-posts ul { list-style: none; margin: 0; padding: 0; } +.user-card.author .user-card-posts li { padding: 6px 0; border-bottom: 1px dashed var(--color-line, rgba(0,0,0,.06)); } +.user-card.author .user-card-posts li:last-child { border-bottom: 0; } diff --git a/assets/css/frontend/single-post/cover.css b/assets/css/frontend/single-post/cover.css index d7e73991..f37a4a2f 100644 --- a/assets/css/frontend/single-post/cover.css +++ b/assets/css/frontend/single-post/cover.css @@ -273,3 +273,19 @@ .article-avatar .display-name { margin-right: 2px; } + + +/* Slide cover overlay (injected by aksrt_render_slider overlay_html) */ +.aksrt-slider-wrapper--has-overlay, +.aksrt-slider-wrapper--scale { position: relative; overflow: hidden; border-radius: var(--radius-2xl, 16px); } +.aksrt-slider-wrapper--scale .new-swiper { position: absolute; inset: 0; width: 100%; height: 100%; } +.aksrt-slider-wrapper--scale .swiper-slide img.fit-cover { width: 100%; height: 100%; object-fit: cover; } +.aksrt-slider-overlay { position: absolute; inset: 0; z-index: 3; pointer-events: none; } +.aksrt-slider-overlay .abs-center.left-bottom { pointer-events: none; padding: 18px 20px; color: #fff; } +.aksrt-slider-overlay .article-title, +.aksrt-slider-overlay .article-title.title-h-left { color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,.45); } +.aksrt-slider-overlay .breadcrumb a, +.aksrt-slider-overlay .breadcrumb li { color: rgba(255,255,255,.9); } +.aksrt-slider-overlay .single-metabox, +.aksrt-slider-overlay .cover-meta { pointer-events: auto; } +.aksrt-slider-overlay .post-drop-meta { pointer-events: auto; } diff --git a/assets/js/section-navs.js b/assets/js/section-navs.js index 1d82416e..ad70ed9f 100644 --- a/assets/js/section-navs.js +++ b/assets/js/section-navs.js @@ -19,19 +19,31 @@ var navContainer = document.querySelector('[data-nav]'); if (!navContainer) return; + // Single-post TOC is owned by page-template.js (.posts-nav-box .toc-list). + // Do not invent a second .aksrt-section-nav tree on article singles. + if (document.querySelector('.posts-nav-box .toc-list, .posts-nav-box.toc-widget')) { + return; + } + var headings = navContainer.querySelectorAll(HEADING_SEL); if (headings.length < MIN_HEADINGS) return; var tocContainer = document.querySelector('.posts-nav-lists, .aksrt-section-nav'); if (!tocContainer) { + // Docs / page-navs only: create a dedicated container. Never inject into single sidebars. + if (document.body.classList.contains('single') || document.body.classList.contains('single-post')) { + return; + } tocContainer = document.createElement('nav'); tocContainer.className = 'aksrt-section-nav'; tocContainer.setAttribute('aria-label', '文章目录'); var sidebar = document.querySelector('.content-layout__aside, .sidebar, .widget-area'); if (sidebar) { sidebar.insertBefore(tocContainer, sidebar.firstChild); - } else { + } else if (navContainer.parentNode) { navContainer.parentNode.insertBefore(tocContainer, navContainer); + } else { + return; } } diff --git a/assets/js/video-player.js b/assets/js/video-player.js index 22d22241..fe52c533 100644 --- a/assets/js/video-player.js +++ b/assets/js/video-player.js @@ -220,9 +220,16 @@ if (!switchBtn) return; var container = switchBtn.closest('.aksrt-video') || switchBtn.closest('[data-aksrt-video]'); + // Single cover: episodes sit in .single-video-footer; player is sibling under .single-video + if (!container) { + var singleVideo = switchBtn.closest('.single-video, .single-head-cover'); + if (singleVideo) { + container = singleVideo.querySelector('.aksrt-video, [data-aksrt-video]'); + } + } if (!container) return; - var src = switchBtn.dataset.src || switchBtn.getAttribute('href'); + var src = switchBtn.dataset.src || switchBtn.getAttribute('video-url') || switchBtn.getAttribute('href'); if (!src) return; e.preventDefault(); @@ -237,9 +244,14 @@ } // 高亮当前集 - var siblings = container.parentElement ? container.parentElement.querySelectorAll('.switch-video, .switch-iframe') : []; - siblings.forEach(function (btn) { btn.classList.remove('is-active'); }); + var scope = switchBtn.closest('.featured-video-episode') || (container.parentElement || document); + var siblings = scope.querySelectorAll('.switch-video, .switch-iframe'); + siblings.forEach(function (btn) { + btn.classList.remove('is-active'); + btn.classList.remove('active'); + }); switchBtn.classList.add('is-active'); + switchBtn.classList.add('active'); }); // ── Public API ── diff --git a/inc/classes/class-aksrt-post-meta-box.php b/inc/classes/class-aksrt-post-meta-box.php index 2849cc15..6ab28132 100644 --- a/inc/classes/class-aksrt-post-meta-box.php +++ b/inc/classes/class-aksrt-post-meta-box.php @@ -43,8 +43,46 @@ class AKSRT_Post_Meta_Box { public static function get_meta(int $post_id): array { $defaults = AKSRT_Domain_Model::default_post_meta(); $saved = get_post_meta($post_id, self::META_KEY, true); - if (!is_array($saved)) $saved = []; - return array_merge($defaults, $saved); + if (!is_array($saved)) { + $saved = []; + } + $meta = array_merge($defaults, $saved); + // New route only: episodes is always a list of {url,title}. + $meta['featured_video_episode'] = self::sanitize_video_episodes($meta['featured_video_episode'] ?? []); + if (!in_array((string) ($meta['show_toc'] ?? ''), ['', '0', '1'], true)) { + $meta['show_toc'] = ''; + } + return $meta; + } + + /** + * Sanitize video episode list (new route: array of url/title rows). + * + * @param mixed $raw Raw form / meta value. + * @return array + */ + public static function sanitize_video_episodes($raw): array { + if (!is_array($raw)) { + return []; + } + $out = []; + foreach ($raw as $row) { + if (!is_array($row)) { + continue; + } + $url = esc_url_raw((string) ($row['url'] ?? '')); + if ($url === '') { + continue; + } + $out[] = [ + 'url' => $url, + 'title' => sanitize_text_field((string) ($row['title'] ?? '')), + ]; + if (count($out) >= 50) { + break; + } + } + return $out; } /** @@ -147,10 +185,12 @@ class AKSRT_Post_Meta_Box {

- + +

@@ -210,11 +250,24 @@ class AKSRT_Post_Meta_Box {

- + -

- - 0 表示第 N 集。', 'aksrt'); ?> +

+ +

+ + +

+ +

@@ -308,7 +361,8 @@ class AKSRT_Post_Meta_Box { 'custom_excerpt' => isset($raw['custom_excerpt']) ? sanitize_textarea_field($raw['custom_excerpt']) : '', 'layout_override' => isset($raw['layout_override']) ? sanitize_key($raw['layout_override']) : '', 'header_image_mode' => isset($raw['header_image_mode']) ? sanitize_key($raw['header_image_mode']) : '', - 'show_toc' => !empty($raw['show_toc']) ? '1' : '', + // New route: '' follow | '1' force on | '0' force off. + 'show_toc' => isset($raw['show_toc']) && in_array((string) $raw['show_toc'], ['', '0', '1'], true) ? (string) $raw['show_toc'] : '', 'show_reading_progress' => isset($raw['show_reading_progress']) ? sanitize_key((string) $raw['show_reading_progress']) : '', 'enable_lightbox' => isset($raw['enable_lightbox']) ? sanitize_key((string) $raw['enable_lightbox']) : '', 'show_related' => isset($raw['show_related']) ? sanitize_key((string) $raw['show_related']) : '', @@ -324,7 +378,7 @@ class AKSRT_Post_Meta_Box { 'thumbnail_url' => isset($raw['thumbnail_url']) ? esc_url_raw((string) $raw['thumbnail_url']) : '', 'featured_slide' => isset($raw['featured_slide']) ? sanitize_text_field($raw['featured_slide']) : '', 'featured_video' => isset($raw['featured_video']) ? esc_url_raw((string) $raw['featured_video']) : '', - 'featured_video_episode' => isset($raw['featured_video_episode']) ? max(0, (int) $raw['featured_video_episode']) : 0, + 'featured_video_episode' => self::sanitize_video_episodes($raw['featured_video_episode'] ?? []), 'subtitle' => isset($raw['subtitle']) ? sanitize_text_field($raw['subtitle']) : '', 'views' => isset($raw['views']) ? max(0, (int) $raw['views']) : 0, 'likes' => isset($raw['likes']) ? max(0, (int) $raw['likes']) : 0, diff --git a/inc/helpers/aksrt-domain-model.php b/inc/helpers/aksrt-domain-model.php index 09f26f02..af4ea59f 100644 --- a/inc/helpers/aksrt-domain-model.php +++ b/inc/helpers/aksrt-domain-model.php @@ -77,9 +77,10 @@ class AKSRT_Domain_Model { // §3.8 文章扩展字段 'thumbnail_url' => '', 'featured_slide' => '', - 'featured_video' => '', - 'featured_video_episode' => 0, - 'subtitle' => '', + 'featured_video' => '', + // Episode list: [ ['url' => string, 'title' => string], ... ] + 'featured_video_episode' => [], + 'subtitle' => '', 'views' => 0, 'likes' => 0, 'maxheight' => 0, diff --git a/inc/helpers/aksrt-frontend-tags.php b/inc/helpers/aksrt-frontend-tags.php index 9c83700c..0623f5d3 100644 --- a/inc/helpers/aksrt-frontend-tags.php +++ b/inc/helpers/aksrt-frontend-tags.php @@ -1,1183 +1,1102 @@ - - */ - function aksrt_comment_smilie_map(): array { - return [ - 'grinning' => '😀', - 'joy' => '😂', - 'rofl' => '🤣', - 'blush' => '😊', - 'heart-eyes' => '😍', - 'smiling-hearts' => '🥰', - 'sunglasses' => '😎', - 'thinking' => '🤔', - 'cry' => '😢', - 'sob' => '😭', - 'angry' => '😡', - 'pleading' => '🥺', - 'scream' => '😱', - 'rolling-eyes' => '🙄', - 'sleeping' => '😴', - 'nauseated' => '🤮', - 'thumbs-up' => '👍', - 'thumbs-down' => '👎', - 'clap' => '👏', - 'pray' => '🙏', - 'muscle' => '💪', - 'heart' => '❤️', - 'fire' => '🔥', - 'star' => '⭐', - 'tada' => '🎉', - 'confetti' => '🎊', - 'check' => '✅', - 'cross' => '❌', - 'warning' => '⚠️', - 'hundred' => '💯', - 'ok' => '🆗', - 'new' => '🆕', - ]; - } -endif; - -if (!function_exists('aksrt_render_comment_smilie')) : - /** - * 渲染单个 grin 表情为 smilie-icon 图片。 - * - * @param string $key grin key. - * @return string - */ - function aksrt_render_comment_smilie(string $key): string { - $map = aksrt_comment_smilie_map(); - if (!isset($map[$key])) { - return '[g=' . $key . ']'; - } - - $emoji = $map[$key]; - $svg = '' . esc_html($emoji) . ''; - $src = 'data:image/svg+xml;charset=UTF-8,' . rawurlencode($svg); - - return sprintf( - '%2$s', - esc_url($src), - esc_attr($emoji), - esc_attr($key) - ); - } -endif; - -if (!function_exists('aksrt_render_comment_content')) : - /** - * 将评论中的 grin token 渲染为 smilie-icon,并按 post 白名单输出。 - * - * @param string $content Comment HTML/text. - * @return string - */ - function aksrt_render_comment_content(string $content): string { - $rendered = preg_replace_callback('/\[g=([a-z0-9-]+)\]/i', static function (array $matches): string { - return aksrt_render_comment_smilie(sanitize_key($matches[1])); - }, $content); - - $allowed = wp_kses_allowed_html('post'); - if (!isset($allowed['img'])) { - $allowed['img'] = []; - } - $allowed['img'] = array_merge($allowed['img'], [ - 'class' => true, - 'src' => true, - 'alt' => true, - 'title' => true, - 'width' => true, - 'height' => true, - 'loading' => true, - 'decoding' => true, - 'data-smilie-key' => true, - ]); - - return wp_kses((string) $rendered, $allowed); - } -endif; - -if (!function_exists('aksrt_reading_minutes')) : - /** - * 估算文章阅读分钟数 - * - * @param int|null $post_id 文章 ID,默认当前文章 - * @return int - */ -function aksrt_reading_minutes($post_id = null) { - $post = get_post($post_id); - if (!$post) { - return 1; - } - $text = wp_strip_all_tags($post->post_content); - $han = 0; - if (preg_match_all('/\p{Han}/u', $text, $m)) { - $han = count($m[0]); - } - $words = str_word_count($text) + (int) ceil($han / 2); - if ($words < 1) { - $words = (int) max(1, strlen($text) / 4); - } - $minutes = (int) ceil($words / 220); - return max(1, $minutes); -} -endif; - -if (!function_exists('aksrt_post_word_count')) : - /** - * Approximate visible word count for single-post meta display. - * - * @param int|null $post_id Post ID. - * @return int - */ - function aksrt_post_word_count($post_id = null): int { - $post = get_post($post_id); - if (!($post instanceof WP_Post)) { - return 0; - } - - $text = wp_strip_all_tags((string) $post->post_content); - $han = 0; - if (preg_match_all('/\p{Han}/u', $text, $matches)) { - $han = count($matches[0]); - } - - $latin_words = str_word_count($text); - $total = $latin_words + $han; - if ($total <= 0) { - $total = (int) ceil(strlen($text) / 2); - } - - return max(0, $total); - } -endif; - -if (!function_exists('aksrt_resolve_single_post_display_settings')) : - /** - * Resolve single post/resource display settings, including post-meta overrides. - * - * @param int|null $post_id Post ID. - * @return array - */ - function aksrt_resolve_single_post_display_settings($post_id = null): array { - $post = get_post($post_id); - if (!($post instanceof WP_Post)) { - return []; - } - - $settings = class_exists('AKSRT_Settings') - ? AKSRT_Settings::get_post_page_settings() - : [ - 'toc_enabled' => true, - 'toc_depth' => 3, - 'reading_progress' => true, - 'lightbox' => true, - 'related_enabled' => true, - 'related_count' => 6, - 'related_style' => 'img', - 'related_source' => 'category', - 'related_type' => 'category', - 'related_orderby' => 'views', - 'related_title' => '', - 'breadcrumbs_single_text' => true, - 'like_enabled' => true, - 'favorite_enabled' => true, - 'share_enabled' => true, - 'breadcrumbs' => true, - 'breadcrumbs_home' => '首页', - 'prevnext' => true, - 'authordesc' => true, - 'hide_time' => false, - 'rewards_enabled' => false, - 'rewards_image' => '', - 'rewards_text' => '', - 'rewards_btn_text' => '赞赏', - 'maxheight_enabled' => false, - 'maxheight' => 1000, - 'copyright_enabled' => true, - 'copyright_text' => '', - 'front_content' => '', - 'after_content' => '', - 'yiyan_header' => false, - 'yiyan_footer' => false, - 'yiyan_box' => false, - 'print_enabled' => true, - 'pdf_export_enabled' => false, - 'word_count_enabled' => true, - 'reading_time_enabled' => true, - ]; - - // Normalize legacy aliases → canonical render keys (idempotent). - if (!array_key_exists('share_enabled', $settings) && array_key_exists('share_s', $settings)) { - $settings['share_enabled'] = !empty($settings['share_s']); - } - if (!array_key_exists('hide_time', $settings) && array_key_exists('single_hide_time_s', $settings)) { - $settings['hide_time'] = !empty($settings['single_hide_time_s']); - } - if (!array_key_exists('breadcrumbs', $settings) && array_key_exists('breadcrumbs_single_s', $settings)) { - $settings['breadcrumbs'] = !empty($settings['breadcrumbs_single_s']); - } - if (!array_key_exists('breadcrumbs_home', $settings) && array_key_exists('breadcrumbs_home_text', $settings)) { - $settings['breadcrumbs_home'] = (string) $settings['breadcrumbs_home_text']; - } - if (!array_key_exists('prevnext', $settings) && array_key_exists('prevnext_s', $settings)) { - $settings['prevnext'] = !empty($settings['prevnext_s']); - } - if (!array_key_exists('authordesc', $settings) && array_key_exists('authordesc_s', $settings)) { - $settings['authordesc'] = !empty($settings['authordesc_s']); - } - // related_style / related_source (legacy related_type held either). - if (!array_key_exists('related_style', $settings) || !in_array((string) $settings['related_style'], ['img', 'list', 'text'], true)) { - $legacy = sanitize_key((string) ($settings['related_type'] ?? '')); - $settings['related_style'] = in_array($legacy, ['img', 'list', 'text'], true) ? $legacy : 'img'; - } - if (!array_key_exists('related_source', $settings) || !in_array((string) $settings['related_source'], ['category', 'tag', 'random'], true)) { - $legacy = sanitize_key((string) ($settings['related_type'] ?? '')); - $settings['related_source'] = in_array($legacy, ['category', 'tag', 'random'], true) ? $legacy : 'category'; - } - // Keep related_type as source alias for older callers. - $settings['related_type'] = (string) $settings['related_source']; - - $meta = class_exists('AKSRT_Post_Meta_Box') ? AKSRT_Post_Meta_Box::get_meta($post->ID) : []; - if (($meta['show_toc'] ?? '') !== '') { - $settings['toc_enabled'] = $meta['show_toc'] === '1'; - } - - foreach ([ - 'show_reading_progress' => 'reading_progress', - 'enable_lightbox' => 'lightbox', - 'show_related' => 'related_enabled', - 'enable_like' => 'like_enabled', - 'enable_favorite' => 'favorite_enabled', - 'enable_share' => 'share_enabled', - 'enable_print' => 'print_enabled', - 'enable_pdf' => 'pdf_export_enabled', - ] as $meta_key => $setting_key) { - if (($meta[$meta_key] ?? '') === '') { - continue; - } - - $settings[$setting_key] = $meta[$meta_key] === '1'; - } - - if (!empty($meta['related_count'])) { - $settings['related_count'] = max(1, min(20, (int) $meta['related_count'])); - } - - // Per-post maxheight override (Zibll article_maxheight_xz). Global may already set maxheight_enabled. - $meta_maxheight = (int) ($meta['maxheight'] ?? 0); - if ($meta_maxheight > 0) { - $settings['maxheight_enabled'] = true; - $settings['maxheight'] = $meta_maxheight; - } elseif (empty($settings['maxheight_enabled'])) { - $settings['maxheight_enabled'] = false; - $settings['maxheight'] = max(0, (int) ($settings['maxheight'] ?? 0)); - } - - $cover_mode = (string) ($meta['header_image_mode'] ?? ''); - $cover_url = ''; - $cover_type = 'none'; - - // Check global cover toggles; per-post header_image_mode='none' overrides all. - $image_cover_on = !empty($settings['article_image_cover']); - $slide_cover_on = !empty($settings['article_slide_cover']); - $video_cover_on = !empty($settings['article_video_cover']); - - if ($cover_mode !== 'none') { - // 1. Video cover — check for video meta (featured_video from metabox). - $video_url = ''; - if ($video_cover_on) { - $video_url = (string) ($meta['featured_video'] ?? ''); - } - - // 2. Slide cover — featured_slide URLs and/or cover_images (attachment IDs). - $slide_images = []; - if ($slide_cover_on) { - $raw_slides = []; - if (!empty($meta['featured_slide'])) { - $raw_slides = array_merge($raw_slides, preg_split('/[,\s]+/', (string) $meta['featured_slide']) ?: []); - } - if (!empty($meta['cover_images'])) { - $raw_slides = array_merge($raw_slides, preg_split('/[,\s]+/', (string) $meta['cover_images']) ?: []); - } - foreach ($raw_slides as $token) { - $token = trim((string) $token); - if ($token === '') { - continue; - } - if (ctype_digit($token)) { - $src = wp_get_attachment_image_url((int) $token, 'full'); - if (is_string($src) && $src !== '') { - $slide_images[] = $src; - } - continue; - } - if (filter_var($token, FILTER_VALIDATE_URL)) { - $slide_images[] = esc_url_raw($token); - } - } - $slide_images = array_values(array_unique(array_filter($slide_images))); - } - - // 3. Image cover — existing logic. - if ($video_url !== '') { - $cover_type = 'video'; - $cover_url = $video_url; - } elseif (!empty($slide_images) && count($slide_images) >= 1) { - // One explicit slide image still becomes image cover; 2+ become slide. - if (count($slide_images) > 1) { - $cover_type = 'slide'; - $cover_url = $slide_images[0]; - } else { - $cover_type = 'image'; - $cover_url = $slide_images[0]; - $slide_images = []; - } - } elseif ($image_cover_on) { - if ($cover_mode === 'auto') { - $cover_url = function_exists('aksrt_get_first_content_image_url') - ? (string) aksrt_get_first_content_image_url($post->ID) - : ''; - } - if ($cover_url === '' && has_post_thumbnail($post)) { - $cover_url = (string) get_the_post_thumbnail_url($post, 'full'); - } - if ($cover_url !== '') { - $cover_type = 'image'; - } - } - } - - $settings['cover_mode'] = $cover_mode; - $settings['cover_url'] = $cover_url; - $settings['cover_type'] = $cover_type; - $settings['show_cover'] = $cover_type !== 'none'; - $settings['slide_images'] = $slide_images ?? []; - $settings['featured_video'] = (string) ($meta['featured_video'] ?? ''); - $settings['episode_count'] = max(0, (int) ($meta['featured_video_episode'] ?? 0)); - $settings['video_poster'] = (string) ($meta['cover_image'] ?? ''); - $meta_copyright = trim((string) ($meta['copyright_text'] ?? '')); - if ($meta_copyright !== '') { - $settings['copyright_text'] = $meta_copyright; - } - - return $settings; - } -endif; - -if (!function_exists('aksrt_breadcrumbs')) : - /** - * 输出面包屑 HTML(single 对齐 Zibll `ul.breadcrumb`)。 - * - * @param string $home_text Home label. - * @param array $args { - * @type bool $single_text When true on single post, last crumb is 「正文」. - * } - */ - function aksrt_breadcrumbs(string $home_text = '', array $args = []) { - if (is_front_page()) { - return; - } - - if ($home_text === '') { - $home_text = __('首页', 'aksrt'); - } - - $single_text = array_key_exists('single_text', $args) - ? !empty($args['single_text']) - : true; - - // Zibll single breadcrumb: flat ul.breadcrumb without sep nodes. - if (is_singular('post')) { - $cats = get_the_category(); - if (empty($cats)) { - return; - } - $category = $cats[0]; - $last = $single_text ? __('正文', 'aksrt') : get_the_title(); - // get_category_parents returns trailing separator; strip and rebuild like Zibll. - $parents = get_category_parents($category->term_id, true, '
  • '); - if (is_wp_error($parents) || !is_string($parents) || $parents === '') { - $parents = '' . esc_html($category->name) . '
  • '; - } - $html = ''; - echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - return; - } - - $items = []; - $items[] = '
  • '; - - if (is_category()) { - $term = get_queried_object(); - if ($term instanceof WP_Term) { - $ancestors = array_reverse(get_ancestors($term->term_id, 'category')); - foreach ($ancestors as $ancestor_id) { - $ancestor = get_term($ancestor_id, 'category'); - if ($ancestor instanceof WP_Term && !is_wp_error($ancestor)) { - $items[] = ''; - } - } - $items[] = ''; - } - } elseif (is_tag()) { - $items[] = ''; - } elseif (is_author()) { - $items[] = ''; - } elseif (is_search()) { - $search_query = trim((string) get_search_query()); - $items[] = ''; - } elseif (function_exists('aksrt_is_resource_archive') && aksrt_is_resource_archive()) { - $items[] = ''; - } elseif (is_page()) { - $items[] = ''; - } elseif (is_archive()) { - $items[] = ''; - } else { - $items[] = ''; - } - - echo ''; - } -endif; - -if (!function_exists('aksrt_get_post_views')) : - /** - * 获取文章浏览量 - * - * @param int|null $post_id 文章 ID,默认当前文章 - * @return int|string - */ - function aksrt_get_post_views($post_id = null) { - $post_id = $post_id ? (int) $post_id : get_the_ID(); - if (! $post_id) return 0; - - $common_count = (int) get_post_meta($post_id, 'aksrt_post_views', true); - $resource_count = (int) get_post_meta($post_id, '_aksrt_res_view_count', true); - - if (function_exists('aksrt_is_resource_post') && aksrt_is_resource_post($post_id)) { - return max(0, max($common_count, $resource_count)); - } - - return max(0, $common_count); - } -endif; - -if (! function_exists('aksrt_format_number')) : - function aksrt_format_number($num) { - if ($num >= 10000) { - return round($num / 10000, 1) . 'W+'; - } elseif ($num >= 1000) { - return round($num / 1000, 1) . 'k'; - } - return (string) number_format_i18n($num); - } -endif; - -if (!function_exists('aksrt_render_mini_post_item')) : - /** - * Render a compact sidebar post item for homepage widgets. - * - * Thin wrapper over the unified shared renderer (mode=mini). - * - * @param int|WP_Post|null $post - * @param array $args - */ - function aksrt_render_mini_post_item($post = null, array $args = []): void { - $post = get_post($post); - if (!($post instanceof WP_Post)) { - return; - } - - // Set up the global post so the shared renderer works - global $wp_query; - $original_post = isset($wp_query->post) ? $wp_query->post : null; - $wp_query->post = $post; - setup_postdata($post); - - if (function_exists('aksrt_render_post')) { - $config = [ - 'mode' => 'mini', - 'context' => 'sidebar', - 'show_thumb' => true, - 'show_meta' => !isset($args['show_views']) || !empty($args['show_views']), - ]; - - if (!empty($args['class']) && is_string($args['class'])) { - $config['extra_class'] = $args['class']; - } - - echo aksrt_render_post($config); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } else { - // Fallback: self-contained mini rendering - $post_id = (int) $post->ID; - $title = get_the_title($post_id); - $permalink = get_permalink($post_id); - $thumb_url = has_post_thumbnail($post_id) - ? (string) get_the_post_thumbnail_url($post_id, 'medium') - : ''; - - if ($thumb_url === '' && function_exists('aksrt_get_first_content_image_url')) { - $thumb_url = (string) aksrt_get_first_content_image_url($post_id); - } - - $placeholder_url = get_template_directory_uri() . '/assets/img/placeholder.svg'; - $view_count = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : 0; - $show_views = !isset($args['show_views']) || !empty($args['show_views']); - $date_text = function_exists('aksrt_time_ago') - ? aksrt_time_ago((string) $post->post_date) - : get_the_date('Y-m-d', $post_id); - $classes = ['posts-mini']; - - if (!empty($args['class']) && is_string($args['class'])) { - foreach (preg_split('/\s+/', trim($args['class'])) as $extra_class) { - $extra_class = sanitize_html_class($extra_class); - if ($extra_class !== '') { - $classes[] = $extra_class; - } - } - } - ?> -
    - - <?php echo esc_attr($title); ?> - -
    -

    - -

    -
    - - - - - - - - -
    -
    -
    - post = $original_post; - setup_postdata($original_post); - } else { - wp_reset_postdata(); - } - } -endif; - -if (!function_exists('aksrt_set_post_views')) : - /** - * 记录文章/资源浏览量(每浏览器 24 小时只计 1 次)。 - * 使用原生 SQL UPDATE,避免高并发丢失。 - */ - function aksrt_set_post_views() { - if (!is_singular()) return; - if (is_preview()) return; - - $post_id = (int) get_queried_object_id(); - if ($post_id <= 0) { - $q = get_queried_object(); - if ($q instanceof WP_Post) { - $post_id = (int) $q->ID; - } - } - if ($post_id <= 0) { - $post_id = (int) get_the_ID(); - } - if ($post_id <= 0) return; - - $post_type = get_post_type($post_id); - // 只对 post / 历史 aksrt_resource CPT 计数 - if (!in_array($post_type, ['post', 'aksrt_resource'], true)) return; - - $key = 'aksrt_post_views_' . $post_id; - if (!empty($_COOKIE[$key])) return; - - // 资源(post + _aksrt_is_resource,或旧 CPT)使用独立 meta key,便于排序 - $is_resource = function_exists('aksrt_is_resource_post') && aksrt_is_resource_post($post_id); - - if ($is_resource) { - if (function_exists('aksrt_update_post_counter')) { - aksrt_update_post_counter($post_id, '_aksrt_res_view_count', 1); - aksrt_update_post_counter($post_id, 'aksrt_post_views', 1); - } else { - $resource_count = (int) get_post_meta($post_id, '_aksrt_res_view_count', true); - update_post_meta($post_id, '_aksrt_res_view_count', $resource_count + 1); - - $common_count = (int) get_post_meta($post_id, 'aksrt_post_views', true); - update_post_meta($post_id, 'aksrt_post_views', $common_count + 1); - } - } else { - if (function_exists('aksrt_update_post_counter')) { - aksrt_update_post_counter($post_id, 'aksrt_post_views', 1); - } else { - $count = (int) get_post_meta($post_id, 'aksrt_post_views', true); - update_post_meta($post_id, 'aksrt_post_views', $count + 1); - } - } - - $cookie_path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; - $cookie_domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; - setcookie($key, '1', time() + DAY_IN_SECONDS, $cookie_path, $cookie_domain); - // 同一次请求内立即可见,避免重复执行时再次计数。 - $_COOKIE[$key] = '1'; - } -endif; - -if (!function_exists('aksrt_get_post_likes')) : - function aksrt_get_post_likes($post_id = null) { - $post_id = $post_id ? (int) $post_id : get_the_ID(); - if (! $post_id) return 0; - return max(0, (int) get_post_meta($post_id, '_aksrt_like_count', true)); - } -endif; - -if (!function_exists('aksrt_get_post_favorites')) : - function aksrt_get_post_favorites($post_id = null) { - $post_id = $post_id ? (int) $post_id : get_the_ID(); - if (! $post_id) return 0; - return max(0, (int) get_post_meta($post_id, '_aksrt_favorite_count', true)); - } -endif; - -if (!function_exists('aksrt_get_interaction_cookie_name')) : - function aksrt_get_interaction_cookie_name($type, $post_id) { - return 'aksrt_' . sanitize_key($type) . '_' . (int) $post_id; - } -endif; - -if (!function_exists('aksrt_normalize_post_id_list')) : - function aksrt_normalize_post_id_list($ids) { - if (! is_array($ids)) { - $ids = []; - } - - $ids = array_map('absint', $ids); - $ids = array_filter($ids, static function ($id) { - return $id > 0; - }); - - return array_values(array_unique($ids)); - } -endif; - -if (!function_exists('aksrt_set_interaction_cookie')) : - function aksrt_set_interaction_cookie($name, $active) { - $expire = $active ? time() + YEAR_IN_SECONDS : time() - HOUR_IN_SECONDS; - $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; - $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; - - setcookie($name, $active ? '1' : '', $expire, $path, $domain, is_ssl(), true); - - if ($active) { - $_COOKIE[$name] = '1'; - } else { - unset($_COOKIE[$name]); - } - } -endif; - -if (!function_exists('aksrt_get_user_interaction_posts')) : - function aksrt_get_user_interaction_posts($meta_key, $user_id = null) { - $user_id = $user_id ? (int) $user_id : get_current_user_id(); - if ($user_id <= 0) { - return []; - } - - return aksrt_normalize_post_id_list(get_user_meta($user_id, $meta_key, true)); - } -endif; - -if (!function_exists('aksrt_set_user_interaction_state')) : - function aksrt_set_user_interaction_state($meta_key, $post_id, $active, $user_id = null) { - $user_id = $user_id ? (int) $user_id : get_current_user_id(); - if ($user_id <= 0) { - return; - } - - $posts = aksrt_get_user_interaction_posts($meta_key, $user_id); - if ($active) { - $posts[] = (int) $post_id; - } else { - $posts = array_filter($posts, static function ($id) use ($post_id) { - return (int) $id !== (int) $post_id; - }); - } - - update_user_meta($user_id, $meta_key, aksrt_normalize_post_id_list($posts)); - } -endif; - -if (!function_exists('aksrt_is_post_liked')) : - function aksrt_is_post_liked($post_id = null) { - $post_id = $post_id ? (int) $post_id : get_the_ID(); - if ($post_id <= 0) { - return false; - } - - if (is_user_logged_in()) { - return in_array($post_id, aksrt_get_user_interaction_posts('_aksrt_liked_posts'), true); - } - - $cookie = aksrt_get_interaction_cookie_name('like', $post_id); - return ! empty($_COOKIE[$cookie]) && '1' === $_COOKIE[$cookie]; - } -endif; - -if (!function_exists('aksrt_is_post_favorited')) : - function aksrt_is_post_favorited($post_id = null) { - $post_id = $post_id ? (int) $post_id : get_the_ID(); - if ($post_id <= 0) { - return false; - } - - if (is_user_logged_in()) { - return in_array($post_id, aksrt_get_user_interaction_posts('_aksrt_favorite_posts'), true); - } - - $cookie = aksrt_get_interaction_cookie_name('favorite', $post_id); - return ! empty($_COOKIE[$cookie]) && '1' === $_COOKIE[$cookie]; - } -endif; - -if (!function_exists('aksrt_update_post_counter')) : - /** - * 原生修改 post meta 数字计数器,避免高并发 read-modify-write 丢失。 - * - * 实现:单条 SQL UPDATE postmeta SET meta_value = GREATEST(0, CAST + delta), - * 若行不存在则先 add_post_meta 初始化为 max(0, delta) 再读出。 - * - * @param int $post_id - * @param string $meta_key - * @param int $delta 可正可负 - * @return int 操作后的最新计数(>= 0) - */ - function aksrt_update_post_counter($post_id, $meta_key, $delta) { - global $wpdb; - $post_id = (int) $post_id; - $delta = (int) $delta; - - $exists = (int) $wpdb->get_var($wpdb->prepare( - "SELECT 1 FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s LIMIT 1", - $post_id, - $meta_key - )); - - if ($exists) { - $wpdb->query($wpdb->prepare( - "UPDATE {$wpdb->postmeta} - SET meta_value = GREATEST(0, CAST(meta_value AS SIGNED) + %d) - WHERE post_id = %d AND meta_key = %s", - $delta, - $post_id, - $meta_key - )); - } else { - add_post_meta($post_id, $meta_key, max(0, $delta), true); - } - - wp_cache_delete($post_id, 'post_meta'); - return (int) get_post_meta($post_id, $meta_key, true); - } -endif; - -if (!function_exists('aksrt_toggle_like_ajax')) : - function aksrt_toggle_like_ajax() { - check_ajax_referer('aksrt_public_nonce', 'nonce'); - - $post_id = isset($_POST['postId']) ? absint(wp_unslash($_POST['postId'])) : 0; - $post = $post_id ? get_post($post_id) : null; - if (! $post || 'post' !== $post->post_type || 'publish' !== $post->post_status) { - wp_send_json_error(['message' => __('Invalid post.', 'aksrt')], 400); - } - - $current = aksrt_is_post_liked($post_id); - $target = isset($_POST['active']) ? '1' === sanitize_text_field(wp_unslash($_POST['active'])) : ! $current; - - if ($target !== $current) { - if (is_user_logged_in()) { - aksrt_set_user_interaction_state('_aksrt_liked_posts', $post_id, $target); - } else { - aksrt_set_interaction_cookie(aksrt_get_interaction_cookie_name('like', $post_id), $target); - } - - $count = aksrt_update_post_counter($post_id, '_aksrt_like_count', $target ? 1 : -1); - } else { - $count = aksrt_get_post_likes($post_id); - } - - wp_send_json_success([ - 'active' => $target, - 'count' => $count, - ]); - } -endif; - -if (!function_exists('aksrt_toggle_favorite_ajax')) : - function aksrt_toggle_favorite_ajax() { - check_ajax_referer('aksrt_public_nonce', 'nonce'); - - $post_id = isset($_POST['postId']) ? absint(wp_unslash($_POST['postId'])) : 0; - $post = $post_id ? get_post($post_id) : null; - if (! $post || 'post' !== $post->post_type || 'publish' !== $post->post_status) { - wp_send_json_error(['message' => __('Invalid post.', 'aksrt')], 400); - } - - $current = aksrt_is_post_favorited($post_id); - $target = isset($_POST['active']) ? '1' === sanitize_text_field(wp_unslash($_POST['active'])) : ! $current; - - if ($target !== $current) { - if (is_user_logged_in()) { - aksrt_set_user_interaction_state('_aksrt_favorite_posts', $post_id, $target); - } else { - aksrt_set_interaction_cookie(aksrt_get_interaction_cookie_name('favorite', $post_id), $target); - } - - $count = aksrt_update_post_counter($post_id, '_aksrt_favorite_count', $target ? 1 : -1); - } else { - $count = aksrt_get_post_favorites($post_id); - } - - wp_send_json_success([ - 'active' => $target, - 'count' => $count, - ]); - } -endif; - -add_action('wp_ajax_aksrt_toggle_like', 'aksrt_toggle_like_ajax'); -add_action('wp_ajax_nopriv_aksrt_toggle_like', 'aksrt_toggle_like_ajax'); -add_action('wp_ajax_aksrt_toggle_favorite', 'aksrt_toggle_favorite_ajax'); -add_action('wp_ajax_nopriv_aksrt_toggle_favorite', 'aksrt_toggle_favorite_ajax'); - -// Float moderator-pinned top-level comments to the front of the first-page list. -// Runs during comments_template() before comments.php is loaded. -add_filter('comments_array', 'aksrt_reorder_pinned_comments', 20, 2); - -if (!function_exists('aksrt_comment_action_permissions')) : - /** - * Single source of truth for per-comment action permissions. - * - * Both the renderer (button visibility) and the REST endpoints - * (server-side enforcement) must read from this contract so the UI never - * advertises an action the backend would reject. - * - * @param WP_Comment|int $comment Comment object or ID. - * @return array{can_edit:bool,can_delete:bool,can_moderate:bool,can_pin:bool} - */ - function aksrt_comment_action_permissions($comment): array { - $comment = $comment instanceof WP_Comment ? $comment : get_comment($comment); - $none = ['can_edit' => false, 'can_delete' => false, 'can_moderate' => false, 'can_pin' => false]; - - if (!($comment instanceof WP_Comment) || !is_user_logged_in()) { - return $none; - } - - $is_moderator = current_user_can('moderate_comments'); - $is_owner = (int) $comment->user_id > 0 && (int) $comment->user_id === get_current_user_id(); - - // Owner self-edit window (minutes); moderators bypass the window. - $edit_window = 10; - if (class_exists('AKSRT_Settings')) { - $display = AKSRT_Settings::get_comment_display_settings(); - $edit_window = (int) ($display['edit_window'] ?? 10); - } - $within_window = $edit_window > 0 - && (time() - strtotime((string) $comment->comment_date_gmt)) < ($edit_window * MINUTE_IN_SECONDS); - - return [ - 'can_edit' => $is_moderator || ($is_owner && $within_window), - 'can_delete' => $is_moderator || $is_owner, - 'can_moderate' => $is_moderator, - 'can_pin' => $is_moderator && (int) $comment->comment_parent === 0, - ]; - } -endif; - -if (!function_exists('aksrt_is_comment_pinned')) : - /** - * Whether a comment is pinned ("置顶") by a moderator. - * - * @param int|WP_Comment $comment Comment object or ID. - * @return bool - */ - function aksrt_is_comment_pinned($comment): bool { - $comment_id = $comment instanceof WP_Comment ? (int) $comment->comment_ID : (int) $comment; - if ($comment_id <= 0) { - return false; - } - - return '1' === (string) get_comment_meta($comment_id, '_aksrt_comment_pinned', true); - } -endif; - -if (!function_exists('aksrt_update_comment_counter')) : - /** - * Atomically bump a numeric comment-meta counter, floored at zero. - * - * @param int $comment_id Comment ID. - * @param string $meta_key Meta key. - * @param int $delta Signed delta. - * @return int Latest value (>= 0). - */ - function aksrt_update_comment_counter($comment_id, $meta_key, $delta) { - global $wpdb; - $comment_id = (int) $comment_id; - $delta = (int) $delta; - - $exists = (int) $wpdb->get_var($wpdb->prepare( - "SELECT 1 FROM {$wpdb->commentmeta} WHERE comment_id = %d AND meta_key = %s LIMIT 1", - $comment_id, - $meta_key - )); - - if ($exists) { - $wpdb->query($wpdb->prepare( - "UPDATE {$wpdb->commentmeta} - SET meta_value = GREATEST(0, CAST(meta_value AS SIGNED) + %d) - WHERE comment_id = %d AND meta_key = %s", - $delta, - $comment_id, - $meta_key - )); - } else { - add_comment_meta($comment_id, $meta_key, max(0, $delta), true); - } - - wp_cache_delete($comment_id, 'comment_meta'); - return (int) get_comment_meta($comment_id, $meta_key, true); - } -endif; - -if (!function_exists('aksrt_get_comment_likes')) : - /** - * Comment like count (shared by renderer, "hottest" sort and REST). - * - * @param int|WP_Comment $comment Comment object or ID. - * @return int - */ - function aksrt_get_comment_likes($comment): int { - $comment_id = $comment instanceof WP_Comment ? (int) $comment->comment_ID : (int) $comment; - if ($comment_id <= 0) { - return 0; - } - - return max(0, (int) get_comment_meta($comment_id, 'aksrt_comment_likes', true)); - } -endif; - -if (!function_exists('aksrt_is_comment_liked')) : - /** - * Whether the current visitor already liked a comment. - * - * @param int $comment_id Comment ID. - * @return bool - */ - function aksrt_is_comment_liked($comment_id): bool { - $comment_id = (int) $comment_id; - if ($comment_id <= 0) { - return false; - } - - if (is_user_logged_in()) { - $liked = get_user_meta(get_current_user_id(), '_aksrt_liked_comments', true); - $liked = is_array($liked) ? array_map('intval', $liked) : []; - return in_array($comment_id, $liked, true); - } - - $cookie = 'aksrt_clike_' . $comment_id; - return !empty($_COOKIE[$cookie]) && '1' === $_COOKIE[$cookie]; - } -endif; - -if (!function_exists('aksrt_set_comment_like_state')) : - /** - * Persist a visitor's like state for a comment. - * - * @param int $comment_id Comment ID. - * @param bool $active Target state. - */ - function aksrt_set_comment_like_state($comment_id, $active): void { - $comment_id = (int) $comment_id; - if ($comment_id <= 0) { - return; - } - - if (is_user_logged_in()) { - $user_id = get_current_user_id(); - $liked = get_user_meta($user_id, '_aksrt_liked_comments', true); - $liked = is_array($liked) ? array_map('intval', $liked) : []; - if ($active) { - $liked[] = $comment_id; - } else { - $liked = array_filter($liked, static function ($id) use ($comment_id) { - return (int) $id !== $comment_id; - }); - } - update_user_meta($user_id, '_aksrt_liked_comments', array_values(array_unique($liked))); - return; - } - - $cookie = 'aksrt_clike_' . $comment_id; - $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; - $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; - setcookie($cookie, $active ? '1' : '', $active ? time() + YEAR_IN_SECONDS : time() - HOUR_IN_SECONDS, $path, $domain, is_ssl(), true); - if ($active) { - $_COOKIE[$cookie] = '1'; - } else { - unset($_COOKIE[$cookie]); - } - } -endif; - -if (!function_exists('aksrt_reorder_pinned_comments')) : - /** - * Float pinned top-level comments to the front of the queried list. - * - * Attached to `comments_array` on singular views so the first-page render - * and the canonical renderer agree on pin ordering. Nested replies keep - * their parent ordering; only top-level pins are reordered. - * - * @param array $comments Comment objects. - * @param int $post_id Post ID. - * @return array - */ - function aksrt_reorder_pinned_comments($comments, $post_id) { - if (!is_array($comments) || count($comments) < 2) { - return $comments; - } - - $pinned = []; - $normal = []; - foreach ($comments as $comment) { - if ($comment instanceof WP_Comment && (int) $comment->comment_parent === 0 && aksrt_is_comment_pinned($comment)) { - $pinned[] = $comment; - } else { - $normal[] = $comment; - } - } - - if (empty($pinned)) { - return $comments; - } - - return array_merge($pinned, $normal); - } -endif; - -if (!function_exists('aksrt_comment_callback')) : - /** - * Canonical comment list item renderer. - * - * This is the single contract used by first-page rendering (comments.php), - * AJAX pagination (AKSRT_Admin::ajax_load_more_comments) and the post-submit - * list reload (main.js reloadCommentList(), which refetches the rendered - * page). Emits a stable `data-comment-id` plus reply, like, management - * (edit/delete/moderate) and pin controls gated through - * aksrt_comment_action_permissions(). - * - * @param WP_Comment $comment 评论对象 - * @param array $args 参数 - * @param int $depth 嵌套深度 - */ - function aksrt_comment_callback($comment, $args, $depth) { - $GLOBALS['comment'] = $comment; - $settings = get_query_var('aksrt_comment_settings', []); - $settings = is_array($settings) ? $settings : []; - $comment_id = (int) $comment->comment_ID; - $depth_class = 'aksrt-depth-' . min((int) ($settings['nested_depth'] ?? 3), (int) $depth); - $author_name = get_comment_author($comment); - $comment_date = get_comment_date('c', $comment); - $comment_label = get_comment_date('Y-m-d', $comment); - $avatar_size = max(24, (int) ($settings['avatar_size'] ?? 48)); - $allow_reply = !empty($settings['allow_reply']); - $show_author_tag = !isset($settings['author_tag']) || !empty($settings['author_tag']); - $like_enabled = !isset($settings['like_enabled']) || !empty($settings['like_enabled']); - $is_post_author = (int) $comment->user_id > 0 && (int) $comment->user_id === (int) get_post_field('post_author', $comment->comment_post_ID); - $is_pinned = aksrt_is_comment_pinned($comment); - $perms = aksrt_comment_action_permissions($comment); - $is_pending = '0' === $comment->comment_approved; - $is_spam = 'spam' === $comment->comment_approved; - $like_count = aksrt_get_comment_likes($comment); - $is_liked = aksrt_is_comment_liked($comment_id); - - $li_classes = trim($depth_class . ($is_pinned ? ' aksrt-comment--pinned' : '')); - ?> -
  • id="comment-" data-comment-id="" data-comment-status="comment_approved); ?>"> -
      -
    • -
      -
      - 'avatar-img comt-avatar']); ?> - - - - - - - -
      - -
      -
      - -

      - - - -
      - -
    • -
    -
  • - + */ + function aksrt_comment_smilie_map(): array { + return [ + 'grinning' => '😀', + 'joy' => '😂', + 'rofl' => '🤣', + 'blush' => '😊', + 'heart-eyes' => '😍', + 'smiling-hearts' => '🥰', + 'sunglasses' => '😎', + 'thinking' => '🤔', + 'cry' => '😢', + 'sob' => '😭', + 'angry' => '😡', + 'pleading' => '🥺', + 'scream' => '😱', + 'rolling-eyes' => '🙄', + 'sleeping' => '😴', + 'nauseated' => '🤮', + 'thumbs-up' => '👍', + 'thumbs-down' => '👎', + 'clap' => '👏', + 'pray' => '🙏', + 'muscle' => '💪', + 'heart' => '❤️', + 'fire' => '🔥', + 'star' => '⭐', + 'tada' => '🎉', + 'confetti' => '🎊', + 'check' => '✅', + 'cross' => '❌', + 'warning' => '⚠️', + 'hundred' => '💯', + 'ok' => '🆗', + 'new' => '🆕', + ]; + } +endif; +if (!function_exists('aksrt_render_comment_smilie')) : + /** + * 渲染单个 grin 表情为 smilie-icon 图片。 + * + * @param string $key grin key. + * @return string + */ + function aksrt_render_comment_smilie(string $key): string { + $map = aksrt_comment_smilie_map(); + if (!isset($map[$key])) { + return '[g=' . $key . ']'; + } + $emoji = $map[$key]; + $svg = '' . esc_html($emoji) . ''; + $src = 'data:image/svg+xml;charset=UTF-8,' . rawurlencode($svg); + return sprintf( + '%2$s', + esc_url($src), + esc_attr($emoji), + esc_attr($key) + ); + } +endif; +if (!function_exists('aksrt_render_comment_content')) : + /** + * 将评论中的 grin token 渲染为 smilie-icon,并按 post 白名单输出。 + * + * @param string $content Comment HTML/text. + * @return string + */ + function aksrt_render_comment_content(string $content): string { + $rendered = preg_replace_callback('/\[g=([a-z0-9-]+)\]/i', static function (array $matches): string { + return aksrt_render_comment_smilie(sanitize_key($matches[1])); + }, $content); + $allowed = wp_kses_allowed_html('post'); + if (!isset($allowed['img'])) { + $allowed['img'] = []; + } + $allowed['img'] = array_merge($allowed['img'], [ + 'class' => true, + 'src' => true, + 'alt' => true, + 'title' => true, + 'width' => true, + 'height' => true, + 'loading' => true, + 'decoding' => true, + 'data-smilie-key' => true, + ]); + return wp_kses((string) $rendered, $allowed); + } +endif; +if (!function_exists('aksrt_reading_minutes')) : + /** + * 估算文章阅读分钟数 + * + * @param int|null $post_id 文章 ID,默认当前文章 + * @return int + */ +function aksrt_reading_minutes($post_id = null) { + $post = get_post($post_id); + if (!$post) { + return 1; + } + $text = wp_strip_all_tags($post->post_content); + $han = 0; + if (preg_match_all('/\p{Han}/u', $text, $m)) { + $han = count($m[0]); + } + $words = str_word_count($text) + (int) ceil($han / 2); + if ($words < 1) { + $words = (int) max(1, strlen($text) / 4); + } + $minutes = (int) ceil($words / 220); + return max(1, $minutes); +} +endif; +if (!function_exists('aksrt_post_word_count')) : + /** + * Approximate visible word count for single-post meta display. + * + * @param int|null $post_id Post ID. + * @return int + */ + function aksrt_post_word_count($post_id = null): int { + $post = get_post($post_id); + if (!($post instanceof WP_Post)) { + return 0; + } + $text = wp_strip_all_tags((string) $post->post_content); + $han = 0; + if (preg_match_all('/\p{Han}/u', $text, $matches)) { + $han = count($matches[0]); + } + $latin_words = str_word_count($text); + $total = $latin_words + $han; + if ($total <= 0) { + $total = (int) ceil(strlen($text) / 2); + } + return max(0, $total); + } +endif; +if (!function_exists('aksrt_resolve_single_post_display_settings')) : + /** + * Resolve single post/resource display settings, including post-meta overrides. + * + * @param int|null $post_id Post ID. + * @return array + */ + function aksrt_resolve_single_post_display_settings($post_id = null): array { + $post = get_post($post_id); + if (!($post instanceof WP_Post)) { + return []; + } + $settings = class_exists('AKSRT_Settings') + ? AKSRT_Settings::get_post_page_settings() + : [ + 'toc_enabled' => true, + 'toc_depth' => 3, + 'reading_progress' => true, + 'lightbox' => true, + 'related_enabled' => true, + 'related_count' => 6, + 'related_style' => 'img', + 'related_source' => 'category', + 'related_type' => 'category', + 'related_orderby' => 'views', + 'related_title' => '', + 'breadcrumbs_single_text' => true, + 'like_enabled' => true, + 'favorite_enabled' => true, + 'share_enabled' => true, + 'breadcrumbs' => true, + 'breadcrumbs_home' => '首页', + 'prevnext' => true, + 'authordesc' => true, + 'hide_time' => false, + 'rewards_enabled' => false, + 'rewards_image' => '', + 'rewards_text' => '', + 'rewards_btn_text' => '赞赏', + 'maxheight_enabled' => false, + 'maxheight' => 1000, + 'copyright_enabled' => true, + 'copyright_text' => '', + 'front_content' => '', + 'after_content' => '', + 'yiyan_header' => false, + 'yiyan_footer' => false, + 'yiyan_box' => false, + 'print_enabled' => true, + 'pdf_export_enabled' => false, + 'word_count_enabled' => true, + 'reading_time_enabled' => true, + ]; + // Normalize legacy aliases → canonical render keys (idempotent). + if (!array_key_exists('share_enabled', $settings) && array_key_exists('share_s', $settings)) { + $settings['share_enabled'] = !empty($settings['share_s']); + } + if (!array_key_exists('hide_time', $settings) && array_key_exists('single_hide_time_s', $settings)) { + $settings['hide_time'] = !empty($settings['single_hide_time_s']); + } + if (!array_key_exists('breadcrumbs', $settings) && array_key_exists('breadcrumbs_single_s', $settings)) { + $settings['breadcrumbs'] = !empty($settings['breadcrumbs_single_s']); + } + if (!array_key_exists('breadcrumbs_home', $settings) && array_key_exists('breadcrumbs_home_text', $settings)) { + $settings['breadcrumbs_home'] = (string) $settings['breadcrumbs_home_text']; + } + if (!array_key_exists('prevnext', $settings) && array_key_exists('prevnext_s', $settings)) { + $settings['prevnext'] = !empty($settings['prevnext_s']); + } + if (!array_key_exists('authordesc', $settings) && array_key_exists('authordesc_s', $settings)) { + $settings['authordesc'] = !empty($settings['authordesc_s']); + } + // related_style / related_source (legacy related_type held either). + if (!array_key_exists('related_style', $settings) || !in_array((string) $settings['related_style'], ['img', 'list', 'text'], true)) { + $legacy = sanitize_key((string) ($settings['related_type'] ?? '')); + $settings['related_style'] = in_array($legacy, ['img', 'list', 'text'], true) ? $legacy : 'img'; + } + if (!array_key_exists('related_source', $settings) || !in_array((string) $settings['related_source'], ['category', 'tag', 'random'], true)) { + $legacy = sanitize_key((string) ($settings['related_type'] ?? '')); + $settings['related_source'] = in_array($legacy, ['category', 'tag', 'random'], true) ? $legacy : 'category'; + } + // Keep related_type as source alias for older callers. + $settings['related_type'] = (string) $settings['related_source']; + $meta = class_exists('AKSRT_Post_Meta_Box') ? AKSRT_Post_Meta_Box::get_meta($post->ID) : []; + // show_toc new route: '' follow | '1' on | '0' off +if (array_key_exists('show_toc', $meta) && $meta['show_toc'] !== '' && $meta['show_toc'] !== null) { + $settings['toc_enabled'] = (string) $meta['show_toc'] === '1'; +} + foreach ([ + 'show_reading_progress' => 'reading_progress', + 'enable_lightbox' => 'lightbox', + 'show_related' => 'related_enabled', + 'enable_like' => 'like_enabled', + 'enable_favorite' => 'favorite_enabled', + 'enable_share' => 'share_enabled', + 'enable_print' => 'print_enabled', + 'enable_pdf' => 'pdf_export_enabled', + ] as $meta_key => $setting_key) { + if (($meta[$meta_key] ?? '') === '') { + continue; + } + $settings[$setting_key] = $meta[$meta_key] === '1'; + } + if (!empty($meta['related_count'])) { + $settings['related_count'] = max(1, min(20, (int) $meta['related_count'])); + } + // Per-post maxheight override (Zibll article_maxheight_xz). Global may already set maxheight_enabled. + $meta_maxheight = (int) ($meta['maxheight'] ?? 0); + if ($meta_maxheight > 0) { + $settings['maxheight_enabled'] = true; + $settings['maxheight'] = $meta_maxheight; + } elseif (empty($settings['maxheight_enabled'])) { + $settings['maxheight_enabled'] = false; + $settings['maxheight'] = max(0, (int) ($settings['maxheight'] ?? 0)); + } + $cover_mode = (string) ($meta['header_image_mode'] ?? ''); + $cover_url = ''; + $cover_type = 'none'; + // Check global cover toggles; per-post header_image_mode='none' overrides all. + $image_cover_on = !empty($settings['article_image_cover']); + $slide_cover_on = !empty($settings['article_slide_cover']); + $video_cover_on = !empty($settings['article_video_cover']); + if ($cover_mode !== 'none') { + // 1. Video cover — featured_video or first episode URL (new route list). + $video_url = ''; + if ($video_cover_on) { + $video_url = (string) ($meta['featured_video'] ?? ''); + if ($video_url === '' && is_array($meta['featured_video_episode'] ?? null)) { + foreach ((array) $meta['featured_video_episode'] as $row) { + if (!is_array($row)) { continue; } + $cand = esc_url_raw((string) ($row['url'] ?? '')); + if ($cand !== '') { $video_url = $cand; break; } + } + } + } + // 2. Slide cover — featured_slide URLs and/or cover_images (attachment IDs). + $slide_images = []; + if ($slide_cover_on) { + $raw_slides = []; + if (!empty($meta['featured_slide'])) { + $raw_slides = array_merge($raw_slides, preg_split('/[,\s]+/', (string) $meta['featured_slide']) ?: []); + } + if (!empty($meta['cover_images'])) { + $raw_slides = array_merge($raw_slides, preg_split('/[,\s]+/', (string) $meta['cover_images']) ?: []); + } + foreach ($raw_slides as $token) { + $token = trim((string) $token); + if ($token === '') { + continue; + } + if (ctype_digit($token)) { + $src = wp_get_attachment_image_url((int) $token, 'full'); + if (is_string($src) && $src !== '') { + $slide_images[] = $src; + } + continue; + } + if (filter_var($token, FILTER_VALIDATE_URL)) { + $slide_images[] = esc_url_raw($token); + } + } + $slide_images = array_values(array_unique(array_filter($slide_images))); + } + // 3. Image cover — existing logic. + if ($video_url !== '') { + $cover_type = 'video'; + $cover_url = $video_url; + } elseif (!empty($slide_images) && count($slide_images) >= 1) { + // One explicit slide image still becomes image cover; 2+ become slide. + if (count($slide_images) > 1) { + $cover_type = 'slide'; + $cover_url = $slide_images[0]; + } else { + $cover_type = 'image'; + $cover_url = $slide_images[0]; + $slide_images = []; + } + } elseif ($image_cover_on) { + if ($cover_mode === 'auto') { + $cover_url = function_exists('aksrt_get_first_content_image_url') + ? (string) aksrt_get_first_content_image_url($post->ID) + : ''; + } + if ($cover_url === '' && has_post_thumbnail($post)) { + $cover_url = (string) get_the_post_thumbnail_url($post, 'full'); + } + if ($cover_url !== '') { + $cover_type = 'image'; + } + } + } + $settings['cover_mode'] = $cover_mode; + $settings['cover_url'] = $cover_url; + $settings['cover_type'] = $cover_type; + $settings['show_cover'] = $cover_type !== 'none'; + $settings['slide_images'] = $slide_images ?? []; + $settings['featured_video'] = (string) ($meta['featured_video'] ?? ''); +// New route: episode list only. +$episodes = []; +$raw_eps = $meta['featured_video_episode'] ?? []; +if (is_array($raw_eps)) { + foreach ($raw_eps as $row) { + if (!is_array($row)) { + continue; + } + $url = esc_url_raw((string) ($row['url'] ?? '')); + if ($url === '') { + continue; + } + $episodes[] = [ + 'url' => $url, + 'title' => sanitize_text_field((string) ($row['title'] ?? '')), + ]; + } +} +if ($settings['featured_video'] === '' && !empty($episodes[0]['url'])) { + $settings['featured_video'] = (string) $episodes[0]['url']; +} +// If video cover already selected with empty url, refill from featured_video. +if (($settings['cover_type'] ?? '') === 'video' && (string) ($settings['cover_url'] ?? '') === '' && $settings['featured_video'] !== '') { + $settings['cover_url'] = $settings['featured_video']; + $settings['show_cover'] = true; +} +$settings['video_episodes'] = $episodes; +$settings['episode_count'] = count($episodes); +$settings['video_poster'] = (string) ($meta['cover_image'] ?? ''); + $meta_copyright = trim((string) ($meta['copyright_text'] ?? '')); + if ($meta_copyright !== '') { + $settings['copyright_text'] = $meta_copyright; + } + return $settings; + } +endif; +if (!function_exists('aksrt_breadcrumbs')) : + /** + * 输出面包屑 HTML(single 对齐 Zibll `ul.breadcrumb`)。 + * + * @param string $home_text Home label. + * @param array $args { + * @type bool $single_text When true on single post, last crumb is 「正文」. + * } + */ + function aksrt_breadcrumbs(string $home_text = '', array $args = []) { + if (is_front_page()) { + return; + } + if ($home_text === '') { + $home_text = __('首页', 'aksrt'); + } + $single_text = array_key_exists('single_text', $args) + ? !empty($args['single_text']) + : true; + // Zibll single breadcrumb: flat ul.breadcrumb without sep nodes. + if (is_singular('post')) { + $cats = get_the_category(); + if (empty($cats)) { + return; + } + $category = $cats[0]; + $last = $single_text ? __('正文', 'aksrt') : get_the_title(); + // New route: hand-built ancestor chain (no get_category_parents noise). + $parts = []; + $parts[] = '
  • ' . esc_html($home_text) . '
  • '; + $ancestors = array_reverse(get_ancestors((int) $category->term_id, 'category')); + foreach ($ancestors as $ancestor_id) { + $ancestor = get_term((int) $ancestor_id, 'category'); + if ($ancestor instanceof WP_Term && !is_wp_error($ancestor)) { + $parts[] = '
  • ' . esc_html($ancestor->name) . '
  • '; + } + } + $parts[] = '
  • ' . esc_html($category->name) . '
  • '; + $parts[] = '
  • ' . esc_html((string) $last) . '
  • '; + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return; + } + $items = []; + $items[] = ''; + if (is_category()) { + $term = get_queried_object(); + if ($term instanceof WP_Term) { + $ancestors = array_reverse(get_ancestors($term->term_id, 'category')); + foreach ($ancestors as $ancestor_id) { + $ancestor = get_term($ancestor_id, 'category'); + if ($ancestor instanceof WP_Term && !is_wp_error($ancestor)) { + $items[] = ''; + } + } + $items[] = ''; + } + } elseif (is_tag()) { + $items[] = ''; + } elseif (is_author()) { + $items[] = ''; + } elseif (is_search()) { + $search_query = trim((string) get_search_query()); + $items[] = ''; + } elseif (function_exists('aksrt_is_resource_archive') && aksrt_is_resource_archive()) { + $items[] = ''; + } elseif (is_page()) { + $items[] = ''; + } elseif (is_archive()) { + $items[] = ''; + } else { + $items[] = ''; + } + echo ''; + } +endif; +if (!function_exists('aksrt_get_post_views')) : + /** + * 获取文章浏览量 + * + * @param int|null $post_id 文章 ID,默认当前文章 + * @return int|string + */ + function aksrt_get_post_views($post_id = null) { + $post_id = $post_id ? (int) $post_id : get_the_ID(); + if (! $post_id) return 0; + $common_count = (int) get_post_meta($post_id, 'aksrt_post_views', true); + $resource_count = (int) get_post_meta($post_id, '_aksrt_res_view_count', true); + if (function_exists('aksrt_is_resource_post') && aksrt_is_resource_post($post_id)) { + return max(0, max($common_count, $resource_count)); + } + return max(0, $common_count); + } +endif; +if (! function_exists('aksrt_format_number')) : + function aksrt_format_number($num) { + if ($num >= 10000) { + return round($num / 10000, 1) . 'W+'; + } elseif ($num >= 1000) { + return round($num / 1000, 1) . 'k'; + } + return (string) number_format_i18n($num); + } +endif; +if (!function_exists('aksrt_render_mini_post_item')) : + /** + * Render a compact sidebar post item for homepage widgets. + * + * Thin wrapper over the unified shared renderer (mode=mini). + * + * @param int|WP_Post|null $post + * @param array $args + */ + function aksrt_render_mini_post_item($post = null, array $args = []): void { + $post = get_post($post); + if (!($post instanceof WP_Post)) { + return; + } + // Set up the global post so the shared renderer works + global $wp_query; + $original_post = isset($wp_query->post) ? $wp_query->post : null; + $wp_query->post = $post; + setup_postdata($post); + if (function_exists('aksrt_render_post')) { + $config = [ + 'mode' => 'mini', + 'context' => 'sidebar', + 'show_thumb' => true, + 'show_meta' => !isset($args['show_views']) || !empty($args['show_views']), + ]; + if (!empty($args['class']) && is_string($args['class'])) { + $config['extra_class'] = $args['class']; + } + echo aksrt_render_post($config); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } else { + // Fallback: self-contained mini rendering + $post_id = (int) $post->ID; + $title = get_the_title($post_id); + $permalink = get_permalink($post_id); + $thumb_url = has_post_thumbnail($post_id) + ? (string) get_the_post_thumbnail_url($post_id, 'medium') + : ''; + if ($thumb_url === '' && function_exists('aksrt_get_first_content_image_url')) { + $thumb_url = (string) aksrt_get_first_content_image_url($post_id); + } + $placeholder_url = get_template_directory_uri() . '/assets/img/placeholder.svg'; + $view_count = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : 0; + $show_views = !isset($args['show_views']) || !empty($args['show_views']); + $date_text = function_exists('aksrt_time_ago') + ? aksrt_time_ago((string) $post->post_date) + : get_the_date('Y-m-d', $post_id); + $classes = ['posts-mini']; + if (!empty($args['class']) && is_string($args['class'])) { + foreach (preg_split('/\s+/', trim($args['class'])) as $extra_class) { + $extra_class = sanitize_html_class($extra_class); + if ($extra_class !== '') { + $classes[] = $extra_class; + } + } + } + ?> +
    + + <?php echo esc_attr($title); ?> + +
    +

    + +

    +
    + + + + + + + + +
    +
    +
    + post = $original_post; + setup_postdata($original_post); + } else { + wp_reset_postdata(); + } + } +endif; +if (!function_exists('aksrt_set_post_views')) : + /** + * 记录文章/资源浏览量(每浏览器 24 小时只计 1 次)。 + * 使用原生 SQL UPDATE,避免高并发丢失。 + */ + function aksrt_set_post_views() { + if (!is_singular()) return; + if (is_preview()) return; + $post_id = (int) get_queried_object_id(); + if ($post_id <= 0) { + $q = get_queried_object(); + if ($q instanceof WP_Post) { + $post_id = (int) $q->ID; + } + } + if ($post_id <= 0) { + $post_id = (int) get_the_ID(); + } + if ($post_id <= 0) return; + $post_type = get_post_type($post_id); + // 只对 post / 历史 aksrt_resource CPT 计数 + if (!in_array($post_type, ['post', 'aksrt_resource'], true)) return; + $key = 'aksrt_post_views_' . $post_id; + if (!empty($_COOKIE[$key])) return; + // 资源(post + _aksrt_is_resource,或旧 CPT)使用独立 meta key,便于排序 + $is_resource = function_exists('aksrt_is_resource_post') && aksrt_is_resource_post($post_id); + if ($is_resource) { + if (function_exists('aksrt_update_post_counter')) { + aksrt_update_post_counter($post_id, '_aksrt_res_view_count', 1); + aksrt_update_post_counter($post_id, 'aksrt_post_views', 1); + } else { + $resource_count = (int) get_post_meta($post_id, '_aksrt_res_view_count', true); + update_post_meta($post_id, '_aksrt_res_view_count', $resource_count + 1); + $common_count = (int) get_post_meta($post_id, 'aksrt_post_views', true); + update_post_meta($post_id, 'aksrt_post_views', $common_count + 1); + } + } else { + if (function_exists('aksrt_update_post_counter')) { + aksrt_update_post_counter($post_id, 'aksrt_post_views', 1); + } else { + $count = (int) get_post_meta($post_id, 'aksrt_post_views', true); + update_post_meta($post_id, 'aksrt_post_views', $count + 1); + } + } + $cookie_path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; + $cookie_domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; + setcookie($key, '1', time() + DAY_IN_SECONDS, $cookie_path, $cookie_domain); + // 同一次请求内立即可见,避免重复执行时再次计数。 + $_COOKIE[$key] = '1'; + } +endif; +if (!function_exists('aksrt_get_post_likes')) : + function aksrt_get_post_likes($post_id = null) { + $post_id = $post_id ? (int) $post_id : get_the_ID(); + if (! $post_id) return 0; + return max(0, (int) get_post_meta($post_id, '_aksrt_like_count', true)); + } +endif; +if (!function_exists('aksrt_get_post_favorites')) : + function aksrt_get_post_favorites($post_id = null) { + $post_id = $post_id ? (int) $post_id : get_the_ID(); + if (! $post_id) return 0; + return max(0, (int) get_post_meta($post_id, '_aksrt_favorite_count', true)); + } +endif; +if (!function_exists('aksrt_get_interaction_cookie_name')) : + function aksrt_get_interaction_cookie_name($type, $post_id) { + return 'aksrt_' . sanitize_key($type) . '_' . (int) $post_id; + } +endif; +if (!function_exists('aksrt_normalize_post_id_list')) : + function aksrt_normalize_post_id_list($ids) { + if (! is_array($ids)) { + $ids = []; + } + $ids = array_map('absint', $ids); + $ids = array_filter($ids, static function ($id) { + return $id > 0; + }); + return array_values(array_unique($ids)); + } +endif; +if (!function_exists('aksrt_set_interaction_cookie')) : + function aksrt_set_interaction_cookie($name, $active) { + $expire = $active ? time() + YEAR_IN_SECONDS : time() - HOUR_IN_SECONDS; + $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; + $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; + setcookie($name, $active ? '1' : '', $expire, $path, $domain, is_ssl(), true); + if ($active) { + $_COOKIE[$name] = '1'; + } else { + unset($_COOKIE[$name]); + } + } +endif; +if (!function_exists('aksrt_get_user_interaction_posts')) : + function aksrt_get_user_interaction_posts($meta_key, $user_id = null) { + $user_id = $user_id ? (int) $user_id : get_current_user_id(); + if ($user_id <= 0) { + return []; + } + return aksrt_normalize_post_id_list(get_user_meta($user_id, $meta_key, true)); + } +endif; +if (!function_exists('aksrt_set_user_interaction_state')) : + function aksrt_set_user_interaction_state($meta_key, $post_id, $active, $user_id = null) { + $user_id = $user_id ? (int) $user_id : get_current_user_id(); + if ($user_id <= 0) { + return; + } + $posts = aksrt_get_user_interaction_posts($meta_key, $user_id); + if ($active) { + $posts[] = (int) $post_id; + } else { + $posts = array_filter($posts, static function ($id) use ($post_id) { + return (int) $id !== (int) $post_id; + }); + } + update_user_meta($user_id, $meta_key, aksrt_normalize_post_id_list($posts)); + } +endif; +if (!function_exists('aksrt_is_post_liked')) : + function aksrt_is_post_liked($post_id = null) { + $post_id = $post_id ? (int) $post_id : get_the_ID(); + if ($post_id <= 0) { + return false; + } + if (is_user_logged_in()) { + return in_array($post_id, aksrt_get_user_interaction_posts('_aksrt_liked_posts'), true); + } + $cookie = aksrt_get_interaction_cookie_name('like', $post_id); + return ! empty($_COOKIE[$cookie]) && '1' === $_COOKIE[$cookie]; + } +endif; +if (!function_exists('aksrt_is_post_favorited')) : + function aksrt_is_post_favorited($post_id = null) { + $post_id = $post_id ? (int) $post_id : get_the_ID(); + if ($post_id <= 0) { + return false; + } + if (is_user_logged_in()) { + return in_array($post_id, aksrt_get_user_interaction_posts('_aksrt_favorite_posts'), true); + } + $cookie = aksrt_get_interaction_cookie_name('favorite', $post_id); + return ! empty($_COOKIE[$cookie]) && '1' === $_COOKIE[$cookie]; + } +endif; +if (!function_exists('aksrt_update_post_counter')) : + /** + * 原生修改 post meta 数字计数器,避免高并发 read-modify-write 丢失。 + * + * 实现:单条 SQL UPDATE postmeta SET meta_value = GREATEST(0, CAST + delta), + * 若行不存在则先 add_post_meta 初始化为 max(0, delta) 再读出。 + * + * @param int $post_id + * @param string $meta_key + * @param int $delta 可正可负 + * @return int 操作后的最新计数(>= 0) + */ + function aksrt_update_post_counter($post_id, $meta_key, $delta) { + global $wpdb; + $post_id = (int) $post_id; + $delta = (int) $delta; + $exists = (int) $wpdb->get_var($wpdb->prepare( + "SELECT 1 FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s LIMIT 1", + $post_id, + $meta_key + )); + if ($exists) { + $wpdb->query($wpdb->prepare( + "UPDATE {$wpdb->postmeta} + SET meta_value = GREATEST(0, CAST(meta_value AS SIGNED) + %d) + WHERE post_id = %d AND meta_key = %s", + $delta, + $post_id, + $meta_key + )); + } else { + add_post_meta($post_id, $meta_key, max(0, $delta), true); + } + wp_cache_delete($post_id, 'post_meta'); + return (int) get_post_meta($post_id, $meta_key, true); + } +endif; +if (!function_exists('aksrt_toggle_like_ajax')) : + function aksrt_toggle_like_ajax() { + check_ajax_referer('aksrt_public_nonce', 'nonce'); + $post_id = isset($_POST['postId']) ? absint(wp_unslash($_POST['postId'])) : 0; + $post = $post_id ? get_post($post_id) : null; + if (! $post || 'post' !== $post->post_type || 'publish' !== $post->post_status) { + wp_send_json_error(['message' => __('Invalid post.', 'aksrt')], 400); + } + $current = aksrt_is_post_liked($post_id); + $target = isset($_POST['active']) ? '1' === sanitize_text_field(wp_unslash($_POST['active'])) : ! $current; + if ($target !== $current) { + if (is_user_logged_in()) { + aksrt_set_user_interaction_state('_aksrt_liked_posts', $post_id, $target); + } else { + aksrt_set_interaction_cookie(aksrt_get_interaction_cookie_name('like', $post_id), $target); + } + $count = aksrt_update_post_counter($post_id, '_aksrt_like_count', $target ? 1 : -1); + } else { + $count = aksrt_get_post_likes($post_id); + } + wp_send_json_success([ + 'active' => $target, + 'count' => $count, + ]); + } +endif; +if (!function_exists('aksrt_toggle_favorite_ajax')) : + function aksrt_toggle_favorite_ajax() { + check_ajax_referer('aksrt_public_nonce', 'nonce'); + $post_id = isset($_POST['postId']) ? absint(wp_unslash($_POST['postId'])) : 0; + $post = $post_id ? get_post($post_id) : null; + if (! $post || 'post' !== $post->post_type || 'publish' !== $post->post_status) { + wp_send_json_error(['message' => __('Invalid post.', 'aksrt')], 400); + } + $current = aksrt_is_post_favorited($post_id); + $target = isset($_POST['active']) ? '1' === sanitize_text_field(wp_unslash($_POST['active'])) : ! $current; + if ($target !== $current) { + if (is_user_logged_in()) { + aksrt_set_user_interaction_state('_aksrt_favorite_posts', $post_id, $target); + } else { + aksrt_set_interaction_cookie(aksrt_get_interaction_cookie_name('favorite', $post_id), $target); + } + $count = aksrt_update_post_counter($post_id, '_aksrt_favorite_count', $target ? 1 : -1); + } else { + $count = aksrt_get_post_favorites($post_id); + } + wp_send_json_success([ + 'active' => $target, + 'count' => $count, + ]); + } +endif; +add_action('wp_ajax_aksrt_toggle_like', 'aksrt_toggle_like_ajax'); +add_action('wp_ajax_nopriv_aksrt_toggle_like', 'aksrt_toggle_like_ajax'); +add_action('wp_ajax_aksrt_toggle_favorite', 'aksrt_toggle_favorite_ajax'); +add_action('wp_ajax_nopriv_aksrt_toggle_favorite', 'aksrt_toggle_favorite_ajax'); +// Float moderator-pinned top-level comments to the front of the first-page list. +// Runs during comments_template() before comments.php is loaded. +add_filter('comments_array', 'aksrt_reorder_pinned_comments', 20, 2); +if (!function_exists('aksrt_comment_action_permissions')) : + /** + * Single source of truth for per-comment action permissions. + * + * Both the renderer (button visibility) and the REST endpoints + * (server-side enforcement) must read from this contract so the UI never + * advertises an action the backend would reject. + * + * @param WP_Comment|int $comment Comment object or ID. + * @return array{can_edit:bool,can_delete:bool,can_moderate:bool,can_pin:bool} + */ + function aksrt_comment_action_permissions($comment): array { + $comment = $comment instanceof WP_Comment ? $comment : get_comment($comment); + $none = ['can_edit' => false, 'can_delete' => false, 'can_moderate' => false, 'can_pin' => false]; + if (!($comment instanceof WP_Comment) || !is_user_logged_in()) { + return $none; + } + $is_moderator = current_user_can('moderate_comments'); + $is_owner = (int) $comment->user_id > 0 && (int) $comment->user_id === get_current_user_id(); + // Owner self-edit window (minutes); moderators bypass the window. + $edit_window = 10; + if (class_exists('AKSRT_Settings')) { + $display = AKSRT_Settings::get_comment_display_settings(); + $edit_window = (int) ($display['edit_window'] ?? 10); + } + $within_window = $edit_window > 0 + && (time() - strtotime((string) $comment->comment_date_gmt)) < ($edit_window * MINUTE_IN_SECONDS); + return [ + 'can_edit' => $is_moderator || ($is_owner && $within_window), + 'can_delete' => $is_moderator || $is_owner, + 'can_moderate' => $is_moderator, + 'can_pin' => $is_moderator && (int) $comment->comment_parent === 0, + ]; + } +endif; +if (!function_exists('aksrt_is_comment_pinned')) : + /** + * Whether a comment is pinned ("置顶") by a moderator. + * + * @param int|WP_Comment $comment Comment object or ID. + * @return bool + */ + function aksrt_is_comment_pinned($comment): bool { + $comment_id = $comment instanceof WP_Comment ? (int) $comment->comment_ID : (int) $comment; + if ($comment_id <= 0) { + return false; + } + return '1' === (string) get_comment_meta($comment_id, '_aksrt_comment_pinned', true); + } +endif; +if (!function_exists('aksrt_update_comment_counter')) : + /** + * Atomically bump a numeric comment-meta counter, floored at zero. + * + * @param int $comment_id Comment ID. + * @param string $meta_key Meta key. + * @param int $delta Signed delta. + * @return int Latest value (>= 0). + */ + function aksrt_update_comment_counter($comment_id, $meta_key, $delta) { + global $wpdb; + $comment_id = (int) $comment_id; + $delta = (int) $delta; + $exists = (int) $wpdb->get_var($wpdb->prepare( + "SELECT 1 FROM {$wpdb->commentmeta} WHERE comment_id = %d AND meta_key = %s LIMIT 1", + $comment_id, + $meta_key + )); + if ($exists) { + $wpdb->query($wpdb->prepare( + "UPDATE {$wpdb->commentmeta} + SET meta_value = GREATEST(0, CAST(meta_value AS SIGNED) + %d) + WHERE comment_id = %d AND meta_key = %s", + $delta, + $comment_id, + $meta_key + )); + } else { + add_comment_meta($comment_id, $meta_key, max(0, $delta), true); + } + wp_cache_delete($comment_id, 'comment_meta'); + return (int) get_comment_meta($comment_id, $meta_key, true); + } +endif; +if (!function_exists('aksrt_get_comment_likes')) : + /** + * Comment like count (shared by renderer, "hottest" sort and REST). + * + * @param int|WP_Comment $comment Comment object or ID. + * @return int + */ + function aksrt_get_comment_likes($comment): int { + $comment_id = $comment instanceof WP_Comment ? (int) $comment->comment_ID : (int) $comment; + if ($comment_id <= 0) { + return 0; + } + return max(0, (int) get_comment_meta($comment_id, 'aksrt_comment_likes', true)); + } +endif; +if (!function_exists('aksrt_is_comment_liked')) : + /** + * Whether the current visitor already liked a comment. + * + * @param int $comment_id Comment ID. + * @return bool + */ + function aksrt_is_comment_liked($comment_id): bool { + $comment_id = (int) $comment_id; + if ($comment_id <= 0) { + return false; + } + if (is_user_logged_in()) { + $liked = get_user_meta(get_current_user_id(), '_aksrt_liked_comments', true); + $liked = is_array($liked) ? array_map('intval', $liked) : []; + return in_array($comment_id, $liked, true); + } + $cookie = 'aksrt_clike_' . $comment_id; + return !empty($_COOKIE[$cookie]) && '1' === $_COOKIE[$cookie]; + } +endif; +if (!function_exists('aksrt_set_comment_like_state')) : + /** + * Persist a visitor's like state for a comment. + * + * @param int $comment_id Comment ID. + * @param bool $active Target state. + */ + function aksrt_set_comment_like_state($comment_id, $active): void { + $comment_id = (int) $comment_id; + if ($comment_id <= 0) { + return; + } + if (is_user_logged_in()) { + $user_id = get_current_user_id(); + $liked = get_user_meta($user_id, '_aksrt_liked_comments', true); + $liked = is_array($liked) ? array_map('intval', $liked) : []; + if ($active) { + $liked[] = $comment_id; + } else { + $liked = array_filter($liked, static function ($id) use ($comment_id) { + return (int) $id !== $comment_id; + }); + } + update_user_meta($user_id, '_aksrt_liked_comments', array_values(array_unique($liked))); + return; + } + $cookie = 'aksrt_clike_' . $comment_id; + $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; + $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; + setcookie($cookie, $active ? '1' : '', $active ? time() + YEAR_IN_SECONDS : time() - HOUR_IN_SECONDS, $path, $domain, is_ssl(), true); + if ($active) { + $_COOKIE[$cookie] = '1'; + } else { + unset($_COOKIE[$cookie]); + } + } +endif; +if (!function_exists('aksrt_reorder_pinned_comments')) : + /** + * Float pinned top-level comments to the front of the queried list. + * + * Attached to `comments_array` on singular views so the first-page render + * and the canonical renderer agree on pin ordering. Nested replies keep + * their parent ordering; only top-level pins are reordered. + * + * @param array $comments Comment objects. + * @param int $post_id Post ID. + * @return array + */ + function aksrt_reorder_pinned_comments($comments, $post_id) { + if (!is_array($comments) || count($comments) < 2) { + return $comments; + } + $pinned = []; + $normal = []; + foreach ($comments as $comment) { + if ($comment instanceof WP_Comment && (int) $comment->comment_parent === 0 && aksrt_is_comment_pinned($comment)) { + $pinned[] = $comment; + } else { + $normal[] = $comment; + } + } + if (empty($pinned)) { + return $comments; + } + return array_merge($pinned, $normal); + } +endif; +if (!function_exists('aksrt_comment_callback')) : + /** + * Canonical comment list item renderer. + * + * This is the single contract used by first-page rendering (comments.php), + * AJAX pagination (AKSRT_Admin::ajax_load_more_comments) and the post-submit + * list reload (main.js reloadCommentList(), which refetches the rendered + * page). Emits a stable `data-comment-id` plus reply, like, management + * (edit/delete/moderate) and pin controls gated through + * aksrt_comment_action_permissions(). + * + * @param WP_Comment $comment 评论对象 + * @param array $args 参数 + * @param int $depth 嵌套深度 + */ + function aksrt_comment_callback($comment, $args, $depth) { + $GLOBALS['comment'] = $comment; + $settings = get_query_var('aksrt_comment_settings', []); + $settings = is_array($settings) ? $settings : []; + $comment_id = (int) $comment->comment_ID; + $depth_class = 'aksrt-depth-' . min((int) ($settings['nested_depth'] ?? 3), (int) $depth); + $author_name = get_comment_author($comment); + $comment_date = get_comment_date('c', $comment); + $comment_label = get_comment_date('Y-m-d', $comment); + $avatar_size = max(24, (int) ($settings['avatar_size'] ?? 48)); + $allow_reply = !empty($settings['allow_reply']); + $show_author_tag = !isset($settings['author_tag']) || !empty($settings['author_tag']); + $like_enabled = !isset($settings['like_enabled']) || !empty($settings['like_enabled']); + $is_post_author = (int) $comment->user_id > 0 && (int) $comment->user_id === (int) get_post_field('post_author', $comment->comment_post_ID); + $is_pinned = aksrt_is_comment_pinned($comment); + $perms = aksrt_comment_action_permissions($comment); + $is_pending = '0' === $comment->comment_approved; + $is_spam = 'spam' === $comment->comment_approved; + $like_count = aksrt_get_comment_likes($comment); + $is_liked = aksrt_is_comment_liked($comment_id); + $li_classes = trim($depth_class . ($is_pinned ? ' aksrt-comment--pinned' : '')); + ?> +
  • id="comment-" data-comment-id="" data-comment-status="comment_approved); ?>"> +
      +
    • +
      +
      + 'avatar-img comt-avatar']); ?> + + + + + + + +
      + +
      +
      + +

      + + + +
      + +
    • +
    +
  • + (int) ($settings['article_video_cover_scale'] ?? 56), 'slide_images' => array_values(array_filter((array) ($settings['slide_images'] ?? []))), 'episode_count' => max(0, (int) ($settings['episode_count'] ?? 0)), + 'video_episodes' => is_array($settings['video_episodes'] ?? null) ? array_values($settings['video_episodes']) : [], 'video_poster' => (string) ($settings['video_poster'] ?? ''), 'meta_items' => $meta_items, 'meta_items_secondary' => $meta_items_secondary, @@ -199,7 +200,7 @@ if (!function_exists('aksrt_single_render_cover')) :
    - +
    style="position:absolute;inset:0;width:100%;height:100%;">
    @@ -264,15 +275,13 @@ if (!function_exists('aksrt_single_render_cover')) : 'show_pagination' => true, 'loop' => count($slide_items) > 1, 'autoplay' => count($slide_items) > 1, + 'overlay_html' => $overlay, ]); } ?>
    -
    - - -
    +
    $slide_url) : ?> @@ -453,49 +462,66 @@ if (!function_exists('aksrt_single_render_author_card')) : * @param array $ctx Single context. */ function aksrt_single_render_author_card(array $ctx): void { - if (!function_exists('aksrt_get_user_dashboard_view')) { + $author_id = (int) get_the_author_meta('ID'); + if ($author_id <= 0) { return; } - - $author_id = (int) get_the_author_meta('ID'); - $dashboard = aksrt_get_user_dashboard_view($author_id, ['viewer_id' => get_current_user_id()]); - $recent = is_array($dashboard['recent_posts'] ?? null) ? $dashboard['recent_posts'] : []; - $stats = [ - ['label' => __('文章', 'aksrt'), 'value' => (int) ($dashboard['post_count'] ?? 0)], - ['label' => __('评论', 'aksrt'), 'value' => (int) ($dashboard['comment_count'] ?? 0)], - ['label' => __('收藏', 'aksrt'), 'value' => (int) ($dashboard['favorite_count'] ?? 0)], - ]; + $author_url = get_author_posts_url($author_id); + $author_name = get_the_author(); $bio = trim((string) get_the_author_meta('description')); + $vip_badge = function_exists('aksrt_render_vip_badge') ? aksrt_render_vip_badge($author_id, 'sm') : ''; + $signals = []; + if (class_exists('AKSRT_User_Home')) { + $signals = AKSRT_User_Home::get_instance()->get_profile_signals($author_id); + } + $avatar = function_exists('aksrt_get_avatar_html') + ? aksrt_get_avatar_html($author_id, 80, ['class' => 'avatar avatar-lg']) + : get_avatar($author_id, 80, '', esc_attr($author_name), ['class' => 'avatar avatar-lg']); + $recent = get_posts([ + 'author' => $author_id, + 'post_type' => 'post', + 'post_status' => 'publish', + 'posts_per_page' => 6, + 'orderby' => 'date', + 'order' => 'DESC', + 'no_found_rows' => true, + ]); ?> -
    -
    - - - -
    - - -

    - -
    - - - +
    +
    +
    +
    +
    + +
    +
      + + +
    • + + +
    +
    +
    - -
    -

    -
      - - -
    • - - -
    -
    -
    get_post_type($post_id), + 'post_type' => get_post_type($post_id) ?: 'post', 'post_status' => 'publish', 'post__not_in' => [$post_id], 'posts_per_page' => max(1, min(12, (int) ($settings['related_count'] ?? 6))), @@ -695,21 +721,6 @@ if (!function_exists('aksrt_single_render_related')) : return; } - $posts_html = ''; - while ($query->have_posts()) { - $query->the_post(); - $p = get_post(); - if (!($p instanceof WP_Post)) { - continue; - } - if ($style === 'img' && function_exists('aksrt_render_related_graphic_card')) { - $posts_html .= aksrt_render_related_graphic_card($p); - } else { - $posts_html .= '
  • ' . esc_html(get_the_title($p)) . '
  • '; - } - } - wp_reset_postdata(); - $wrap_class = 'theme-box relates' . ($style === 'img' ? ' relates-thumb' : ''); ?>
    @@ -717,36 +728,51 @@ if (!function_exists('aksrt_single_render_related')) :
    - -
    -
    - -
    -
    -
    -
    - + 'related', - 'mode' => 'mini', - 'show_thumb' => true, - 'show_meta' => true, - 'show_number' => false, - ]); - } - wp_reset_postdata(); + // New route: single query only. + aksrt_render_post_list($query, [ + 'context' => 'related', + 'mode' => 'mini', + 'show_thumb' => true, + 'show_meta' => true, + 'show_number' => false, + ]); ?> -
      - ' . esc_html__('暂无相关文章', 'aksrt') . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> -
    + have_posts()) { + $query->the_post(); + $p = get_post(); + if (!($p instanceof WP_Post)) { + continue; + } + if ($style === 'img' && function_exists('aksrt_render_related_graphic_card')) { + $posts_html .= aksrt_render_related_graphic_card($p); + } else { + $posts_html .= '
  • ' . esc_html(get_the_title($p)) . '
  • '; + } + } + ?> + +
    +
    + +
    +
    +
    +
    + +
      + ' . esc_html__('暂无相关文章', 'aksrt') . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
    +
    '', 'show_nav' => true, 'show_pagination' => true, + // Optional absolute overlay HTML (e.g. single cover title/meta). Not escaped. + 'overlay_html' => '', ]; $args = wp_parse_args($args, $defaults); @@ -90,7 +92,13 @@ function aksrt_render_slider(array $args = []): string { // 高度样式 $height_style = ''; if ($args['scale_height']) { - $height_style = sprintf('--pc-height:auto;--m-height:auto;--scale-ratio:%s;', esc_attr((string) $args['scale_ratio'])); + // padding-bottom percentage for ratio box (Zibll graphic cover style). + $ratio_pct = max(10, min(100, (float) $args['scale_ratio'] * 100)); + $height_style = sprintf( + '--pc-height:auto;--m-height:auto;--scale-ratio:%s;padding-bottom:%s%%;', + esc_attr((string) $args['scale_ratio']), + esc_attr((string) $ratio_pct) + ); } else { $height_style = sprintf('--pc-height:%s;--m-height:%s;', esc_attr($args['pc_height']), esc_attr($args['m_height'])); } @@ -102,11 +110,13 @@ function aksrt_render_slider(array $args = []): string { $wrapper_classes = ['aksrt-slider-wrapper']; if ($args['wrapper_class']) $wrapper_classes[] = $args['wrapper_class']; + if ($args['scale_height']) $wrapper_classes[] = 'aksrt-slider-wrapper--scale'; + if ((string) $args['overlay_html'] !== '') $wrapper_classes[] = 'aksrt-slider-wrapper--has-overlay'; $wrapper_class_str = implode(' ', $wrapper_classes); // 渲染 HTML - $html = '
    '; - $html .= '
    '; + $html = '
    '; + $html .= '
    '; $html .= '
    '; foreach ($slides as $slide) { @@ -116,7 +126,7 @@ function aksrt_render_slider(array $args = []): string { $desc = sanitize_text_field($slide['desc'] ?? ''); $alt = sanitize_text_field($slide['alt'] ?? $title); $bg_color = sanitize_hex_color($slide['bg_color'] ?? ''); - $target = $slide['target'] === '_blank' ? '_blank' : '_self'; + $target = ($slide['target'] ?? '') === '_blank' ? '_blank' : '_self'; $slide_style = $bg_color ? 'background-color:' . esc_attr($bg_color) . ';' : ''; @@ -127,7 +137,7 @@ function aksrt_render_slider(array $args = []): string { } if ($image) { - $html .= '' . esc_attr($alt) . ''; + $html .= '' . esc_attr($alt) . ''; $html .= '
    '; } @@ -158,6 +168,12 @@ function aksrt_render_slider(array $args = []): string { } $html .= '
    '; // .new-swiper + + // Cover title/meta overlay (trusted HTML from single cover renderer). + if ((string) $args['overlay_html'] !== '') { + $html .= '
    ' . $args['overlay_html'] . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + $html .= '
    '; // .aksrt-slider-wrapper return $html;