fix(single): align slide and video cover heights with image cover

The image-mode cover reserves +50px for the title/breadcrumb overlay
(calc(scale% + 50px)), but the slide cover wrapper was given a bare
inline padding-bottom by aksrt_render_slider, permanently defeating the
.cover-slide rule, and the video cover used a bare video_scale%.

- aksrt_render_slider: add scale_extra so scale mode emits
  calc(ratio% + extra); the single cover passes 50px so all three
  modes share one height formula
- drop the unreachable static slide fallback and function_exists guard
  (the helper is unconditionally loaded by inc/bootstrap.php), the
  always-true cover_url/ep_count conditions, and the dead --scale-ratio
  and inner swiper padding emitted in scale mode
- remove the dead cover CSS cluster with no producer: .single-cover-slide
  (main.css/main.min.css/cover.css), .single-cover-slider family
  (components/base.css), .single-cover-slide-wrap and .single-cover__*
  helpers (cover.css), and initSlideCovers in misc.js
- rebuild public bundles via tools/build-public-assets.mjs
- add single-cover-height-contract test covering the shared formula
  and the dead-code removals
This commit is contained in:
2026-09-05 18:22:37 +08:00
parent 05a6b6fc54
commit 4f2d17f6ee
12 changed files with 133 additions and 418 deletions
+1
View File
@@ -50,6 +50,7 @@
!/tests/widget-system/comment-image-upload-contract.test.cjs
!/tests/widget-system/pay-card-contract.test.cjs
!/tests/widget-system/circular-button-icon-margin-contract.test.cjs
!/tests/widget-system/single-cover-height-contract.test.cjs
!/tests/widget-system/frontend-lifecycle-contract.test.cjs
!/tests/asset-ledger/
!/tests/asset-ledger/*.php
-105
View File
@@ -5724,111 +5724,6 @@ body.aksrt-body-scroll .aksrt-show-on-scroll {
.aksrt-cursor-zoom-in {
cursor: zoom-in;
}
/* ============================================================
* 7. 文章封面轮播
* ============================================================ */
.single-cover-slider {
position: relative;
width: 100%;
overflow: hidden;
border-radius: var(--radius-lg, 8px);
}
.single-cover-slider .swiper-slide {
position: relative;
overflow: hidden;
}
.single-cover-slider__img {
width: 100%;
height: 400px;
object-fit: cover;
display: block;
}
.single-cover-slider__overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,0) 40%, rgba(0,0,0,0.6) 100%);
pointer-events: none;
}
.single-cover-slider__content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 24px;
color: #fff;
z-index: 2;
}
.single-cover-slider__title {
margin: 0 0 8px;
font-size: 24px;
font-weight: 700;
text-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.single-cover-slider__meta {
display: flex;
gap: 16px;
font-size: 13px;
opacity: 0.9;
}
.single-cover-slider__pagination {
position: absolute;
bottom: 12px;
right: 16px;
z-index: 3;
}
.single-cover-slider__pagination .swiper-pagination-bullet {
background: rgba(255,255,255,0.6);
opacity: 1;
}
.single-cover-slider__pagination .swiper-pagination-bullet-active {
background: #fff;
}
.single-cover-slider__prev,
.single-cover-slider__next {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 3;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: rgba(0,0,0,0.3);
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.single-cover-slider__prev:hover,
.single-cover-slider__next:hover {
background: rgba(0,0,0,0.5);
}
.single-cover-slider__prev { left: 12px; }
.single-cover-slider__next { right: 12px; }
@media (max-width: 640px) {
.single-cover-slider__img {
height: 240px;
}
.single-cover-slider__title {
font-size: 18px;
}
}
/* ============================================================
* 8. 打赏按钮
* ============================================================ */
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -1,108 +1,3 @@
/* ============================================================
* 7. 文章封面轮播
* ============================================================ */
.single-cover-slider {
position: relative;
width: 100%;
overflow: hidden;
border-radius: var(--radius-lg, 8px);
}
.single-cover-slider .swiper-slide {
position: relative;
overflow: hidden;
}
.single-cover-slider__img {
width: 100%;
height: 400px;
object-fit: cover;
display: block;
}
.single-cover-slider__overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,0) 40%, rgba(0,0,0,0.6) 100%);
pointer-events: none;
}
.single-cover-slider__content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 24px;
color: #fff;
z-index: 2;
}
.single-cover-slider__title {
margin: 0 0 8px;
font-size: 24px;
font-weight: 700;
text-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.single-cover-slider__meta {
display: flex;
gap: 16px;
font-size: 13px;
opacity: 0.9;
}
.single-cover-slider__pagination {
position: absolute;
bottom: 12px;
right: 16px;
z-index: 3;
}
.single-cover-slider__pagination .swiper-pagination-bullet {
background: rgba(255,255,255,0.6);
opacity: 1;
}
.single-cover-slider__pagination .swiper-pagination-bullet-active {
background: #fff;
}
.single-cover-slider__prev,
.single-cover-slider__next {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 3;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: rgba(0,0,0,0.3);
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.single-cover-slider__prev:hover,
.single-cover-slider__next:hover {
background: rgba(0,0,0,0.5);
}
.single-cover-slider__prev { left: 12px; }
.single-cover-slider__next { right: 12px; }
@media (max-width: 640px) {
.single-cover-slider__img {
height: 240px;
}
.single-cover-slider__title {
font-size: 18px;
}
}
/* ============================================================
* 8. 打赏按钮
* ============================================================ */
-118
View File
@@ -14,11 +14,6 @@
padding-bottom: calc(var(--single-cover-scale, 35%) + 50px);
}
.single-cover-slide {
padding-bottom: calc(var(--scale-height, var(--single-cover-scale, 35%)) + 50px);
border-radius: var(--main-radius, var(--radius, 8px));
}
.single-cover > .fit-cover,
.single-cover > img {
position: absolute;
@@ -46,118 +41,15 @@
font-weight: 700;
}
/* Legacy overlay helpers (kept for non-legacy markup) */
.single-cover__overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0) 30%,
rgba(0, 0, 0, 0.6) 100%
);
pointer-events: none;
}
.single-cover__content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 24px;
color: #fff;
border-radius: 0 0 var(--main-radius, var(--radius, 8px)) var(--main-radius, var(--radius, 8px));
}
.single-cover__title,
.single-cover__content .article-title {
font-size: clamp(1.125rem, 2.5vw, 1.5rem);
font-weight: 700;
margin: 0 0 6px;
line-height: 1.35;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
color: #fff;
}
.single-cover__meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
opacity: 0.85;
}
.single-cover__meta span {
display: inline-flex;
align-items: center;
gap: 4px;
}
/* ===== Video Cover ===== */
.single-cover--video {
background: #000;
}
.single-cover--video .single-cover__video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* ===== Slide Cover ===== */
.single-cover--slide {
background: var(--color-bg-2, #f5f5f5);
}
.single-cover__slides {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.single-cover__slide {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.6s ease;
}
.single-cover__slide.is-active {
opacity: 1;
}
.single-cover__slide img {
width: 100%;
height: 100%;
object-fit: cover;
}
.single-cover__dots {
position: absolute;
bottom: 14px;
right: 18px;
display: flex;
gap: 8px;
z-index: 2;
}
.single-cover__dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.45);
border: 1px solid rgba(255, 255, 255, 0.32);
transition: background 0.3s ease, transform 0.3s ease;
cursor: pointer;
}
.single-cover__dot.is-active {
background: #fff;
transform: scale(1.12);
}
/* ===== 面包屑 ===== */
.post-breadcrumbs {
margin-bottom: 12px;
@@ -240,16 +132,6 @@
margin-right: 3px;
opacity: 0.9;
}
.single-cover-slide-wrap {
position: relative;
overflow: hidden;
border-radius: var(--main-radius, var(--radius, 8px));
}
.single-cover-slide-wrap .abs-center.left-bottom {
z-index: 2;
padding: 15px 15px 10px 15px;
color: #fff;
}
.article-avatar name.flex {
display: inline-flex;
align-items: center;
-5
View File
@@ -8501,11 +8501,6 @@ body.modal-open .float-btn.wait-pay-btn .hover-show-con {
padding-bottom: calc(var(--single-cover-scale) + 50px);
}
.single-cover-slide {
padding-bottom: calc(var(--scale-height) + 50px);
border-radius: var(--main-radius);
}
.single-cover-con {
color: #fff;
padding: 15px 15px 10px 15px;
+1 -1
View File
File diff suppressed because one or more lines are too long
-44
View File
@@ -99,50 +99,6 @@
})();
(function () {
"use strict";
function initSlideCovers() {
var covers = document.querySelectorAll(".single-cover--slide");
covers.forEach(function (cover) {
var slides = cover.querySelectorAll(".single-cover__slide");
var dots = cover.querySelectorAll(".single-cover__dot");
if (slides.length < 2) return;
var current = 0;
var interval = 5000;
function goTo(idx) {
slides[current].classList.remove("is-active");
if (dots[current]) dots[current].classList.remove("is-active");
current = idx;
slides[current].classList.add("is-active");
if (dots[current]) dots[current].classList.add("is-active");
}
var timer = setInterval(function () {
goTo((current + 1) % slides.length);
}, interval);
dots.forEach(function (dot, i) {
dot.addEventListener("click", function () {
clearInterval(timer);
goTo(i);
timer = setInterval(function () {
goTo((current + 1) % slides.length);
}, interval);
});
});
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initSlideCovers);
} else {
initSlideCovers();
}
})();
(function () {
"use strict";
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -16
View File
@@ -31,7 +31,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); ?>%;">
<div class="graphic single-cover single-cover--video imgbox-container" style="padding-bottom:calc(<?php echo esc_attr((string) $video_scale); ?>% + 50px);">
<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">
@@ -41,7 +41,7 @@ if (!function_exists('aksrt_single_render_cover')) :
<h1 class="article-title">
<?php
$episodes = is_array($ctx['video_episodes'] ?? null) ? $ctx['video_episodes'] : [];
if ($episodes === [] && !empty($ctx['cover_url'])) {
if ($episodes === []) {
$episodes = [['url' => (string) $ctx['cover_url'], 'title' => '']];
}
$ep_count = count($episodes);
@@ -53,7 +53,6 @@ if (!function_exists('aksrt_single_render_cover')) :
<?php echo $title_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</h1>
<?php echo $badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php if ($ep_count > 0) : ?>
<div class="featured-video-episode mt10">
<?php foreach ($episodes as $idx => $ep) :
$ep_url = (string) ($ep['url'] ?? '');
@@ -68,7 +67,6 @@ if (!function_exists('aksrt_single_render_cover')) :
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
@@ -89,12 +87,12 @@ if (!function_exists('aksrt_single_render_cover')) :
. ($crumbs !== '' ? $crumbs : '')
. $badge
. '</div>';
$slider_html = '';
if (function_exists('aksrt_render_slider')) {
// 与单图封面同一高度公式:calc(比例% + 50px)。
$slider_html = aksrt_render_slider([
'slides' => $slide_items,
'scale_height' => true,
'scale_ratio' => $img_scale / 100,
'scale_extra' => '50px',
'wrapper_class' => 'mb20 single-cover-slide imgbox-container',
'slider_class' => 'single-cover single-cover--slide',
'show_nav' => true,
@@ -103,19 +101,9 @@ if (!function_exists('aksrt_single_render_cover')) :
'autoplay' => count($slide_items) > 1,
'overlay_html' => $overlay,
]);
}
?>
<div class="single-head-cover">
<?php if ($slider_html !== '') : ?>
<?php echo $slider_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php else : ?>
<div class="graphic mb20 single-cover single-cover--slide imgbox-container" style="padding-bottom:calc(<?php echo esc_attr((string) $img_scale); ?>% + 50px);">
<?php foreach ($slides as $idx => $slide_url) : ?>
<img<?php echo aksrt_get_lazy_attr('lazy_sider', (string) $slide_url, 'fit-cover', aksrt_get_lazy_thumb('lg')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> alt="" style="<?php echo $idx === 0 ? '' : 'display:none;'; ?>">
<?php endforeach; ?>
<?php echo $overlay; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
<?php endif; ?>
</div>
<?php
return;
+11 -7
View File
@@ -27,6 +27,7 @@ defined('ABSPATH') || exit;
* @type string $m_height 移动端高度 CSS 值 (默认 auto)
* @type bool $scale_height 是否使用比例高度 (默认 false)
* @type float $scale_ratio 比例值 (默认 0.45 = 45%)
* @type string $scale_extra 比例高度附加量,如 '50px'(用于封面标题预留,默认空)
* @type string $wrapper_class 额外 wrapper 类
* @type string $slider_class 额外 slider 类
* @type bool $show_nav 是否显示导航箭头 (默认 true)
@@ -61,6 +62,7 @@ function aksrt_render_slider(array $args = []): string {
'm_height' => 'auto',
'scale_height' => false,
'scale_ratio' => 0.45,
'scale_extra' => '',
'wrapper_class' => '',
'slider_class' => '',
'show_nav' => true,
@@ -91,14 +93,16 @@ function aksrt_render_slider(array $args = []): string {
// 高度样式
$height_style = '';
$scale_padding = '';
if ($args['scale_height']) {
// padding-bottom percentage for ratio box (legacy graphic cover style).
// 内层 .new-swiper 为绝对定位铺满 wrapper,高度完全由 wrapper 的 padding-bottom 决定。
$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)
);
$scale_padding = $ratio_pct . '%';
$scale_extra = trim((string) $args['scale_extra']);
if ($scale_extra !== '') {
$scale_padding = 'calc(' . $scale_padding . ' + ' . $scale_extra . ')';
}
} else {
$height_style = sprintf('--pc-height:%s;--m-height:%s;', esc_attr($args['pc_height']), esc_attr($args['m_height']));
}
@@ -115,8 +119,8 @@ function aksrt_render_slider(array $args = []): string {
$wrapper_class_str = implode(' ', $wrapper_classes);
// 渲染 HTML
$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="' . esc_attr($wrapper_class_str) . '"' . ($args['scale_height'] ? ' style="position:relative;padding-bottom:' . esc_attr($scale_padding) . ';"' : '') . '>';
$html .= '<div class="' . esc_attr($slider_class_str) . '" ' . implode(' ', $data_attrs) . ($args['scale_height'] ? ' style="position:absolute;inset:0;width:100%;height:100%;"' : ' style="' . esc_attr($height_style) . '"') . '>';
$html .= '<div class="swiper-wrapper">';
foreach ($slides as $slide) {
@@ -0,0 +1,99 @@
'use strict';
// 文章详情页顶部封面高度契约:单图 / 多图(轮播) / 视频三种模式
// 必须共用同一高度公式 padding-bottom: calc(比例% + 50px)。
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const singleRender = read('inc/helpers/aksrt-single-render.php');
const sliderHelper = read('inc/helpers/aksrt-slider.php');
test('single cover: image mode keeps the calc(scale% + 50px) height', () => {
assert.match(
singleRender,
/single-cover imgbox-container" style="padding-bottom:calc\(<\?php echo esc_attr\(\(string\) \$img_scale\); \?>% \+ 50px\);"/,
'单图封面必须输出 calc(img_scale% + 50px) 内联高度'
);
});
test('single cover: video mode aligns with the image-mode height formula', () => {
assert.match(
singleRender,
/single-cover--video imgbox-container" style="padding-bottom:calc\(<\?php echo esc_attr\(\(string\) \$video_scale\); \?>% \+ 50px\);"/,
'视频封面必须输出 calc(video_scale% + 50px) 内联高度'
);
// 不得回退到裸比例高度。
assert.doesNotMatch(
singleRender,
/padding-bottom:<\?php echo esc_attr\(\(string\) \$video_scale\); \?>%;"/,
'视频封面禁止输出不带 +50px 的裸比例高度'
);
});
test('single cover: slide mode reserves the same +50px via the slider helper', () => {
assert.match(
singleRender,
/'scale_extra'\s*=>\s*'50px'/,
'多图封面必须向 aksrt_render_slider 传入 scale_extra=50px'
);
assert.match(
sliderHelper,
/'scale_extra'\s*=>\s*''/,
'slider helper 需声明 scale_extra 默认参数'
);
// wrapper 的 padding-bottom 必须由比例 + 附加量组合而成。
assert.match(
sliderHelper,
/\$scale_padding = 'calc\(' \. \$scale_padding \. ' \+ ' \. \$scale_extra \. '\)';/,
'scale 模式必须把 scale_extra 合成 calc 表达式'
);
});
test('single cover: the dead static slide fallback is gone', () => {
assert.doesNotMatch(
singleRender,
/function_exists\('aksrt_render_slider'\)/,
'aksrt_render_slider 由 bootstrap 无条件加载,不得保留 function_exists 双路判断'
);
assert.doesNotMatch(
singleRender,
/single-cover--slide imgbox-container" style="padding-bottom/,
'多图封面只允许经由 slider helper 输出,禁止保留静态 fallback 分支'
);
});
test('slider scale mode: inner swiper carries no dead inline height styles', () => {
assert.doesNotMatch(
sliderHelper,
/--scale-ratio/,
'--scale-ratio 无任何 CSS 消费方,禁止输出'
);
const scaleMode = sliderHelper.split("if ($args['scale_height']) {")[1] || '';
assert.ok(scaleMode.includes("position:absolute;inset:0;width:100%;height:100%;"));
});
test('dead cover css is removed from every stylesheet surface', () => {
for (const file of [
'assets/css/frontend/single-post/cover.css',
'assets/css/main.css',
'assets/css/main.min.css',
'assets/css/build/public.css',
'assets/css/build/public.min.css',
'assets/css/frontend/enhancements/components/base.css',
]) {
const css = read(file);
assert.doesNotMatch(css, /\.single-cover-slide[,{ ]/, `${file} 不得再包含死的 .single-cover-slide 规则`);
assert.doesNotMatch(css, /\.single-cover-slider\b/, `${file} 不得再包含死的 .single-cover-slider 规则`);
}
assert.doesNotMatch(
read('assets/js/core/misc.js'),
/initSlideCovers/,
'misc.js 的 initSlideCovers(无标记生产方)已删除'
);
});