From 48f743603066e383d85156560e96fc05ca38d473 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 13 Jul 2026 12:43:07 +0800 Subject: [PATCH] feat(frontend): deepen single post 1:1 parity with Zibll Split related style/source settings, add subtitle/meta/allow-view helpers, align cover/author/tags/pay shell markup and CSS with Zibll single. --- .../src/schema/sections/post-list/page.js | 20 +- assets/css/frontend/breadcrumb.css | 62 +- .../frontend/single-post/author-actions.css | 8 + assets/css/frontend/single-post/cover.css | 64 +- .../css/frontend/single-post/related-toc.css | 23 + inc/bootstrap.php | 1 + .../class-aksrt-admin-schema-post-list.php | 18 +- inc/classes/class-aksrt-settings.php | 77 +- .../class-aksrt-settings-post-display.php | 47 +- inc/helpers/aksrt-frontend-tags.php | 2337 +++++++++-------- inc/helpers/aksrt-pay.php | 8 +- inc/helpers/aksrt-single-components.php | 507 ++++ inc/helpers/aksrt-single.php | 342 +-- templates/parts/single-author-bar.php | 76 +- 14 files changed, 2156 insertions(+), 1434 deletions(-) create mode 100644 inc/helpers/aksrt-single-components.php diff --git a/assets/admin/src/schema/sections/post-list/page.js b/assets/admin/src/schema/sections/post-list/page.js index 2c468e9b..84341277 100644 --- a/assets/admin/src/schema/sections/post-list/page.js +++ b/assets/admin/src/schema/sections/post-list/page.js @@ -21,9 +21,13 @@ export const pageSchema = { post_lightbox_play: false, post_related_enabled: true, post_related_count: 6, + // Display style (Zibll img/list/text). Legacy post_related_type may still hold source or style. + post_related_style: 'img', + post_related_source: 'category', post_related_type: 'category', - post_related_orderby: 'rand', + post_related_orderby: 'views', post_related_title: '相关推荐', + breadcrumbs_single_text: true, post_copyright_enabled: true, post_copyright_text: '', share_s: true, @@ -64,6 +68,7 @@ export const pageSchema = { { key: 'post_reading_progress', label: '阅读进度条', type: 'switch', default: true }, { key: 'breadcrumbs_single_s', label: '面包屑导航', type: 'switch', default: true }, { key: 'breadcrumbs_home_text', label: '面包屑首页文字', type: 'input', default: '首页', dependsOn: { key: 'breadcrumbs_single_s', value: true } }, + { key: 'breadcrumbs_single_text', label: '末项显示「正文」', type: 'switch', default: true, dependsOn: { key: 'breadcrumbs_single_s', value: true }, help: '关闭后末项显示文章标题' }, { key: 'post_prevnext_s', label: '上一篇/下一篇', type: 'switch', default: true }, { key: 'post_authordesc_s', label: '作者简介', type: 'switch', default: true }, { key: 'post_single_hide_time_s', label: '隐藏文章时间', type: 'switch', default: false }, @@ -101,16 +106,21 @@ export const pageSchema = { fields: [ { key: 'post_related_enabled', label: '显示相关推荐', type: 'switch', default: true }, { key: 'post_related_count', label: '推荐数量', type: 'number', default: 6, min: 1, max: 20, dependsOn: { key: 'post_related_enabled', value: true } }, - { key: 'post_related_type', label: '推荐方式', type: 'radio', default: 'category', options: [ - { label: '同分类', value: 'category' }, + { key: 'post_related_style', label: '展示样式', type: 'radio', default: 'img', options: [ + { label: '图文卡片', value: 'img' }, + { label: '列表', value: 'list' }, + { label: '纯文字', value: 'text' }, + ], dependsOn: { key: 'post_related_enabled', value: true }, help: '对齐 Zibll 相关文章三种样式' }, + { key: 'post_related_source', label: '推荐来源', type: 'radio', default: 'category', options: [ + { label: '同分类/标签/专题', value: 'category' }, { label: '同标签', value: 'tag' }, { label: '随机', value: 'random' }, ], dependsOn: { key: 'post_related_enabled', value: true } }, - { key: 'post_related_orderby', label: '推荐排序', type: 'select', default: 'rand', options: [ + { key: 'post_related_orderby', label: '推荐排序', type: 'select', default: 'views', options: [ + { label: '浏览量', value: 'views' }, { label: '随机', value: 'rand' }, { label: '相关度', value: 'relevance' }, { label: '时间', value: 'date' }, - { label: '浏览量', value: 'views' }, { label: '更新时间', value: 'modified' }, ], dependsOn: { key: 'post_related_enabled', value: true } }, { key: 'post_related_title', label: '推荐标题', type: 'input', default: '相关推荐', dependsOn: { key: 'post_related_enabled', value: true } }, diff --git a/assets/css/frontend/breadcrumb.css b/assets/css/frontend/breadcrumb.css index a5603001..f00800a9 100644 --- a/assets/css/frontend/breadcrumb.css +++ b/assets/css/frontend/breadcrumb.css @@ -1,5 +1,7 @@ /* ========== 面包屑导航 — 对齐 zibll breadcrumb ========== */ -.breadcrumb-nav { +.breadcrumb-nav, +ul.breadcrumb, +.breadcrumbs-wrap { font-size: 13px; padding: 0 8px; margin-bottom: 12px; @@ -8,7 +10,9 @@ } .breadcrumb-nav ol, -.breadcrumb-nav ul { +.breadcrumb-nav ul, +ul.breadcrumb, +.breadcrumbs.breadcrumb { list-style: none; margin: 0; padding: 0; @@ -18,59 +22,85 @@ gap: 0; } -.breadcrumb-nav li { +.breadcrumb-nav li, +ul.breadcrumb > li, +.breadcrumbs.breadcrumb > li { display: inline-flex; align-items: center; } -.breadcrumb-nav li + li::before { +/* Zibll flat ul.breadcrumb inserts parents with "
  • " — separators via content */ +.breadcrumb-nav li + li::before, +ul.breadcrumb > li + li::before { content: '/'; - padding: 0 0 0 6px; + padding: 0 6px 0 6px; opacity: 0.6; } -.breadcrumb-nav a { +.breadcrumbs.breadcrumb .breadcrumbs__sep { + padding: 0 6px; + opacity: 0.6; +} + +.breadcrumb-nav a, +ul.breadcrumb a, +.breadcrumbs a { color: var(--color-ink-4, #999); text-decoration: none; transition: color 0.2s; } -.breadcrumb-nav a:hover { +.breadcrumb-nav a:hover, +ul.breadcrumb a:hover, +.breadcrumbs a:hover { color: var(--color-primary, #f04494); } -.breadcrumb-nav .current { +.breadcrumb-nav .current, +ul.breadcrumb > li:last-child, +.breadcrumbs__item.is-current { color: var(--color-ink-2, #4e5358); font-weight: 500; } .single-cover-con .breadcrumb-nav, -.single-cover-con .breadcrumb { +.single-cover-con .breadcrumb, +.single-cover-con ul.breadcrumb, +.post-breadcrumbs .breadcrumb { font-size: 13px; margin: 5px 0 0 -6px; opacity: unset; + padding: 0; } /* 封面内面包屑 */ -.single-cover-con .breadcrumb-nav { - color: rgba(255, 255, 255, 0.7); +.single-cover-con .breadcrumb-nav, +.single-cover-con ul.breadcrumb, +.single-cover-con .breadcrumb { + color: rgba(255, 255, 255, 0.85); } -.single-cover-con .breadcrumb-nav a { - color: rgba(255, 255, 255, 0.7); +.single-cover-con .breadcrumb-nav a, +.single-cover-con ul.breadcrumb a, +.single-cover-con .breadcrumb a { + color: rgba(255, 255, 255, 0.85); } -.single-cover-con .breadcrumb-nav a:hover { +.single-cover-con .breadcrumb-nav a:hover, +.single-cover-con ul.breadcrumb a:hover { color: #fff; } -.single-cover-con .breadcrumb-nav .current { +.single-cover-con .breadcrumb-nav .current, +.single-cover-con ul.breadcrumb > li:last-child { color: #fff; } /* 移动端 */ @media (max-width: 767px) { - .breadcrumb-nav { + .breadcrumb-nav, + ul.breadcrumb, + .breadcrumbs-wrap { font-size: 12px; } } diff --git a/assets/css/frontend/single-post/author-actions.css b/assets/css/frontend/single-post/author-actions.css index a102eddc..e04a84b2 100644 --- a/assets/css/frontend/single-post/author-actions.css +++ b/assets/css/frontend/single-post/author-actions.css @@ -540,3 +540,11 @@ html.dark .post-drop-meta__item:hover { .post-actions .action i { font-size: 16px; } + + +.article-tags br { flex-basis: 100%; width: 100%; content: ""; display: block; height: 0; } +.article-tags .but.c-blue { color: #2196f3; } +.article-tags .but.c-yellow { color: #f0ad4e; } +.article-tags .but.c-green { color: #5cb85c; } +.article-tags .but.c-purple { color: #9c27b0; } +.article-tags .but.c-red { color: #f44336; } diff --git a/assets/css/frontend/single-post/cover.css b/assets/css/frontend/single-post/cover.css index 022527c5..d7e73991 100644 --- a/assets/css/frontend/single-post/cover.css +++ b/assets/css/frontend/single-post/cover.css @@ -211,5 +211,65 @@ .single-head-cover .cover-meta.single-metabox { display: flex; flex-wrap: wrap; gap: 8px 12px; font-size: 12px; color: var(--color-ink-4); } -.aksrt-single-pay-box { margin: 16px 0 20px; } -.aksrt-single-pay-box .pay-box, .aksrt-single-pay-box .aksrt-paywall { margin: 0; } +/* Pay box root — Zibll #posts-pay.zib-widget.pay-box */ +#posts-pay.zib-widget, +#posts-pay.pay-box, +.aksrt-single-pay-box { + margin: 16px 0 20px; +} +#posts-pay .aksrt-paywall, +.aksrt-single-pay-box .pay-box, +.aksrt-single-pay-box .aksrt-paywall { + margin: 0; +} + +/* Cover meta pills + title-h-left */ +.single-cover-con .article-title.title-h-left, +.single-cover .article-title.title-h-left { + color: #fff; + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.45); + margin: 0; +} +.single-metabox.cover-meta, +.cover-meta.abs-right { + position: absolute; + top: 12px; + right: 12px; + z-index: 3; + color: #fff; +} +.single-metabox .post-metas item, +.single-metabox .post-metas item a { + color: inherit; + opacity: 0.92; + margin-left: 10px; + font-size: 12px; +} +.single-metabox .post-metas item .fa, +.single-metabox .post-metas item i { + margin-right: 3px; + opacity: 0.9; +} +.single-cover-slide-wrap { + position: relative; + overflow: hidden; + border-radius: var(--radius-2xl, 16px); +} +.single-cover-slide-wrap .abs-center.left-bottom { + z-index: 2; + padding: 18px 20px; + color: #fff; +} +.article-avatar name.flex { + gap: 4px; + flex-wrap: wrap; +} +.article-avatar .aksrt-vip-badge, +.article-avatar .aksrt-identity-chip { + font-size: 12px; + line-height: 1.2; + vertical-align: middle; +} +.article-avatar .display-name { + margin-right: 2px; +} diff --git a/assets/css/frontend/single-post/related-toc.css b/assets/css/frontend/single-post/related-toc.css index 6ce36a13..277cd053 100644 --- a/assets/css/frontend/single-post/related-toc.css +++ b/assets/css/frontend/single-post/related-toc.css @@ -549,3 +549,26 @@ html.dark .article-end-separator span { .post-nav-box__item.is-empty { visibility: hidden; } + + +/* ===== Related (Zibll .relates / .relates-thumb) ===== */ +.relates .title-theme { margin-bottom: 0; } +.relates .zib-widget { padding: 12px 15px; } +.relates ul.no-thumb { list-style: none; margin: 0; padding: 0; } +.relates ul.no-thumb li { padding: 6px 0; border-bottom: 1px dashed var(--color-line, rgba(0,0,0,.06)); } +.relates ul.no-thumb li:last-child { border-bottom: 0; } +.relates ul.no-thumb a.icon-circle { color: inherit; text-decoration: none; } +.relates ul.no-thumb a.icon-circle:hover { color: var(--focus-color, #f04494); } +.relates-thumb .swiper-scroll { padding-bottom: 4px; } +.relates-thumb .swiper-slide { width: 200px; max-width: 70vw; } +.relates-thumb .graphic.style-3 { position: relative; overflow: hidden; border-radius: 8px; } +.relates-thumb .graphic.style-3 .graphic-img { position: relative; overflow: hidden; background: var(--surface-2, #f3f4f6); } +.relates-thumb .graphic.style-3 img.fit-cover { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; } +.relates-thumb .graphic.style-3 .graphic-text { padding: 10px 12px; width: 100%; background: linear-gradient(transparent, rgba(0,0,0,.55)); color: #fff; } +.relates-thumb .graphic.style-3 .text1 { font-size: 13px; font-weight: 600; color: #fff; } +.relates-thumb .graphic.style-3 .text3 { opacity: .9; margin-top: 4px; } + +/* Colored tag pills on single */ +.article-tags .but { display: inline-flex; align-items: center; gap: 4px; margin: 0 6px 6px 0; } +.article-tags .but.ml6 { margin-left: 0; } +.allow-view-deny { margin: 20px 0; } diff --git a/inc/bootstrap.php b/inc/bootstrap.php index 88b71e30..ea49c56f 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -27,6 +27,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-template-tags.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-frontend-tags.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-flow.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-homepage.php'; +require_once AKSRT_DIR . '/inc/helpers/aksrt-single-components.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-single.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-archive.php'; require_once AKSRT_DIR . '/inc/helpers/aksrt-archive-filters.php'; diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php b/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php index 80e81be0..2ca5ce6b 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-post-list.php @@ -72,6 +72,7 @@ class AKSRT_Admin_Schema_Post_List { ['key' => 'post_reading_progress', 'label' => '阅读进度条', 'type' => 'switch', 'default' => true], ['key' => 'breadcrumbs_single_s', 'label' => '面包屑导航', 'type' => 'switch', 'default' => true], ['key' => 'breadcrumbs_home_text', 'label' => '面包屑首页文字', 'type' => 'input', 'default' => '首页', 'depends_on' => 'breadcrumbs_single_s'], + ['key' => 'breadcrumbs_single_text', 'label' => '末项显示「正文」', 'type' => 'switch', 'default' => true, 'depends_on' => 'breadcrumbs_single_s', 'description' => '关闭后末项显示文章标题'], ['key' => 'post_prevnext_s', 'label' => '上一篇/下一篇', 'type' => 'switch', 'default' => true], ['key' => 'post_authordesc_s', 'label' => '作者简介', 'type' => 'switch', 'default' => true], ['key' => 'post_single_hide_time_s', 'label' => '隐藏文章时间', 'type' => 'switch', 'default' => false], @@ -94,16 +95,27 @@ class AKSRT_Admin_Schema_Post_List { ['key' => 'related', 'title' => '相关推荐', 'fields' => [ ['key' => 'post_related_enabled', 'label' => '显示相关推荐', 'type' => 'switch', 'default' => true], ['key' => 'post_related_count', 'label' => '推荐数量', 'type' => 'number', 'default' => 6, 'min' => 1, 'max' => 20], - ['key' => 'post_related_type', 'label' => '推荐方式', 'type' => 'radio', 'default' => 'category', 'options' => [ + ['key' => 'post_related_style', 'label' => '展示样式', 'type' => 'radio', 'default' => 'img', 'options' => [ + ['label' => '图文卡片', 'value' => 'img'], + ['label' => '列表', 'value' => 'list'], + ['label' => '纯文字', 'value' => 'text'], + ], 'description' => '对齐 Zibll 相关文章三种样式'], + ['key' => 'post_related_source', 'label' => '推荐来源', 'type' => 'radio', 'default' => 'category', 'options' => [ + ['label' => '同分类/标签/专题', 'value' => 'category'], + ['label' => '同标签', 'value' => 'tag'], + ['label' => '随机', 'value' => 'random'], + ]], + // Legacy alias kept so old saved values still round-trip. + ['key' => 'post_related_type', 'label' => '推荐方式(兼容)', 'type' => 'radio', 'default' => 'category', 'options' => [ ['label' => '同分类', 'value' => 'category'], ['label' => '同标签', 'value' => 'tag'], ['label' => '随机', 'value' => 'random'], ]], - ['key' => 'post_related_orderby', 'label' => '推荐排序', 'type' => 'select', 'default' => 'rand', 'options' => [ + ['key' => 'post_related_orderby', 'label' => '推荐排序', 'type' => 'select', 'default' => 'views', 'options' => [ + ['label' => '浏览量', 'value' => 'views'], ['label' => '随机', 'value' => 'rand'], ['label' => '相关度', 'value' => 'relevance'], ['label' => '时间', 'value' => 'date'], - ['label' => '浏览量', 'value' => 'views'], ['label' => '更新时间', 'value' => 'modified'], ]], ['key' => 'post_related_title', 'label' => '推荐标题', 'type' => 'input', 'default' => '相关推荐'], diff --git a/inc/classes/class-aksrt-settings.php b/inc/classes/class-aksrt-settings.php index a93d9ead..e2d15094 100644 --- a/inc/classes/class-aksrt-settings.php +++ b/inc/classes/class-aksrt-settings.php @@ -896,68 +896,25 @@ class AKSRT_Settings { * @return array{toc_enabled:bool,toc_depth:int,reading_progress:bool,lightbox:bool,related_enabled:bool,related_count:int,like_enabled:bool,favorite_enabled:bool,share_enabled:bool,copyright_enabled:bool,print_enabled:bool,pdf_export_enabled:bool,word_count_enabled:bool,reading_time_enabled:bool} */ public static function get_post_page_settings(): array { + // Canonical assembly lives in the post-display helper (related style/source split, aliases). + if (class_exists('AKSRT_Settings_Post_Display')) { + return AKSRT_Settings_Post_Display::get_post_page_settings(); + } + $settings = self::get_all(); - - $related_type = sanitize_key((string) ($settings['post_related_type'] ?? 'category')); - if (!in_array($related_type, ['category', 'tag', 'random'], true)) { - $related_type = 'category'; - } - $related_orderby = sanitize_key((string) ($settings['post_related_orderby'] ?? 'rand')); - if (!in_array($related_orderby, ['rand', 'relevance', 'date', 'views', 'modified'], true)) { - $related_orderby = 'rand'; - } - return [ - 'toc_enabled' => !isset($settings['post_toc_enabled']) || !empty($settings['post_toc_enabled']), - 'toc_depth' => max(1, min(6, (int) ($settings['post_toc_depth'] ?? 3))), - 'reading_progress' => !isset($settings['post_reading_progress']) || !empty($settings['post_reading_progress']), - 'breadcrumbs' => !isset($settings['breadcrumbs_single_s']) || !empty($settings['breadcrumbs_single_s']), - 'breadcrumbs_home' => trim((string) ($settings['breadcrumbs_home_text'] ?? '')) ?: __('首页', 'aksrt'), - 'prevnext' => !isset($settings['post_prevnext_s']) || !empty($settings['post_prevnext_s']), - 'authordesc' => !isset($settings['post_authordesc_s']) || !empty($settings['post_authordesc_s']), - 'hide_time' => !empty($settings['post_single_hide_time_s']), - 'lightbox' => !isset($settings['post_lightbox']) || !empty($settings['post_lightbox']), - 'related_enabled' => !isset($settings['post_related_enabled']) || !empty($settings['post_related_enabled']), - 'related_count' => max(1, min(20, (int) ($settings['post_related_count'] ?? 6))), - 'related_type' => $related_type, - 'related_orderby' => $related_orderby, - 'related_title' => sanitize_text_field((string) ($settings['post_related_title'] ?? '')), - 'like_enabled' => !isset($settings['post_like_enabled']) || !empty($settings['post_like_enabled']), - 'favorite_enabled' => !isset($settings['post_favorite_enabled']) || !empty($settings['post_favorite_enabled']), - 'share_enabled' => !isset($settings['share_s']) || !empty($settings['share_s']) || !empty($settings['post_share_enabled']), - 'rewards_enabled' => !empty($settings['post_rewards_s']) || !empty($settings['post_rewards_enabled']), - 'rewards_text' => trim((string) ($settings['post_rewards_text'] ?? '')) ?: __('赞赏支持', 'aksrt'), - 'rewards_image' => esc_url_raw((string) ($settings['post_rewards_img'] ?? '')), - 'rewards_btn_text' => trim((string) ($settings['post_rewards_btn_text'] ?? '')) ?: __('赞赏', 'aksrt'), - 'copyright_enabled' => !isset($settings['post_copyright_enabled']) || !empty($settings['post_copyright_enabled']), - 'copyright_text' => trim((string) ($settings['post_copyright_text'] ?? '')), - 'front_content' => trim((string) ($settings['post_front_content'] ?? '')), - 'after_content' => trim((string) ($settings['post_after_content'] ?? '')), - 'maxheight_enabled' => !empty($settings['post_maxheight_kg']), - 'maxheight' => max(0, (int) ($settings['post_maxheight'] ?? 1000)), - 'yiyan_header' => !empty($settings['post_yiyan_header']), - 'yiyan_footer' => !empty($settings['post_yiyan_footer']), - 'yiyan_box' => !empty($settings['post_yiyan_box']), - 'default_views_min' => max(0, (int) ($settings['post_default_views_min'] ?? 20)), - 'default_views_max' => max(1, (int) ($settings['post_default_views_max'] ?? 50)), - 'default_likes_min' => max(0, (int) ($settings['post_default_likes_min'] ?? 5)), - 'default_likes_max' => max(1, (int) ($settings['post_default_likes_max'] ?? 15)), - 'print_enabled' => !isset($settings['feature_print']) || !empty($settings['feature_print']), - 'pdf_export_enabled' => !empty($settings['feature_pdf_export']), - 'word_count_enabled' => !isset($settings['feature_word_count']) || !empty($settings['feature_word_count']), - 'reading_time_enabled' => !isset($settings['feature_reading_time']) || !empty($settings['feature_reading_time']), - 'article_image_cover' => !isset($settings['post_article_image_cover']) || !empty($settings['post_article_image_cover']), - 'article_image_cover_scale' => max(10, min(100, (int) ($settings['post_article_image_cover_scale'] ?? 35))), - 'article_slide_cover' => !isset($settings['post_article_slide_cover']) || !empty($settings['post_article_slide_cover']), - 'article_video_cover' => !isset($settings['post_article_video_cover']) || !empty($settings['post_article_video_cover']), - 'article_video_cover_scale' => max(20, min(100, (int) ($settings['post_article_video_cover_scale'] ?? 56))), - 'button_toptext' => trim((string) ($settings['post_button_toptext'] ?? '')) ?: __('喜欢就支持一下吧', 'aksrt'), - 'lightbox_thumbs' => !isset($settings['post_lightbox_thumbs']) || !empty($settings['post_lightbox_thumbs']), - 'lightbox_zoom' => !isset($settings['post_lightbox_zoom']) || !empty($settings['post_lightbox_zoom']), - 'lightbox_full' => !isset($settings['post_lightbox_full']) || !empty($settings['post_lightbox_full']), - 'lightbox_down' => !empty($settings['post_lightbox_down']), - 'lightbox_play' => !empty($settings['post_lightbox_play']), - 'mobile_nav_s' => !isset($settings['article_nav_mobile_nav_s']) || !empty($settings['article_nav_mobile_nav_s']), + 'toc_enabled' => !isset($settings['post_toc_enabled']) || !empty($settings['post_toc_enabled']), + 'toc_depth' => max(1, min(6, (int) ($settings['post_toc_depth'] ?? 3))), + 'reading_progress' => !isset($settings['post_reading_progress']) || !empty($settings['post_reading_progress']), + 'breadcrumbs' => !isset($settings['breadcrumbs_single_s']) || !empty($settings['breadcrumbs_single_s']), + 'breadcrumbs_home' => trim((string) ($settings['breadcrumbs_home_text'] ?? '')) ?: __('首页', 'aksrt'), + 'related_enabled' => !isset($settings['post_related_enabled']) || !empty($settings['post_related_enabled']), + 'related_count' => max(1, min(20, (int) ($settings['post_related_count'] ?? 6))), + 'related_style' => 'img', + 'related_source' => 'category', + 'related_type' => 'category', + 'related_orderby' => 'views', + 'related_title' => sanitize_text_field((string) ($settings['post_related_title'] ?? '相关推荐')), ]; } diff --git a/inc/classes/settings/class-aksrt-settings-post-display.php b/inc/classes/settings/class-aksrt-settings-post-display.php index 432f4f25..e33c6254 100644 --- a/inc/classes/settings/class-aksrt-settings-post-display.php +++ b/inc/classes/settings/class-aksrt-settings-post-display.php @@ -231,30 +231,61 @@ class AKSRT_Settings_Post_Display { // Canonical render keys (used by aksrt_single_*). Legacy *_s aliases kept for older callers. $breadcrumbs = !isset($settings['breadcrumbs_single_s']) || !empty($settings['breadcrumbs_single_s']); $breadcrumbs_home = sanitize_text_field((string) ($settings['breadcrumbs_home_text'] ?? '首页')); + // Zibll breadcrumbs_single_text: true → last crumb is "正文"; false → full title. + $breadcrumbs_single_text = !isset($settings['breadcrumbs_single_text']) || !empty($settings['breadcrumbs_single_text']); $prevnext = !isset($settings['post_prevnext_s']) || !empty($settings['post_prevnext_s']); $authordesc = !isset($settings['post_authordesc_s']) || !empty($settings['post_authordesc_s']); $hide_time = !empty($settings['post_single_hide_time_s']); $share_enabled = !isset($settings['share_s']) || !empty($settings['share_s']); $rewards_enabled = !empty($settings['post_rewards_enabled']) || !empty($settings['post_rewards_s']); - $rewards_image = (string) ($settings['post_rewards_image'] ?? $settings['post_rewards_qrcode'] ?? ''); + $rewards_image = (string) ($settings['post_rewards_image'] ?? $settings['post_rewards_qrcode'] ?? $settings['post_rewards_img'] ?? ''); $rewards_text = sanitize_text_field((string) ($settings['post_rewards_text'] ?? '')); $rewards_btn_text = sanitize_text_field((string) ($settings['post_rewards_btn_text'] ?? '赞赏')); - $maxheight_enabled = !empty($settings['article_maxheight_kg']) || !empty($settings['post_article_maxheight_kg']); - $maxheight = max(0, (int) ($settings['article_maxheight'] ?? $settings['post_article_maxheight'] ?? 1000)); + $maxheight_enabled = !empty($settings['article_maxheight_kg']) + || !empty($settings['post_article_maxheight_kg']) + || !empty($settings['post_maxheight_kg']); + $maxheight = max(0, (int) ($settings['article_maxheight'] ?? $settings['post_article_maxheight'] ?? $settings['post_maxheight'] ?? 1000)); if ($maxheight <= 0) { $maxheight = 1000; } + // Related: split Zibll display style (img/list/text) from query source (category/tag/random). + // Legacy post_related_type held either style (img|list|text) or source (category|tag|random). + $raw_related_type = sanitize_key((string) ($settings['post_related_type'] ?? '')); + $raw_related_style = sanitize_key((string) ($settings['post_related_style'] ?? '')); + $raw_related_source = sanitize_key((string) ($settings['post_related_source'] ?? '')); + + $related_style = 'img'; + if (in_array($raw_related_style, ['img', 'list', 'text'], true)) { + $related_style = $raw_related_style; + } elseif (in_array($raw_related_type, ['img', 'list', 'text'], true)) { + $related_style = $raw_related_type; + } + + $related_source = 'category'; + if (in_array($raw_related_source, ['category', 'tag', 'random'], true)) { + $related_source = $raw_related_source; + } elseif (in_array($raw_related_type, ['category', 'tag', 'random'], true)) { + $related_source = $raw_related_type; + } + + $related_orderby = sanitize_key((string) ($settings['post_related_orderby'] ?? 'views')); + if (!in_array($related_orderby, ['rand', 'relevance', 'date', 'views', 'modified'], true)) { + $related_orderby = 'views'; + } + return [ 'toc_enabled' => !isset($settings['post_toc_enabled']) || !empty($settings['post_toc_enabled']), 'toc_depth' => max(1, min(6, (int) ($settings['post_toc_depth'] ?? 3))), 'article_nav_mobile_nav_s' => !isset($settings['article_nav_mobile_nav_s']) || !empty($settings['article_nav_mobile_nav_s']), + 'mobile_nav_s' => !isset($settings['article_nav_mobile_nav_s']) || !empty($settings['article_nav_mobile_nav_s']), 'reading_progress' => !isset($settings['post_reading_progress']) || !empty($settings['post_reading_progress']), // Canonical + legacy breadcrumbs. 'breadcrumbs' => $breadcrumbs, 'breadcrumbs_home' => $breadcrumbs_home, 'breadcrumbs_single_s' => $breadcrumbs, 'breadcrumbs_home_text' => $breadcrumbs_home, + 'breadcrumbs_single_text' => $breadcrumbs_single_text, // Canonical + legacy prev/next / author / time. 'prevnext' => $prevnext, 'prevnext_s' => $prevnext, @@ -275,8 +306,12 @@ class AKSRT_Settings_Post_Display { 'lightbox_play' => !empty($settings['post_lightbox_play']), 'related_enabled' => !isset($settings['post_related_enabled']) || !empty($settings['post_related_enabled']), 'related_count' => max(1, min(20, (int) ($settings['post_related_count'] ?? 6))), - 'related_type' => in_array(sanitize_key((string) ($settings['post_related_type'] ?? 'category')), ['category', 'tag', 'random'], true) ? sanitize_key((string) ($settings['post_related_type'] ?? 'category')) : 'category', - 'related_orderby' => in_array(sanitize_key((string) ($settings['post_related_orderby'] ?? 'rand')), ['rand', 'relevance', 'date', 'views', 'modified'], true) ? sanitize_key((string) ($settings['post_related_orderby'] ?? 'rand')) : 'rand', + // Display style (Zibll img/list/text) + query source (category/tag/random). + 'related_style' => $related_style, + 'related_source' => $related_source, + // Legacy alias: older code treated related_type as source. + 'related_type' => $related_source, + 'related_orderby' => $related_orderby, 'related_title' => sanitize_text_field((string) ($settings['post_related_title'] ?? '相关推荐')), 'like_enabled' => !isset($settings['post_like_enabled']) || !empty($settings['post_like_enabled']), 'favorite_enabled' => !isset($settings['post_favorite_enabled']) || !empty($settings['post_favorite_enabled']), @@ -305,6 +340,8 @@ class AKSRT_Settings_Post_Display { 'default_likes_max' => max(1, min(9999, (int) ($settings['post_default_likes_max'] ?? 15))), 'word_count_enabled' => !isset($settings['post_word_count_enabled']) || !empty($settings['post_word_count_enabled']), 'reading_time_enabled' => !isset($settings['post_reading_time_enabled']) || !empty($settings['post_reading_time_enabled']), + 'print_enabled' => !isset($settings['feature_print']) || !empty($settings['feature_print']), + 'pdf_export_enabled' => !empty($settings['feature_pdf_export']), ]; } diff --git a/inc/helpers/aksrt-frontend-tags.php b/inc/helpers/aksrt-frontend-tags.php index e8db0f06..9c83700c 100644 --- a/inc/helpers/aksrt-frontend-tags.php +++ b/inc/helpers/aksrt-frontend-tags.php @@ -1,1154 +1,1183 @@ - - */ - 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_type' => 'category', - 'related_orderby' => 'rand', - 'related_title' => '', - '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']); - } - - $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 - */ - function aksrt_breadcrumbs(string $home_text = '') { - if (is_front_page()) { - return; - } - - if ($home_text === '') { - $home_text = __('首页', 'aksrt'); - } - - $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_singular('post')) { - $cats = get_the_category(); - if (!empty($cats)) { - $category = $cats[0]; - $ancestors = array_reverse(get_ancestors($category->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[] = ''; - } - $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) : []; + 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']); ?> + + + + + + + +
      + +
      +
      + +

      + + + +
      + +
    • +
    +
  • + - - + + 0) : ?> -

    diff --git a/inc/helpers/aksrt-single-components.php b/inc/helpers/aksrt-single-components.php new file mode 100644 index 00000000..839b3347 --- /dev/null +++ b/inc/helpers/aksrt-single-components.php @@ -0,0 +1,507 @@ +' . esc_html($subtitle) . ''; + } +endif; + +if (!function_exists('aksrt_get_single_title_html')) : + /** + * Title + optional subtitle for single headers. + */ + function aksrt_get_single_title_html($post_id = null): string { + $post_id = $post_id ? (int) $post_id : (int) get_the_ID(); + $title = get_the_title($post_id); + $sub = function_exists('aksrt_get_the_subtitle') ? aksrt_get_the_subtitle(true, '', $post_id) : ''; + return esc_html($title) . $sub; + } +endif; + +if (!function_exists('aksrt_get_posts_meta_items_html')) : + /** + * Zibll-like post meta pills: comment + views + likes. + * + * @param int|null $post_id Post ID. + */ + function aksrt_get_posts_meta_items_html($post_id = null): string { + $post = get_post($post_id); + if (!($post instanceof WP_Post)) { + return ''; + } + $post_id = (int) $post->ID; + $html = ''; + + $comments_closed = function_exists('aksrt_are_comments_closed') + ? aksrt_are_comments_closed() + : (!comments_open($post_id)); + if (comments_open($post_id) && !$comments_closed) { + $href = is_single($post_id) + ? 'javascript:(typeof scrollTopTo===\'function\'?scrollTopTo(\'#comments\'):void(0));' + : get_comments_link($post_id); + $count = (int) get_comments_number($post_id); + $html .= '' + . ' ' . esc_html((string) $count) . ''; + } + + $views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post_id) : 0; + $views_label = function_exists('aksrt_format_number') ? aksrt_format_number($views) : number_format_i18n($views); + $html .= ' ' . esc_html((string) $views_label) . ''; + + $like_on = true; + if (class_exists('AKSRT_Settings')) { + $page = AKSRT_Settings::get_post_page_settings(); + $like_on = !isset($page['like_enabled']) || !empty($page['like_enabled']); + } + if ($like_on && $post->post_type === 'post') { + $likes = function_exists('aksrt_get_post_likes') ? (int) aksrt_get_post_likes($post_id) : 0; + $html .= ' ' . esc_html((string) $likes) . ''; + } + + return $html; + } +endif; + +if (!function_exists('aksrt_get_single_meta_box')) : + /** + * Zibll zib_get_single_meta_box equivalent. + * + * @param string $class Extra class on wrapper. + * @param string $direction Unused (kept for API parity). + * @param int|null $post_id Post ID. + */ + function aksrt_get_single_meta_box(string $class = 'abs-right', string $direction = 'down', $post_id = null): string { + if (!is_single() && !is_singular()) { + return ''; + } + $post_id = $post_id ? (int) $post_id : (int) get_the_ID(); + $meta = function_exists('aksrt_get_posts_meta_items_html') ? aksrt_get_posts_meta_items_html($post_id) : ''; + + $more = ''; + $edit_link = get_edit_post_link($post_id); + $permalink = get_permalink($post_id); + $login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url($permalink ?: home_url('/')); + + $more .= '

    '; + $more .= ''; + $more .= '
    '; + + return '
    ' . $more . '
    '; + } +endif; + +if (!function_exists('aksrt_get_post_status_badge')) : + /** + * Non-publish status badge (text badge, Zibll-compatible classes). + */ + function aksrt_get_post_status_badge($post = null): string { + $post = get_post($post); + if (!($post instanceof WP_Post)) { + return ''; + } + $map = [ + 'draft' => ['label' => __('草稿', 'aksrt'), 'class' => 'jb-yellow'], + 'pending' => ['label' => __('待审核', 'aksrt'), 'class' => 'jb-red'], + 'private' => ['label' => __('私密', 'aksrt'), 'class' => 'b-black opacity8'], + 'future' => ['label' => __('定时发布', 'aksrt'), 'class' => 'c-blue'], + 'trash' => ['label' => __('回收站', 'aksrt'), 'class' => 'jb-red'], + ]; + $status = $post->post_status; + if (!isset($map[$status])) { + return ''; + } + return '' + . esc_html($map[$status]['label']) . ''; + } +endif; + +if (!function_exists('aksrt_get_colored_term_tags')) : + /** + * Colored but-links for terms (Zibll cat/topics palette). + * + * @param WP_Term[] $terms Terms. + * @param string $class Base class. + * @param string $before Prefix HTML inside link. + * @param string[] $palette Color classes. + */ + function aksrt_get_colored_term_tags(array $terms, string $class = 'but ml6 radius', string $before = '', array $palette = []): string { + if ($palette === []) { + $palette = ['c-blue', 'c-yellow', 'c-green', 'c-purple', 'c-red']; + } + $html = ''; + $i = 0; + foreach ($terms as $term) { + if (!($term instanceof WP_Term)) { + continue; + } + $color = $palette[$i % count($palette)]; + $i++; + $url = get_term_link($term); + if (is_wp_error($url)) { + continue; + } + $html .= '' + . $before . esc_html($term->name) . ''; + } + return $html; + } +endif; + +if (!function_exists('aksrt_get_single_article_tags_html')) : + /** + * topics → categories → tags (Zibll footer order). + */ + function aksrt_get_single_article_tags_html(int $post_id): string { + $topics_html = ''; + if (taxonomy_exists('topics')) { + $topics = wp_get_post_terms($post_id, 'topics'); + if (!is_wp_error($topics) && !empty($topics)) { + $topics_html = aksrt_get_colored_term_tags( + array_slice($topics, 0, 6), + 'but ml6 radius', + ' ', + ['c-yellow', 'c-green', 'c-purple', 'c-red', 'c-blue'] + ); + } + } + + $cats = get_the_category($post_id) ?: []; + $cat_html = aksrt_get_colored_term_tags( + $cats, + 'but ml6 radius', + ' ', + ['c-blue', 'c-yellow', 'c-green', 'c-purple', 'c-red'] + ); + + $tags_html = ''; + $tags = get_the_tags($post_id) ?: []; + if (!empty($tags) && is_array($tags)) { + // Sort by count desc like Zibll. + usort($tags, static function ($a, $b) { + return (int) $b->count <=> (int) $a->count; + }); + foreach ($tags as $tag) { + if (!($tag instanceof WP_Term)) { + continue; + } + $tags_html .= '# ' . esc_html($tag->name) . ''; + } + } + + $top = $topics_html . $cat_html; + if ($top === '' && $tags_html === '') { + return ''; + } + return ''; + } +endif; + +if (!function_exists('aksrt_get_post_allow_view_data')) : + /** + * Category/post reading permission gate (Zibll zib_get_post_allow_view_data subset). + * + * Supported term meta keys (first match wins, walks parents): + * - term meta `post_cat_config` array with `allow_view` = signin|login|roles + * - term meta `aksrt_allow_view` string + * - post meta `_aksrt_allow_view` string (public|login|signin|roles) + * + * @return array{allow:bool,type:string,not_html:string,allow_reason:string,html:string} + */ + function aksrt_get_post_allow_view_data($post = null): array { + static $cache = []; + + $data = [ + 'allow' => true, + 'type' => '', + 'not_html' => '', + 'allow_reason' => '', + 'html' => '', + ]; + + $post = get_post($post); + if (!($post instanceof WP_Post) || $post->post_type !== 'post') { + return $data; + } + + if (isset($cache[$post->ID])) { + return $cache[$post->ID]; + } + + if (current_user_can('manage_options')) { + $data['allow_reason'] = __('您是管理员,可查看所有内容', 'aksrt'); + $cache[$post->ID] = $data; + return $data; + } + + // Post-level override. + $post_allow = sanitize_key((string) get_post_meta($post->ID, '_aksrt_allow_view', true)); + if ($post_allow !== '' && $post_allow !== 'public') { + $resolved = aksrt_resolve_allow_view_rule($post_allow, [], $post); + $cache[$post->ID] = $resolved; + return $resolved; + } + + $cats = get_the_category($post->ID); + if (empty($cats)) { + $cache[$post->ID] = $data; + return $data; + } + + foreach ($cats as $cat) { + if (!($cat instanceof WP_Term)) { + continue; + } + $rule = aksrt_get_term_allow_view_rule((int) $cat->term_id); + if ($rule['type'] === '' || $rule['type'] === 'public') { + continue; + } + $resolved = aksrt_resolve_allow_view_rule($rule['type'], $rule['roles'], $post); + if (!$resolved['allow']) { + $cache[$post->ID] = $resolved; + return $resolved; + } + } + + $cache[$post->ID] = $data; + return $data; + } +endif; + +if (!function_exists('aksrt_get_term_allow_view_rule')) : + /** + * Walk category ancestors for allow_view config. + * + * @return array{type:string,roles:array} + */ + function aksrt_get_term_allow_view_rule(int $term_id): array { + static $term_cache = []; + if (isset($term_cache[$term_id])) { + return $term_cache[$term_id]; + } + + $empty = ['type' => '', 'roles' => []]; + $chain = array_merge([$term_id], get_ancestors($term_id, 'category')); + foreach ($chain as $id) { + $id = (int) $id; + $config = get_term_meta($id, 'post_cat_config', true); + if (is_array($config) && !empty($config['allow_view'])) { + $rule = [ + 'type' => sanitize_key((string) $config['allow_view']), + 'roles' => is_array($config['allow_view_roles'] ?? null) ? $config['allow_view_roles'] : [], + ]; + $term_cache[$term_id] = $rule; + return $rule; + } + $flat = sanitize_key((string) get_term_meta($id, 'aksrt_allow_view', true)); + if ($flat !== '' && $flat !== 'public') { + $rule = ['type' => $flat, 'roles' => []]; + $term_cache[$term_id] = $rule; + return $rule; + } + } + + $term_cache[$term_id] = $empty; + return $empty; + } +endif; + +if (!function_exists('aksrt_resolve_allow_view_rule')) : + /** + * @param array $roles Role constraints for type=roles. + * @return array{allow:bool,type:string,not_html:string,allow_reason:string,html:string} + */ + function aksrt_resolve_allow_view_rule(string $type, array $roles, WP_Post $post): array { + $type = sanitize_key($type); + if (in_array($type, ['login', 'signin'], true)) { + $type = 'signin'; + } + + $data = [ + 'allow' => true, + 'type' => $type, + 'not_html' => '', + 'allow_reason' => '', + 'html' => '', + ]; + + $user_id = get_current_user_id(); + $login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink($post)); + + if ($type === 'signin') { + $data['html'] = '

    ' . esc_html__('登录后可查看', 'aksrt') . '

    '; + if ($user_id <= 0) { + $data['allow'] = false; + $data['not_html'] = aksrt_get_allow_view_deny_shell( + __('内容已隐藏,请登录后查看', 'aksrt'), + '

    ' . esc_html__('登录后继续查看', 'aksrt') . '

    ' + . '

    ' . esc_html__('登录', 'aksrt') . '

    ' + ); + } else { + $data['allow_reason'] = __('您已登录,可查看此内容', 'aksrt'); + } + return $data; + } + + if ($type === 'roles') { + $need_vip = isset($roles['vip']) ? (int) $roles['vip'] : 0; + $need_level = isset($roles['level']) ? (int) $roles['level'] : 0; + $need_auth = !empty($roles['auth']); + + $badges = ''; + if ($need_vip > 0) { + $badges .= '' . esc_html(sprintf(__('VIP%d 及以上', 'aksrt'), $need_vip)) . ''; + } + if ($need_level > 0) { + $badges .= '' . esc_html(sprintf(__('等级 %d 及以上', 'aksrt'), $need_level)) . ''; + } + if ($need_auth) { + $badges .= '' . esc_html__('认证用户', 'aksrt') . ''; + } + $data['html'] = '

    ' . esc_html__('以下用户组可查看', 'aksrt') . '

    ' . $badges . '
    '; + + $ok = false; + if ($user_id > 0) { + if ($need_vip > 0 && function_exists('aksrt_get_user_vip_level') && aksrt_get_user_vip_level($user_id) >= $need_vip) { + $ok = true; + } + if (!$ok && $need_level > 0) { + $user_level = (int) get_user_meta($user_id, 'aksrt_user_level', true); + if ($user_level >= $need_level) { + $ok = true; + } + } + if (!$ok && $need_auth) { + $verify = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_verify_status', true)); + if (in_array($verify, ['verified', '1', 'yes'], true)) { + $ok = true; + } + } + // If no specific role constraints, any logged-in user passes. + if (!$ok && $need_vip <= 0 && $need_level <= 0 && !$need_auth) { + $ok = true; + } + } + + if (!$ok) { + $data['allow'] = false; + $body = $data['html']; + if ($user_id <= 0) { + $body .= '

    ' . esc_html__('登录', 'aksrt') . '

    '; + } + $data['not_html'] = aksrt_get_allow_view_deny_shell(__('内容已隐藏', 'aksrt'), $body); + } else { + $data['allow_reason'] = __('您满足查看条件', 'aksrt'); + } + return $data; + } + + // Unknown type → allow. + $data['type'] = ''; + return $data; + } +endif; + +if (!function_exists('aksrt_get_allow_view_deny_shell')) : + function aksrt_get_allow_view_deny_shell(string $title, string $body_html): string { + return '
    ' + . '
    ' + . '

    ' + . '

    ' . esc_html($title) . '

    ' + . $body_html + . '
    '; + } +endif; + +if (!function_exists('aksrt_single_content_allow_view')) : + /** + * the_content filter — hide body when category/post allow_view denies. + */ + function aksrt_single_content_allow_view($content) { + if (is_admin() || !is_singular('post') || !in_the_loop() || !is_main_query()) { + return $content; + } + $data = aksrt_get_post_allow_view_data(get_post()); + if (!empty($data['allow'])) { + return $content; + } + return (string) ($data['not_html'] ?? ''); + } +endif; + +// Register gate once helpers load. +add_filter('the_content', 'aksrt_single_content_allow_view', 8); + +if (!function_exists('aksrt_render_related_graphic_card')) : + /** + * Minimal Zibll style-3 related thumb card. + */ + function aksrt_render_related_graphic_card(WP_Post $post): string { + $title = get_the_title($post); + $url = get_permalink($post); + $thumb = get_the_post_thumbnail_url($post, 'medium_large'); + if (!$thumb) { + $thumb = get_theme_file_uri('assets/img/thumbnail-lg.svg'); + if (!file_exists(get_theme_file_path('assets/img/thumbnail-lg.svg'))) { + $thumb = includes_url('images/media/default.png'); + } + } + $time = human_time_diff((int) get_the_time('U', $post), (int) current_time('timestamp')); + $views = function_exists('aksrt_get_post_views') ? (int) aksrt_get_post_views($post->ID) : 0; + $views_label = function_exists('aksrt_format_number') ? aksrt_format_number($views) : number_format_i18n($views); + + $img_attrs = function_exists('aksrt_lazy_img_attrs') + ? aksrt_lazy_img_attrs($thumb, 'cover', 'fit-cover') + : 'class="fit-cover" src="' . esc_url($thumb) . '" loading="lazy"'; + + $html = ''; + return $html; + } +endif; diff --git a/inc/helpers/aksrt-single.php b/inc/helpers/aksrt-single.php index 1978da8f..ea77c92a 100644 --- a/inc/helpers/aksrt-single.php +++ b/inc/helpers/aksrt-single.php @@ -33,32 +33,20 @@ if (!function_exists('aksrt_single_build_context')) : ? aksrt_reading_minutes($post_id) : 0; + // Zibll primary meta = comment/views/likes via meta_box_html. $meta_items = []; - if (empty($settings['hide_time'])) { - $meta_items[] = [ - 'icon' => 'far fa-calendar-alt', - 'text' => get_the_date('', $post_id), - ]; - } - $meta_items[] = [ - 'icon' => 'far fa-eye', - 'text' => sprintf( - __('浏览 %s', 'aksrt'), - function_exists('aksrt_format_number') ? aksrt_format_number($views) : number_format_i18n($views) - ), - ]; - $meta_items[] = [ - 'icon' => 'far fa-comment-dots', - 'text' => sprintf(__('评论 %s', 'aksrt'), number_format_i18n($comment_count)), - ]; + $meta_box_html = function_exists('aksrt_get_single_meta_box') + ? aksrt_get_single_meta_box('abs-right', 'down', $post_id) + : ''; + $meta_items_secondary = []; if ($word_count > 0) { - $meta_items[] = [ + $meta_items_secondary[] = [ 'icon' => 'far fa-file-word', 'text' => sprintf(__('约 %s 字', 'aksrt'), number_format_i18n($word_count)), ]; } if ($reading_minutes > 0) { - $meta_items[] = [ + $meta_items_secondary[] = [ 'icon' => 'far fa-hourglass', 'text' => sprintf(_n('约 %d 分钟', '约 %d 分钟', $reading_minutes, 'aksrt'), $reading_minutes), ]; @@ -92,23 +80,23 @@ if (!function_exists('aksrt_single_build_context')) : $breadcrumbs_html = ''; if (!empty($settings['breadcrumbs']) && function_exists('aksrt_breadcrumbs')) { ob_start(); - aksrt_breadcrumbs($settings['breadcrumbs_home'] ?? ($settings['breadcrumbs_home_text'] ?? '')); + aksrt_breadcrumbs( + (string) ($settings['breadcrumbs_home'] ?? ($settings['breadcrumbs_home_text'] ?? '')), + ['single_text' => !isset($settings['breadcrumbs_single_text']) || !empty($settings['breadcrumbs_single_text'])] + ); $breadcrumbs_html = trim((string) ob_get_clean()); } - $status_badge = ''; - $post_status = get_post_status($post_id); - if ($post_status && $post_status !== 'publish') { - $status_map = [ - 'draft' => ['label' => __('草稿', 'aksrt'), 'class' => 'jb-yellow'], - 'pending' => ['label' => __('待审核', 'aksrt'), 'class' => 'jb-red'], - 'private' => ['label' => __('私密', 'aksrt'), 'class' => 'b-black opacity8'], - 'future' => ['label' => __('定时发布', 'aksrt'), 'class' => 'c-blue'], - ]; - if (isset($status_map[$post_status])) { - $status_badge = '' - . esc_html($status_map[$post_status]['label']) . ''; - } + $status_badge = function_exists('aksrt_get_post_status_badge') + ? aksrt_get_post_status_badge($post_id) + : ''; + + $allow_view = function_exists('aksrt_get_post_allow_view_data') + ? aksrt_get_post_allow_view_data($post_id) + : ['allow' => true]; + if (empty($allow_view['allow'])) { + // Zibll: pay box is skipped when allow_view denies. + $show_pay_box = false; } $aside_widgets = is_active_sidebar('sidebar-article') @@ -128,6 +116,10 @@ if (!function_exists('aksrt_single_build_context')) : 'episode_count' => max(0, (int) ($settings['episode_count'] ?? 0)), 'video_poster' => (string) ($settings['video_poster'] ?? ''), 'meta_items' => $meta_items, + 'meta_items_secondary' => $meta_items_secondary, + 'meta_box_html' => $meta_box_html, + 'title_html' => function_exists('aksrt_get_single_title_html') ? aksrt_get_single_title_html($post_id) : esc_html(get_the_title($post_id)), + 'allow_view' => $allow_view, 'views' => $views, 'comment_count' => $comment_count, 'like_count' => $like_count, @@ -194,46 +186,52 @@ if (!function_exists('aksrt_single_render_cover')) : $type = (string) $ctx['cover_type']; $badge = (string) $ctx['status_badge']; $crumbs = (string) $ctx['breadcrumbs_html']; - $meta_items = is_array($ctx['meta_items'] ?? null) ? $ctx['meta_items'] : []; + $meta_box = (string) ($ctx['meta_box_html'] ?? ''); $img_scale = max(10, min(100, (int) $ctx['img_scale'])); $video_scale = max(20, min(100, (int) $ctx['video_scale'])); $episode = (int) $ctx['episode_count']; $slides = is_array($ctx['slide_images'] ?? null) ? $ctx['slide_images'] : []; $poster = (string) ($ctx['video_poster'] ?? ''); - $title = get_the_title(); + $title_html = (string) ($ctx['title_html'] ?? esc_html(get_the_title())); if ($type === 'video') : ?>
    -
    - -
    -
    -