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 = '';
- $src = 'data:image/svg+xml;charset=UTF-8,' . rawurlencode($svg);
-
- return sprintf(
- '
',
- 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[] = '' . esc_html($home_text) . '';
-
- 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[] = '' . esc_html($ancestor->name) . '';
- }
- }
- $items[] = '' . esc_html(single_cat_title('', false)) . '';
- }
- } elseif (is_tag()) {
- $items[] = '' . esc_html(single_tag_title('', false)) . '';
- } elseif (is_author()) {
- $items[] = '' . esc_html(get_the_author()) . '';
- } elseif (is_search()) {
- $search_query = trim((string) get_search_query());
- $items[] = '' . esc_html($search_query !== '' ? sprintf(__('搜索:%s', 'aksrt'), $search_query) : __('搜索', 'aksrt')) . '';
- } elseif (function_exists('aksrt_is_resource_archive') && aksrt_is_resource_archive()) {
- $items[] = '' . esc_html__('资源', 'aksrt') . '';
- } 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[] = '' . esc_html($ancestor->name) . '';
- }
- }
- $items[] = '' . esc_html($category->name) . '';
- }
- $items[] = '' . esc_html(get_the_title()) . '';
- } elseif (is_page()) {
- $items[] = '' . esc_html(get_the_title()) . '';
- } elseif (is_archive()) {
- $items[] = '' . esc_html(get_the_archive_title()) . '';
- } else {
- $items[] = '' . esc_html(get_the_title()) . '';
- }
-
- 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;
- }
- }
- }
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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); ?>">
-
-
-
+ */
+ 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 = '';
+ $src = 'data:image/svg+xml;charset=UTF-8,' . rawurlencode($svg);
+
+ return sprintf(
+ '
',
+ 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[] = '' . esc_html($home_text) . '';
+
+ 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[] = '' . esc_html($ancestor->name) . '';
+ }
+ }
+ $items[] = '' . esc_html(single_cat_title('', false)) . '';
+ }
+ } elseif (is_tag()) {
+ $items[] = '' . esc_html(single_tag_title('', false)) . '';
+ } elseif (is_author()) {
+ $items[] = '' . esc_html(get_the_author()) . '';
+ } elseif (is_search()) {
+ $search_query = trim((string) get_search_query());
+ $items[] = '' . esc_html($search_query !== '' ? sprintf(__('搜索:%s', 'aksrt'), $search_query) : __('搜索', 'aksrt')) . '';
+ } elseif (function_exists('aksrt_is_resource_archive') && aksrt_is_resource_archive()) {
+ $items[] = '' . esc_html__('资源', 'aksrt') . '';
+ } elseif (is_page()) {
+ $items[] = '' . esc_html(get_the_title()) . '';
+ } elseif (is_archive()) {
+ $items[] = '' . esc_html(get_the_archive_title()) . '';
+ } else {
+ $items[] = '' . esc_html(get_the_title()) . '';
+ }
+
+ 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;
+ }
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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); ?>">
+
+
+
-
-
+
+
0) : ?>
-