fix(frontend): close single 1:1 residuals without legacy paths

Unify TOC ownership, real video episode list, slider overlay, single-query related list, Zibll user-card after-box, and new-route subtitle/meta contracts.
This commit is contained in:
2026-07-13 13:18:19 +08:00
parent 48f7436030
commit 5ce43c4326
10 changed files with 1373 additions and 1298 deletions
@@ -548,3 +548,17 @@ html.dark .post-drop-meta__item:hover {
.article-tags .but.c-green { color: #5cb85c; }
.article-tags .but.c-purple { color: #9c27b0; }
.article-tags .but.c-red { color: #f44336; }
/* ===== Zibll user-card after-box ===== */
.user-card.author.zib-widget { margin-bottom: 16px; overflow: hidden; }
.user-card.author .user-avatar { display: flex; justify-content: center; }
.user-card.author .user-avatar .avatar,
.user-card.author .user-avatar img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; }
.user-card.author .user-name { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 4px; }
.user-card.author .user-name .display-name { font-weight: 700; color: var(--key-color, inherit); }
.user-card.author .user-desc { text-align: center; }
.user-card.author .user-btns { text-align: center; }
.user-card.author .user-card-posts ul { list-style: none; margin: 0; padding: 0; }
.user-card.author .user-card-posts li { padding: 6px 0; border-bottom: 1px dashed var(--color-line, rgba(0,0,0,.06)); }
.user-card.author .user-card-posts li:last-child { border-bottom: 0; }
+16
View File
@@ -273,3 +273,19 @@
.article-avatar .display-name {
margin-right: 2px;
}
/* Slide cover overlay (injected by aksrt_render_slider overlay_html) */
.aksrt-slider-wrapper--has-overlay,
.aksrt-slider-wrapper--scale { position: relative; overflow: hidden; border-radius: var(--radius-2xl, 16px); }
.aksrt-slider-wrapper--scale .new-swiper { position: absolute; inset: 0; width: 100%; height: 100%; }
.aksrt-slider-wrapper--scale .swiper-slide img.fit-cover { width: 100%; height: 100%; object-fit: cover; }
.aksrt-slider-overlay { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.aksrt-slider-overlay .abs-center.left-bottom { pointer-events: none; padding: 18px 20px; color: #fff; }
.aksrt-slider-overlay .article-title,
.aksrt-slider-overlay .article-title.title-h-left { color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,.45); }
.aksrt-slider-overlay .breadcrumb a,
.aksrt-slider-overlay .breadcrumb li { color: rgba(255,255,255,.9); }
.aksrt-slider-overlay .single-metabox,
.aksrt-slider-overlay .cover-meta { pointer-events: auto; }
.aksrt-slider-overlay .post-drop-meta { pointer-events: auto; }
+13 -1
View File
@@ -19,19 +19,31 @@
var navContainer = document.querySelector('[data-nav]');
if (!navContainer) return;
// Single-post TOC is owned by page-template.js (.posts-nav-box .toc-list).
// Do not invent a second .aksrt-section-nav tree on article singles.
if (document.querySelector('.posts-nav-box .toc-list, .posts-nav-box.toc-widget')) {
return;
}
var headings = navContainer.querySelectorAll(HEADING_SEL);
if (headings.length < MIN_HEADINGS) return;
var tocContainer = document.querySelector('.posts-nav-lists, .aksrt-section-nav');
if (!tocContainer) {
// Docs / page-navs only: create a dedicated container. Never inject into single sidebars.
if (document.body.classList.contains('single') || document.body.classList.contains('single-post')) {
return;
}
tocContainer = document.createElement('nav');
tocContainer.className = 'aksrt-section-nav';
tocContainer.setAttribute('aria-label', '文章目录');
var sidebar = document.querySelector('.content-layout__aside, .sidebar, .widget-area');
if (sidebar) {
sidebar.insertBefore(tocContainer, sidebar.firstChild);
} else {
} else if (navContainer.parentNode) {
navContainer.parentNode.insertBefore(tocContainer, navContainer);
} else {
return;
}
}
+15 -3
View File
@@ -220,9 +220,16 @@
if (!switchBtn) return;
var container = switchBtn.closest('.aksrt-video') || switchBtn.closest('[data-aksrt-video]');
// Single cover: episodes sit in .single-video-footer; player is sibling under .single-video
if (!container) {
var singleVideo = switchBtn.closest('.single-video, .single-head-cover');
if (singleVideo) {
container = singleVideo.querySelector('.aksrt-video, [data-aksrt-video]');
}
}
if (!container) return;
var src = switchBtn.dataset.src || switchBtn.getAttribute('href');
var src = switchBtn.dataset.src || switchBtn.getAttribute('video-url') || switchBtn.getAttribute('href');
if (!src) return;
e.preventDefault();
@@ -237,9 +244,14 @@
}
// 高亮当前集
var siblings = container.parentElement ? container.parentElement.querySelectorAll('.switch-video, .switch-iframe') : [];
siblings.forEach(function (btn) { btn.classList.remove('is-active'); });
var scope = switchBtn.closest('.featured-video-episode') || (container.parentElement || document);
var siblings = scope.querySelectorAll('.switch-video, .switch-iframe');
siblings.forEach(function (btn) {
btn.classList.remove('is-active');
btn.classList.remove('active');
});
switchBtn.classList.add('is-active');
switchBtn.classList.add('active');
});
// ── Public API ──
+66 -12
View File
@@ -43,8 +43,46 @@ class AKSRT_Post_Meta_Box {
public static function get_meta(int $post_id): array {
$defaults = AKSRT_Domain_Model::default_post_meta();
$saved = get_post_meta($post_id, self::META_KEY, true);
if (!is_array($saved)) $saved = [];
return array_merge($defaults, $saved);
if (!is_array($saved)) {
$saved = [];
}
$meta = array_merge($defaults, $saved);
// New route only: episodes is always a list of {url,title}.
$meta['featured_video_episode'] = self::sanitize_video_episodes($meta['featured_video_episode'] ?? []);
if (!in_array((string) ($meta['show_toc'] ?? ''), ['', '0', '1'], true)) {
$meta['show_toc'] = '';
}
return $meta;
}
/**
* Sanitize video episode list (new route: array of url/title rows).
*
* @param mixed $raw Raw form / meta value.
* @return array<int, array{url:string,title:string}>
*/
public static function sanitize_video_episodes($raw): array {
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$url = esc_url_raw((string) ($row['url'] ?? ''));
if ($url === '') {
continue;
}
$out[] = [
'url' => $url,
'title' => sanitize_text_field((string) ($row['title'] ?? '')),
];
if (count($out) >= 50) {
break;
}
}
return $out;
}
/**
@@ -147,10 +185,12 @@ class AKSRT_Post_Meta_Box {
</select>
<p>
<label>
<input type="checkbox" name="aksrt_post_meta[show_toc]" value="1" <?php checked($meta['show_toc'] === '1'); ?>>
<?php esc_html_e('显示目录树', 'aksrt'); ?>
</label>
<label for="aksrt_show_toc"><strong><?php esc_html_e('目录树', 'aksrt'); ?></strong></label>
<select name="aksrt_post_meta[show_toc]" id="aksrt_show_toc" class="widefat">
<option value="" <?php selected((string) ($meta['show_toc'] ?? ''), ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="1" <?php selected((string) ($meta['show_toc'] ?? ''), '1'); ?>><?php esc_html_e('强制开启', 'aksrt'); ?></option>
<option value="0" <?php selected((string) ($meta['show_toc'] ?? ''), '0'); ?>><?php esc_html_e('强制关闭', 'aksrt'); ?></option>
</select>
</p>
<p><strong><?php esc_html_e('单篇交互覆盖', 'aksrt'); ?></strong></p>
@@ -210,11 +250,24 @@ class AKSRT_Post_Meta_Box {
<small class="description"><?php esc_html_e('多张图片 URL 用逗号分隔,用于封面轮播。', 'aksrt'); ?></small>
<p><strong><?php esc_html_e('特色视频', 'aksrt'); ?></strong></p>
<input type="url" name="aksrt_post_meta[featured_video]" value="<?php echo esc_url($meta['featured_video']); ?>" class="widefat" placeholder="<?php esc_attr_e('视频 URL(嵌入/直链)', 'aksrt'); ?>">
<input type="url" name="aksrt_post_meta[featured_video]" value="<?php echo esc_url($meta['featured_video']); ?>" class="widefat" placeholder="<?php esc_attr_e('第 1 集视频 URL(直链)', 'aksrt'); ?>">
<p><strong><?php esc_html_e('视频集数', 'aksrt'); ?></strong></p>
<input type="number" name="aksrt_post_meta[featured_video_episode]" value="<?php echo esc_attr((string) (int) $meta['featured_video_episode']); ?>" class="widefat" min="0" max="9999" step="1">
<small class="description"><?php esc_html_e('0 表示非剧集,>0 表示第 N 集。', 'aksrt'); ?></small>
<p><strong><?php esc_html_e('视频剧集列表', 'aksrt'); ?></strong></p>
<?php
$episodes = is_array($meta['featured_video_episode'] ?? null) ? $meta['featured_video_episode'] : [];
// Always show at least 3 editable rows for new-route authoring.
$rows = max(3, count($episodes));
for ($i = 0; $i < $rows; $i++) :
$ep = is_array($episodes[$i] ?? null) ? $episodes[$i] : [];
$ep_url = (string) ($ep['url'] ?? '');
$ep_title = (string) ($ep['title'] ?? '');
?>
<p class="aksrt-episode-row">
<input type="text" name="aksrt_post_meta[featured_video_episode][<?php echo (int) $i; ?>][title]" value="<?php echo esc_attr($ep_title); ?>" class="widefat" placeholder="<?php echo esc_attr(sprintf(__('第 %d 集标题(可选)', 'aksrt'), $i + 1)); ?>" style="margin-bottom:4px;">
<input type="url" name="aksrt_post_meta[featured_video_episode][<?php echo (int) $i; ?>][url]" value="<?php echo esc_url($ep_url); ?>" class="widefat" placeholder="<?php echo esc_attr(sprintf(__('第 %d 集视频 URL', 'aksrt'), $i + 1)); ?>">
</p>
<?php endfor; ?>
<small class="description"><?php esc_html_e('新路线:每行一集(标题 + URL)。留空 URL 的行会被忽略。第 1 集可与上方「特色视频」二选一填写。', 'aksrt'); ?></small>
<hr>
<p><strong><?php esc_html_e('手动计数覆盖', 'aksrt'); ?></strong></p>
@@ -308,7 +361,8 @@ class AKSRT_Post_Meta_Box {
'custom_excerpt' => isset($raw['custom_excerpt']) ? sanitize_textarea_field($raw['custom_excerpt']) : '',
'layout_override' => isset($raw['layout_override']) ? sanitize_key($raw['layout_override']) : '',
'header_image_mode' => isset($raw['header_image_mode']) ? sanitize_key($raw['header_image_mode']) : '',
'show_toc' => !empty($raw['show_toc']) ? '1' : '',
// New route: '' follow | '1' force on | '0' force off.
'show_toc' => isset($raw['show_toc']) && in_array((string) $raw['show_toc'], ['', '0', '1'], true) ? (string) $raw['show_toc'] : '',
'show_reading_progress' => isset($raw['show_reading_progress']) ? sanitize_key((string) $raw['show_reading_progress']) : '',
'enable_lightbox' => isset($raw['enable_lightbox']) ? sanitize_key((string) $raw['enable_lightbox']) : '',
'show_related' => isset($raw['show_related']) ? sanitize_key((string) $raw['show_related']) : '',
@@ -324,7 +378,7 @@ class AKSRT_Post_Meta_Box {
'thumbnail_url' => isset($raw['thumbnail_url']) ? esc_url_raw((string) $raw['thumbnail_url']) : '',
'featured_slide' => isset($raw['featured_slide']) ? sanitize_text_field($raw['featured_slide']) : '',
'featured_video' => isset($raw['featured_video']) ? esc_url_raw((string) $raw['featured_video']) : '',
'featured_video_episode' => isset($raw['featured_video_episode']) ? max(0, (int) $raw['featured_video_episode']) : 0,
'featured_video_episode' => self::sanitize_video_episodes($raw['featured_video_episode'] ?? []),
'subtitle' => isset($raw['subtitle']) ? sanitize_text_field($raw['subtitle']) : '',
'views' => isset($raw['views']) ? max(0, (int) $raw['views']) : 0,
'likes' => isset($raw['likes']) ? max(0, (int) $raw['likes']) : 0,
+2 -1
View File
@@ -78,7 +78,8 @@ class AKSRT_Domain_Model {
'thumbnail_url' => '',
'featured_slide' => '',
'featured_video' => '',
'featured_video_episode' => 0,
// Episode list: [ ['url' => string, 'title' => string], ... ]
'featured_video_episode' => [],
'subtitle' => '',
'views' => 0,
'likes' => 0,
+53 -134
View File
@@ -4,9 +4,7 @@
*
* @package AKSRT
*/
␍
defined('ABSPATH') || exit;
␍
if (!function_exists('aksrt_comment_smilie_map')) :
/**
* 评论 grin / smilie 映射表。
@@ -50,7 +48,6 @@ if (!function_exists('aksrt_comment_smilie_map')) :
];
}
endif;
␍
if (!function_exists('aksrt_render_comment_smilie')) :
/**
* 渲染单个 grin 表情为 smilie-icon 图片。
@@ -63,11 +60,9 @@ if (!function_exists('aksrt_render_comment_smilie')) :
if (!isset($map[$key])) {
return '[g=' . $key . ']';
}
␍
$emoji = $map[$key];
$svg = '<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-size="24">' . esc_html($emoji) . '</text></svg>';
$src = 'data:image/svg+xml;charset=UTF-8,' . rawurlencode($svg);
␍
return sprintf(
'<img class="smilie-icon" src="%1$s" alt="%2$s" title="%2$s" width="30" height="30" loading="lazy" decoding="async" data-smilie-key="%3$s" />',
esc_url($src),
@@ -76,7 +71,6 @@ if (!function_exists('aksrt_render_comment_smilie')) :
);
}
endif;
␍
if (!function_exists('aksrt_render_comment_content')) :
/**
* 将评论中的 grin token 渲染为 smilie-icon,并按 post 白名单输出。
@@ -88,7 +82,6 @@ if (!function_exists('aksrt_render_comment_content')) :
$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'] = [];
@@ -104,11 +97,9 @@ if (!function_exists('aksrt_render_comment_content')) :
'decoding' => true,
'data-smilie-key' => true,
]);
␍
return wp_kses((string) $rendered, $allowed);
}
endif;
␍
if (!function_exists('aksrt_reading_minutes')) :
/**
* 估算文章阅读分钟数
@@ -134,7 +125,6 @@ function aksrt_reading_minutes($post_id = null) {
return max(1, $minutes);
}
endif;
␍
if (!function_exists('aksrt_post_word_count')) :
/**
* Approximate visible word count for single-post meta display.
@@ -147,23 +137,19 @@ if (!function_exists('aksrt_post_word_count')) :
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.
@@ -176,7 +162,6 @@ if (!function_exists('aksrt_resolve_single_post_display_settings')) :
if (!($post instanceof WP_Post)) {
return [];
}
␍
$settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_post_page_settings()
: [
@@ -218,7 +203,6 @@ if (!function_exists('aksrt_resolve_single_post_display_settings')) :
'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']);
@@ -249,12 +233,11 @@ if (!function_exists('aksrt_resolve_single_post_display_settings')) :
}
// 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';␍
}␍
␍
// show_toc new route: '' follow | '1' on | '0' off
if (array_key_exists('show_toc', $meta) && $meta['show_toc'] !== '' && $meta['show_toc'] !== null) {
$settings['toc_enabled'] = (string) $meta['show_toc'] === '1';
}
foreach ([
'show_reading_progress' => 'reading_progress',
'enable_lightbox' => 'lightbox',
@@ -268,14 +251,11 @@ if (!function_exists('aksrt_resolve_single_post_display_settings')) :
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) {
@@ -285,23 +265,26 @@ if (!function_exists('aksrt_resolve_single_post_display_settings')) :
$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).␍
// 1. Video cover — featured_video or first episode URL (new route list).
$video_url = '';
if ($video_cover_on) {
$video_url = (string) ($meta['featured_video'] ?? '');
if ($video_url === '' && is_array($meta['featured_video_episode'] ?? null)) {
foreach ((array) $meta['featured_video_episode'] as $row) {
if (!is_array($row)) { continue; }
$cand = esc_url_raw((string) ($row['url'] ?? ''));
if ($cand !== '') { $video_url = $cand; break; }
}
}
}
␍
// 2. Slide cover — featured_slide URLs and/or cover_images (attachment IDs).
$slide_images = [];
if ($slide_cover_on) {
@@ -330,7 +313,6 @@ if (!function_exists('aksrt_resolve_single_post_display_settings')) :
}
$slide_images = array_values(array_unique(array_filter($slide_images)));
}
␍
// 3. Image cover — existing logic.
if ($video_url !== '') {
$cover_type = 'video';
@@ -359,24 +341,48 @@ if (!function_exists('aksrt_resolve_single_post_display_settings')) :
}
}
}
␍
$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'] ?? '');␍
// New route: episode list only.
$episodes = [];
$raw_eps = $meta['featured_video_episode'] ?? [];
if (is_array($raw_eps)) {
foreach ($raw_eps as $row) {
if (!is_array($row)) {
continue;
}
$url = esc_url_raw((string) ($row['url'] ?? ''));
if ($url === '') {
continue;
}
$episodes[] = [
'url' => $url,
'title' => sanitize_text_field((string) ($row['title'] ?? '')),
];
}
}
if ($settings['featured_video'] === '' && !empty($episodes[0]['url'])) {
$settings['featured_video'] = (string) $episodes[0]['url'];
}
// If video cover already selected with empty url, refill from featured_video.
if (($settings['cover_type'] ?? '') === 'video' && (string) ($settings['cover_url'] ?? '') === '' && $settings['featured_video'] !== '') {
$settings['cover_url'] = $settings['featured_video'];
$settings['show_cover'] = true;
}
$settings['video_episodes'] = $episodes;
$settings['episode_count'] = count($episodes);
$settings['video_poster'] = (string) ($meta['cover_image'] ?? '');
$meta_copyright = trim((string) ($meta['copyright_text'] ?? ''));
if ($meta_copyright !== '') {
$settings['copyright_text'] = $meta_copyright;
}
␍
return $settings;
}
endif;
␍
if (!function_exists('aksrt_breadcrumbs')) :
/**
* 输出面包屑 HTML(single 对齐 Zibll `ul.breadcrumb`)。
@@ -390,15 +396,12 @@ if (!function_exists('aksrt_breadcrumbs')) :
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();
@@ -407,21 +410,23 @@ if (!function_exists('aksrt_breadcrumbs')) :
}
$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, ' </li><li> ');␍
if (is_wp_error($parents) || !is_string($parents) || $parents === '') {␍
$parents = '<a href="' . esc_url(get_category_link($category->term_id)) . '">' . esc_html($category->name) . '</a> </li><li> ';␍
// New route: hand-built ancestor chain (no get_category_parents noise).
$parts = [];
$parts[] = '<li><a href="' . esc_url(home_url('/')) . '"><i class="fa fa-map-marker"></i> ' . esc_html($home_text) . '</a></li>';
$ancestors = array_reverse(get_ancestors((int) $category->term_id, 'category'));
foreach ($ancestors as $ancestor_id) {
$ancestor = get_term((int) $ancestor_id, 'category');
if ($ancestor instanceof WP_Term && !is_wp_error($ancestor)) {
$parts[] = '<li><a href="' . esc_url(get_category_link($ancestor->term_id)) . '">' . esc_html($ancestor->name) . '</a></li>';
}
$html = '<ul class="breadcrumb">'␍
. '<li><a href="' . esc_url(home_url('/')) . '"><i class="fa fa-map-marker"></i> ' . esc_html($home_text) . '</a></li><li>'␍
. $parents . esc_html($last) . '</li></ul>';␍
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped␍
}
$parts[] = '<li><a href="' . esc_url(get_category_link($category->term_id)) . '">' . esc_html($category->name) . '</a></li>';
$parts[] = '<li>' . esc_html((string) $last) . '</li>';
echo '<ul class="breadcrumb">' . implode('', $parts) . '</ul>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
}
␍
$items = [];
$items[] = '<li class="breadcrumbs__item breadcrumbs__item--home"><a href="' . esc_url(home_url('/')) . '"><i class="fa fa-map-marker" aria-hidden="true"></i><span>' . esc_html($home_text) . '</span></a></li>';
␍
if (is_category()) {
$term = get_queried_object();
if ($term instanceof WP_Term) {
@@ -450,11 +455,9 @@ if (!function_exists('aksrt_breadcrumbs')) :
} else {
$items[] = '<li class="breadcrumbs__item is-current"><span>' . esc_html(get_the_title()) . '</span></li>';
}
␍
echo '<nav class="breadcrumbs-wrap" aria-label="' . esc_attr__('面包屑', 'aksrt') . '"><ol class="breadcrumbs breadcrumb">' . implode('<li class="breadcrumbs__sep" aria-hidden="true">/</li>', $items) . '</ol></nav>';
}
endif;
␍
if (!function_exists('aksrt_get_post_views')) :
/**
* 获取文章浏览量
@@ -465,18 +468,14 @@ if (!function_exists('aksrt_get_post_views')) :
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) {
@@ -487,7 +486,6 @@ if (! function_exists('aksrt_format_number')) :
return (string) number_format_i18n($num);
}
endif;
␍
if (!function_exists('aksrt_render_mini_post_item')) :
/**
* Render a compact sidebar post item for homepage widgets.
@@ -502,13 +500,11 @@ if (!function_exists('aksrt_render_mini_post_item')) :
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',
@@ -516,11 +512,9 @@ if (!function_exists('aksrt_render_mini_post_item')) :
'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
@@ -530,11 +524,9 @@ if (!function_exists('aksrt_render_mini_post_item')) :
$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']);
@@ -542,7 +534,6 @@ if (!function_exists('aksrt_render_mini_post_item')) :
? 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);
@@ -574,7 +565,6 @@ if (!function_exists('aksrt_render_mini_post_item')) :
</article>
<?php
}
␍
// Restore original post
if ($original_post instanceof WP_Post) {
$wp_query->post = $original_post;
@@ -584,7 +574,6 @@ if (!function_exists('aksrt_render_mini_post_item')) :
}
}
endif;
␍
if (!function_exists('aksrt_set_post_views')) :
/**
* 记录文章/资源浏览量(每浏览器 24 小时只计 1 次)。
@@ -593,7 +582,6 @@ if (!function_exists('aksrt_set_post_views')) :
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();
@@ -605,17 +593,13 @@ if (!function_exists('aksrt_set_post_views')) :
$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);
@@ -623,7 +607,6 @@ if (!function_exists('aksrt_set_post_views')) :
} 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);
}
@@ -635,7 +618,6 @@ if (!function_exists('aksrt_set_post_views')) :
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);
@@ -643,7 +625,6 @@ if (!function_exists('aksrt_set_post_views')) :
$_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();
@@ -651,7 +632,6 @@ if (!function_exists('aksrt_get_post_likes')) :
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();
@@ -659,36 +639,29 @@ if (!function_exists('aksrt_get_post_favorites')) :
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 {
@@ -696,25 +669,21 @@ if (!function_exists('aksrt_set_interaction_cookie')) :
}
}
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;
@@ -723,43 +692,35 @@ if (!function_exists('aksrt_set_user_interaction_state')) :
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 丢失。
@@ -776,13 +737,11 @@ if (!function_exists('aksrt_update_post_counter')) :
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}
@@ -795,85 +754,69 @@ if (!function_exists('aksrt_update_post_counter')) :
} 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.
@@ -888,14 +831,11 @@ if (!function_exists('aksrt_comment_action_permissions')) :
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')) {
@@ -904,7 +844,6 @@ if (!function_exists('aksrt_comment_action_permissions')) :
}
$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,
@@ -913,7 +852,6 @@ if (!function_exists('aksrt_comment_action_permissions')) :
];
}
endif;
␍
if (!function_exists('aksrt_is_comment_pinned')) :
/**
* Whether a comment is pinned ("置顶") by a moderator.
@@ -926,11 +864,9 @@ if (!function_exists('aksrt_is_comment_pinned')) :
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.
@@ -944,13 +880,11 @@ if (!function_exists('aksrt_update_comment_counter')) :
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}
@@ -963,12 +897,10 @@ if (!function_exists('aksrt_update_comment_counter')) :
} 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).
@@ -981,11 +913,9 @@ if (!function_exists('aksrt_get_comment_likes')) :
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.
@@ -998,18 +928,15 @@ if (!function_exists('aksrt_is_comment_liked')) :
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.
@@ -1022,7 +949,6 @@ if (!function_exists('aksrt_set_comment_like_state')) :
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);
@@ -1037,7 +963,6 @@ if (!function_exists('aksrt_set_comment_like_state')) :
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 : '';
@@ -1049,7 +974,6 @@ if (!function_exists('aksrt_set_comment_like_state')) :
}
}
endif;
␍
if (!function_exists('aksrt_reorder_pinned_comments')) :
/**
* Float pinned top-level comments to the front of the queried list.
@@ -1066,7 +990,6 @@ if (!function_exists('aksrt_reorder_pinned_comments')) :
if (!is_array($comments) || count($comments) < 2) {
return $comments;
}
␍
$pinned = [];
$normal = [];
foreach ($comments as $comment) {
@@ -1076,15 +999,12 @@ if (!function_exists('aksrt_reorder_pinned_comments')) :
$normal[] = $comment;
}
}
␍
if (empty($pinned)) {
return $comments;
}
␍
return array_merge($pinned, $normal);
}
endif;
␍
if (!function_exists('aksrt_comment_callback')) :
/**
* Canonical comment list item renderer.
@@ -1120,7 +1040,6 @@ if (!function_exists('aksrt_comment_callback')) :
$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' : ''));
?>
<li <?php comment_class('aksrt-comment ' . $li_classes); ?> id="comment-<?php echo esc_attr((string) $comment_id); ?>" data-comment-id="<?php echo esc_attr((string) $comment_id); ?>" data-comment-status="<?php echo esc_attr((string) $comment->comment_approved); ?>">
+6 -1
View File
@@ -22,7 +22,12 @@ if (!function_exists('aksrt_get_the_subtitle')) :
if ($post_id <= 0) {
return '';
}
$subtitle = trim((string) get_post_meta($post_id, '_subtitle', true));
// New route: subtitle lives in AKSRT_Post_Meta_Box aggregate meta.
$subtitle = '';
if (class_exists('AKSRT_Post_Meta_Box')) {
$meta = AKSRT_Post_Meta_Box::get_meta($post_id);
$subtitle = trim((string) ($meta['subtitle'] ?? ''));
}
if ($subtitle === '') {
return '';
}
+92 -66
View File
@@ -114,6 +114,7 @@ if (!function_exists('aksrt_single_build_context')) :
'video_scale' => (int) ($settings['article_video_cover_scale'] ?? 56),
'slide_images' => array_values(array_filter((array) ($settings['slide_images'] ?? []))),
'episode_count' => max(0, (int) ($settings['episode_count'] ?? 0)),
'video_episodes' => is_array($settings['video_episodes'] ?? null) ? array_values($settings['video_episodes']) : [],
'video_poster' => (string) ($settings['video_poster'] ?? ''),
'meta_items' => $meta_items,
'meta_items_secondary' => $meta_items_secondary,
@@ -199,7 +200,7 @@ if (!function_exists('aksrt_single_render_cover')) :
<div class="single-head-cover">
<div class="single-video">
<div class="graphic single-cover single-cover--video imgbox-container" style="padding-bottom:<?php echo esc_attr((string) $video_scale); ?>%;">
<video src="<?php echo esc_url($ctx['cover_url']); ?>"<?php echo $poster !== '' ? ' poster="' . esc_url($poster) . '"' : ''; ?> controls playsinline class="fit-cover single-cover__video" data-aksrt-cover-video></video>
<div class="aksrt-video single-cover__player" data-aksrt-video data-src="<?php echo esc_url($ctx['cover_url']); ?>"<?php echo $poster !== '' ? ' data-poster="' . esc_url($poster) . '"' : ''; ?> style="position:absolute;inset:0;width:100%;height:100%;"></div>
</div>
<div class="single-video-footer dplayer-featured">
<div class="flex jsb at">
@@ -211,24 +212,34 @@ if (!function_exists('aksrt_single_render_cover')) :
<?php endif; ?>
</div>
<h1 class="article-title">
<?php if ($episode > 1) : ?>
<span class="badg badg-sm b-theme mr6"><i class="fa fa-play-circle mr3" aria-hidden="true"></i><?php printf(esc_html__('共%d集', 'aksrt'), $episode); ?></span>
<?php
$episodes = is_array($ctx['video_episodes'] ?? null) ? $ctx['video_episodes'] : [];
if ($episodes === [] && !empty($ctx['cover_url'])) {
$episodes = [['url' => (string) $ctx['cover_url'], 'title' => '']];
}
$ep_count = count($episodes);
if ($ep_count > 1) : ?>
<span class="badg badg-sm b-theme mr6"><i class="fa fa-play-circle mr3" aria-hidden="true"></i><?php printf(esc_html__('共%d集', 'aksrt'), $ep_count); ?></span>
<?php else : ?>
<i class="fa fa-play-circle-o mr6 opacity8" aria-hidden="true"></i>
<?php endif; ?>
<?php echo $title_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</h1>
<?php echo $badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php if ($episode > 0) : ?>
<?php if ($ep_count > 0) : ?>
<div class="featured-video-episode mt10">
<a href="javascript:;" class="switch-video text-ellipsis active" data-index="1" video-url="<?php echo esc_url($ctx['cover_url']); ?>">
<span class="mr6 badg badg-sm">1</span><i class="episode-active-icon"></i><?php printf(esc_html__('第%d集', 'aksrt'), 1); ?>
<?php foreach ($episodes as $idx => $ep) :
$ep_url = (string) ($ep['url'] ?? '');
if ($ep_url === '') { continue; }
$ep_title = trim((string) ($ep['title'] ?? ''));
if ($ep_title === '') {
$ep_title = sprintf(__('第%d集', 'aksrt'), $idx + 1);
}
?>
<a href="javascript:;" class="switch-video text-ellipsis<?php echo $idx === 0 ? ' active' : ''; ?>" data-index="<?php echo (int) ($idx + 1); ?>" data-src="<?php echo esc_url($ep_url); ?>">
<span class="mr6 badg badg-sm"><?php echo (int) ($idx + 1); ?></span><i class="episode-active-icon"></i><?php echo esc_html($ep_title); ?>
</a>
<?php for ($i = 2; $i <= $episode; $i++) : ?>
<a href="javascript:;" class="switch-video text-ellipsis" data-index="<?php echo (int) $i; ?>" video-url="<?php echo esc_url($ctx['cover_url']); ?>">
<span class="mr6 badg badg-sm"><?php echo (int) $i; ?></span><i class="episode-active-icon"></i><?php printf(esc_html__('第%d集', 'aksrt'), $i); ?>
</a>
<?php endfor; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
@@ -264,15 +275,13 @@ if (!function_exists('aksrt_single_render_cover')) :
'show_pagination' => true,
'loop' => count($slide_items) > 1,
'autoplay' => count($slide_items) > 1,
'overlay_html' => $overlay,
]);
}
?>
<div class="single-head-cover">
<?php if ($slider_html !== '') : ?>
<div class="single-cover-slide-wrap" style="position:relative">
<?php echo $slider_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php echo $overlay; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
<?php else : ?>
<div class="graphic mb20 single-cover single-cover--slide imgbox-container" style="padding-bottom:<?php echo esc_attr((string) $img_scale); ?>%;">
<?php foreach ($slides as $idx => $slide_url) : ?>
@@ -453,50 +462,67 @@ if (!function_exists('aksrt_single_render_author_card')) :
* @param array<string,mixed> $ctx Single context.
*/
function aksrt_single_render_author_card(array $ctx): void {
if (!function_exists('aksrt_get_user_dashboard_view')) {
$author_id = (int) get_the_author_meta('ID');
if ($author_id <= 0) {
return;
}
$author_id = (int) get_the_author_meta('ID');
$dashboard = aksrt_get_user_dashboard_view($author_id, ['viewer_id' => get_current_user_id()]);
$recent = is_array($dashboard['recent_posts'] ?? null) ? $dashboard['recent_posts'] : [];
$stats = [
['label' => __('文章', 'aksrt'), 'value' => (int) ($dashboard['post_count'] ?? 0)],
['label' => __('评论', 'aksrt'), 'value' => (int) ($dashboard['comment_count'] ?? 0)],
['label' => __('收藏', 'aksrt'), 'value' => (int) ($dashboard['favorite_count'] ?? 0)],
];
$author_url = get_author_posts_url($author_id);
$author_name = get_the_author();
$bio = trim((string) get_the_author_meta('description'));
$vip_badge = function_exists('aksrt_render_vip_badge') ? aksrt_render_vip_badge($author_id, 'sm') : '';
$signals = [];
if (class_exists('AKSRT_User_Home')) {
$signals = AKSRT_User_Home::get_instance()->get_profile_signals($author_id);
}
$avatar = function_exists('aksrt_get_avatar_html')
? aksrt_get_avatar_html($author_id, 80, ['class' => 'avatar avatar-lg'])
: get_avatar($author_id, 80, '', esc_attr($author_name), ['class' => 'avatar avatar-lg']);
$recent = get_posts([
'author' => $author_id,
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 6,
'orderby' => 'date',
'order' => 'DESC',
'no_found_rows' => true,
]);
?>
<div class="author author-box zib-widget">
<div class="author-box__header">
<a href="<?php echo esc_url(get_author_posts_url($author_id)); ?>" class="author-box__avatar">
<?php echo get_avatar($author_id, 56, '', esc_attr(get_the_author())); ?>
</a>
<div class="author-box__info">
<a href="<?php echo esc_url(get_author_posts_url($author_id)); ?>" class="author-box__name"><?php echo esc_html(get_the_author()); ?></a>
<?php if ($bio !== '') : ?>
<p class="author-box__bio"><?php echo esc_html($bio); ?></p>
<?php endif; ?>
<div class="author-box__stats">
<?php foreach ($stats as $stat) : ?>
<span><strong><?php echo esc_html((string) $stat['value']); ?></strong> <?php echo esc_html($stat['label']); ?></span>
<div class="user-card author zib-widget">
<div class="card-content mt10 relative">
<div class="user-content">
<div class="user-avatar"><?php echo $avatar; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
<div class="user-info mt20 mb10">
<div class="user-name flex jc">
<a class="display-name" href="<?php echo esc_url($author_url); ?>"><?php echo esc_html($author_name); ?></a>
<?php echo $vip_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php foreach ($signals as $signal) : ?>
<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--<?php echo esc_attr((string) ($signal['tone'] ?? 'info')); ?>">
<i class="fa fa-<?php echo esc_attr((string) ($signal['icon'] ?? 'check')); ?>" aria-hidden="true"></i>
<?php echo esc_html((string) ($signal['label'] ?? '')); ?>
</span>
<?php endforeach; ?>
</div>
<?php if ($bio !== '') : ?>
<div class="user-desc mt10 muted-2-color em09"><?php echo esc_html($bio); ?></div>
<?php endif; ?>
<div class="user-btns mt20">
<a class="but pw-1em mr6 jb-blue" href="<?php echo esc_url($author_url); ?>"><?php esc_html_e('个人主页', 'aksrt'); ?></a>
</div>
</div>
</div>
<?php if (!empty($recent)) : ?>
<div class="author-box__posts">
<h3 class="author-box__posts-title"><?php esc_html_e('最新文章', 'aksrt'); ?></h3>
<ul>
<?php foreach ($recent as $recent_post) : ?>
<?php if ($recent_post instanceof WP_Post) : ?>
<li><a href="<?php echo esc_url(get_permalink($recent_post)); ?>"><?php echo esc_html(get_the_title($recent_post)); ?></a></li>
<div class="user-card-posts mt10">
<ul class="no-thumb">
<?php foreach ($recent as $rp) : ?>
<?php if ($rp instanceof WP_Post) : ?>
<li><a class="icon-circle" href="<?php echo esc_url(get_permalink($rp)); ?>"><?php echo esc_html(get_the_title($rp)); ?></a></li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
endif;
@@ -682,7 +708,7 @@ if (!function_exists('aksrt_single_render_related')) :
$args = function_exists('aksrt_single_related_query_args')
? aksrt_single_related_query_args($post_id, $settings)
: [
'post_type' => get_post_type($post_id),
'post_type' => get_post_type($post_id) ?: 'post',
'post_status' => 'publish',
'post__not_in' => [$post_id],
'posts_per_page' => max(1, min(12, (int) ($settings['related_count'] ?? 6))),
@@ -695,6 +721,26 @@ if (!function_exists('aksrt_single_render_related')) :
return;
}
$wrap_class = 'theme-box relates' . ($style === 'img' ? ' relates-thumb' : '');
?>
<div class="<?php echo esc_attr($wrap_class); ?>">
<div class="box-body notop">
<div class="title-theme"><?php echo esc_html($title); ?></div>
</div>
<div class="zib-widget">
<?php if ($style === 'list' && function_exists('aksrt_render_post_list')) : ?>
<?php
// New route: single query only.
aksrt_render_post_list($query, [
'context' => 'related',
'mode' => 'mini',
'show_thumb' => true,
'show_meta' => true,
'show_number' => false,
]);
?>
<?php else : ?>
<?php
$posts_html = '';
while ($query->have_posts()) {
$query->the_post();
@@ -708,15 +754,7 @@ if (!function_exists('aksrt_single_render_related')) :
$posts_html .= '<li><a class="icon-circle" href="' . esc_url(get_permalink($p)) . '">' . esc_html(get_the_title($p)) . '</a></li>';
}
}
wp_reset_postdata();
$wrap_class = 'theme-box relates' . ($style === 'img' ? ' relates-thumb' : '');
?>
<div class="<?php echo esc_attr($wrap_class); ?>">
<div class="box-body notop">
<div class="title-theme"><?php echo esc_html($title); ?></div>
</div>
<div class="zib-widget">
<?php if ($style === 'img') : ?>
<div class="swiper-container swiper-scroll">
<div class="swiper-wrapper">
@@ -725,28 +763,16 @@ if (!function_exists('aksrt_single_render_related')) :
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
<?php elseif ($style === 'list') : ?>
<?php
$list_query = new WP_Query($args);
if (function_exists('aksrt_render_post_list')) {
aksrt_render_post_list($list_query, [
'context' => 'related',
'mode' => 'mini',
'show_thumb' => true,
'show_meta' => true,
'show_number' => false,
]);
}
wp_reset_postdata();
?>
<?php else : ?>
<ul class="no-thumb">
<?php echo $posts_html !== '' ? $posts_html : '<li>' . esc_html__('暂无相关文章', 'aksrt') . '</li>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</ul>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php
wp_reset_postdata();
}
endif;
+21 -5
View File
@@ -65,6 +65,8 @@ function aksrt_render_slider(array $args = []): string {
'slider_class' => '',
'show_nav' => true,
'show_pagination' => true,
// Optional absolute overlay HTML (e.g. single cover title/meta). Not escaped.
'overlay_html' => '',
];
$args = wp_parse_args($args, $defaults);
@@ -90,7 +92,13 @@ function aksrt_render_slider(array $args = []): string {
// 高度样式
$height_style = '';
if ($args['scale_height']) {
$height_style = sprintf('--pc-height:auto;--m-height:auto;--scale-ratio:%s;', esc_attr((string) $args['scale_ratio']));
// padding-bottom percentage for ratio box (Zibll graphic cover style).
$ratio_pct = max(10, min(100, (float) $args['scale_ratio'] * 100));
$height_style = sprintf(
'--pc-height:auto;--m-height:auto;--scale-ratio:%s;padding-bottom:%s%%;',
esc_attr((string) $args['scale_ratio']),
esc_attr((string) $ratio_pct)
);
} else {
$height_style = sprintf('--pc-height:%s;--m-height:%s;', esc_attr($args['pc_height']), esc_attr($args['m_height']));
}
@@ -102,11 +110,13 @@ function aksrt_render_slider(array $args = []): string {
$wrapper_classes = ['aksrt-slider-wrapper'];
if ($args['wrapper_class']) $wrapper_classes[] = $args['wrapper_class'];
if ($args['scale_height']) $wrapper_classes[] = 'aksrt-slider-wrapper--scale';
if ((string) $args['overlay_html'] !== '') $wrapper_classes[] = 'aksrt-slider-wrapper--has-overlay';
$wrapper_class_str = implode(' ', $wrapper_classes);
// 渲染 HTML
$html = '<div class="' . esc_attr($wrapper_class_str) . '">';
$html .= '<div class="' . esc_attr($slider_class_str) . '" ' . implode(' ', $data_attrs) . ' style="' . esc_attr($height_style) . '">';
$html = '<div class="' . esc_attr($wrapper_class_str) . '"' . ($args['scale_height'] ? ' style="position:relative;' . esc_attr(sprintf('padding-bottom:%s%%;', (string) max(10, min(100, (float) $args['scale_ratio'] * 100)))) . '"' : '') . '>';
$html .= '<div class="' . esc_attr($slider_class_str) . '" ' . implode(' ', $data_attrs) . ' style="' . esc_attr($height_style) . ($args['scale_height'] ? 'position:absolute;inset:0;width:100%;height:100%;' : '') . '">';
$html .= '<div class="swiper-wrapper">';
foreach ($slides as $slide) {
@@ -116,7 +126,7 @@ function aksrt_render_slider(array $args = []): string {
$desc = sanitize_text_field($slide['desc'] ?? '');
$alt = sanitize_text_field($slide['alt'] ?? $title);
$bg_color = sanitize_hex_color($slide['bg_color'] ?? '');
$target = $slide['target'] === '_blank' ? '_blank' : '_self';
$target = ($slide['target'] ?? '') === '_blank' ? '_blank' : '_self';
$slide_style = $bg_color ? 'background-color:' . esc_attr($bg_color) . ';' : '';
@@ -127,7 +137,7 @@ function aksrt_render_slider(array $args = []): string {
}
if ($image) {
$html .= '<img class="swiper-lazy" data-src="' . esc_url($image) . '" alt="' . esc_attr($alt) . '" loading="lazy">';
$html .= '<img class="swiper-lazy fit-cover" data-src="' . esc_url($image) . '" src="' . esc_url($image) . '" alt="' . esc_attr($alt) . '" loading="lazy">';
$html .= '<div class="swiper-lazy-preloader"></div>';
}
@@ -158,6 +168,12 @@ function aksrt_render_slider(array $args = []): string {
}
$html .= '</div>'; // .new-swiper
// Cover title/meta overlay (trusted HTML from single cover renderer).
if ((string) $args['overlay_html'] !== '') {
$html .= '<div class="aksrt-slider-overlay">' . $args['overlay_html'] . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$html .= '</div>'; // .aksrt-slider-wrapper
return $html;