feat(diy-article): add dynamic swiper height and date display control
重构文章组件的swiper高度计算逻辑,改为基于实际item高度动态计算;新增日期显示的配置控制,完善样式统一和布局适配,优化空列表兜底逻辑与间距控制。
This commit is contained in:
@@ -43,7 +43,7 @@
|
||||
<uni-icons type="star" size="12" :color="metaColor" />
|
||||
<text>{{ item.dianzan_num || 0 }}</text>
|
||||
</view>
|
||||
<text class="date">{{ $util.timeStampTurnTime(item.create_time, 'date') }}</text>
|
||||
<text class="date" v-if="dateStyle.support && dateStyle.control">{{ $util.timeStampTurnTime(item.create_time, 'date') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -81,7 +81,7 @@
|
||||
<uni-icons type="star" size="12" :color="metaColor" />
|
||||
<text>{{ item.dianzan_num || 0 }}</text>
|
||||
</view>
|
||||
<text class="date">{{ $util.timeStampTurnTime(item.create_time, 'date') }}</text>
|
||||
<text class="date" v-if="dateStyle.support && dateStyle.control">{{ $util.timeStampTurnTime(item.create_time, 'date') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -188,6 +188,11 @@ export default {
|
||||
'--article-view-more-icon-size': r(vm.iconSize, 24) + 'rpx',
|
||||
// 列表项间距(rpx)
|
||||
'--article-item-gap': r(v.itemGap, 16) + 'rpx',
|
||||
// swiper 内 item 统一最小高度(rpx)= list 中最高 item 的自然高度
|
||||
// CSS 中 .auto-scroll-swiper .item { min-height: var(--article-swiper-item-min-height) }
|
||||
// 强制所有 item 高度统一,搭配 .swiper-item-content 的 padding-bottom = itemGap
|
||||
// 制造恒定视觉间距,与 swiperHeight = (maxItemHeight + itemGap) × N 公式一致
|
||||
'--article-swiper-item-min-height': this.maxItemHeight + 'rpx',
|
||||
// 分割线颜色(主要用于 style-4)
|
||||
'--article-divider-color': v.dividerColor || '#eeeeee',
|
||||
// 标题字号 4 风格独立(rpx)
|
||||
@@ -239,6 +244,9 @@ export default {
|
||||
dianzanStyle() {
|
||||
return Object.assign({ support: true, control: false }, this.value.dianzanStyle || {});
|
||||
},
|
||||
dateStyle() {
|
||||
return Object.assign({ support: true, control: true }, this.value.dateStyle || {});
|
||||
},
|
||||
tagStyle() {
|
||||
return Object.assign({ support: true, control: true, fontSize: 22 }, this.value.tagStyle || {});
|
||||
},
|
||||
@@ -272,28 +280,36 @@ export default {
|
||||
return actualCount > 0 ? Math.min(displayCount, actualCount) : 1;
|
||||
},
|
||||
// swiper 高度(rpx)优先级:
|
||||
// 1. 老/新数据显式设置了 swiperConfig.swiperHeight(>0) → 配置值
|
||||
// 2. 否则按 style 算单行高度 × 显示行数 + 行间距
|
||||
// 注:风格1 perRow=210,正好容纳:
|
||||
// 有图 item 200(图160+info 160 被 stretch+padding40)
|
||||
// 无图 item ~200(标题/摘要单行截断+space-between 上下分布+padding40)
|
||||
// swiper 整体 = 210*3+10 = 640rpx,行间紧凑,有图无图视觉高度统一。
|
||||
// 1. 老/新数据显式设置了 swiperConfig.swiperHeight(>0) → 配置值(用户强制覆盖)
|
||||
// 2. 否则按 (maxItemHeight + itemGap) × N 计算(精确,不加分摊 buffer),其中:
|
||||
// - maxItemHeight: list 中最高 item 的自然高度(动态,见 _itemHeight)
|
||||
// - itemGap: 用户配置的列表项间距(0-60rpx,默认 16)
|
||||
// - N: displayMultipleItems(每页行数)
|
||||
// 每个 swiper-item slot 高度 = (maxItemHeight + itemGap),精确等于 perRow。
|
||||
// 所有 item 通过 CSS var --article-swiper-item-min-height 强制 min-height = maxItemHeight,
|
||||
// 保证每条 item 视觉高度一致;swiper-item-content 的 padding-bottom = itemGap 制造
|
||||
// 固定的视觉间距,无论 item 实际内容多高,间距恒为 itemGap。
|
||||
// 早期版本在末尾 +10,被 swiper 平均分摊到每个 slot(10/N),导致间距 = itemGap + 10/N,
|
||||
// 且随 N 变化不可控。现已删除。
|
||||
// 3. list 尚未加载时(初始空状态)走 _staticMaxItemHeight 兜底(全字段最坏情况)。
|
||||
swiperHeight() {
|
||||
// 1. 显式设置了 swiperHeight (>0)
|
||||
// 1. 显式设置了 swiperHeight (>0) → 用户强制覆盖
|
||||
var configHeight = parseInt(this.swiperConfig.swiperHeight) || 0;
|
||||
if (configHeight > 0) return configHeight;
|
||||
// 2. 自动算:每行高度 × 显示行数 + 间距
|
||||
// 注:swiper 内 item 已去掉 margin-bottom,swiper-item 高度 = perRow
|
||||
var perRow;
|
||||
switch (this.value.style) {
|
||||
case 'style-1': perRow = 210; break; // 容纳有图200/无图200+10rpx 缓冲
|
||||
case 'style-2': perRow = 540; break; // 图320 + info 220
|
||||
case 'style-3': perRow = 300; break; // 图200 + info 100
|
||||
case 'style-4': perRow = 120; break; // 纯文字
|
||||
default: perRow = 200; break;
|
||||
// 2. (maxItemHeight + itemGap) × N,精确等于 perRow × N,不分摊 buffer
|
||||
var itemGap = this._num(this.value.itemGap, 16);
|
||||
return (this.maxItemHeight + itemGap) * this.safeDisplayMultipleItems;
|
||||
},
|
||||
// list 中最高 item 的自然高度(动态) — swiperHeight + warpWrapStyle CSS 变量都用它
|
||||
// list 为空时走 _staticMaxItemHeight 兜底
|
||||
maxItemHeight() {
|
||||
if (!this.list || this.list.length === 0) {
|
||||
return this._staticMaxItemHeight();
|
||||
}
|
||||
return perRow * this.safeDisplayMultipleItems + 10; // N 行 + 10rpx 行间距总空间(更紧凑)
|
||||
}
|
||||
return Math.max.apply(null, this.list.map(function (item) {
|
||||
return this._itemHeight(item);
|
||||
}.bind(this)));
|
||||
},
|
||||
// 注: :class 一律在模板里用 :class="['static', dynamic]" 内联数组写法
|
||||
// 不再用 listClass/itemClass computed (computed 返回 array 时,uni-app WXML 编译
|
||||
// 输出 {{[listClass]}},运行时 toString 用逗号 join,产生 "list-wrap,style-1" 错误 class)
|
||||
@@ -305,6 +321,103 @@ export default {
|
||||
var n = parseInt(v);
|
||||
return isNaN(n) ? fb : n;
|
||||
},
|
||||
// ========== 动态 perRow 估算(swiperHeight 用) ==========
|
||||
// 估算单条 item 的自然高度(rpx),考虑:
|
||||
// - style 决定图片高度 / info-wrap 宽度 / padding
|
||||
// - 是否带图(with-image: max(图片, 内容) + padding; no-image: 走 CSS min-height 240)
|
||||
// - 标题/摘要实际字数(按 1 char ≈ 1*fontSize 估算行数,clamp 1 或 2)
|
||||
// - 标签开关 + 数量(按每行 ~3 字 + padding 估算,可能 1/2 行)
|
||||
// - 阅读区(默认 1 行)
|
||||
// 返回值:该 item 的总高度(rpx)
|
||||
_itemHeight(item) {
|
||||
if (!item) return 200;
|
||||
var v = this.value || {};
|
||||
var style = v.style || 'style-1';
|
||||
var hasImg = !!item.cover_img && style !== 'style-4';
|
||||
var padding = style === 'style-3' ? 20 : 40;
|
||||
// 图片高度
|
||||
var imageH = 0;
|
||||
if (hasImg) {
|
||||
if (style === 'style-1') imageH = 160;
|
||||
else if (style === 'style-2') imageH = 320;
|
||||
else if (style === 'style-3') imageH = 200;
|
||||
}
|
||||
// info-wrap 宽度(style-1 有图时图占左侧 160+margin 20+padding 40 → 530;其余 750-padding)
|
||||
var infoW = (style === 'style-1' && hasImg) ? 530 : (750 - padding);
|
||||
// 标题(style-4 是 nowrap 单行;其余 clamp 2)
|
||||
var titleFs = this._titleFsForStyle(style);
|
||||
var titleMaxLines = style === 'style-4' ? 1 : 2;
|
||||
var titleLines = this._estimateLines(item.article_title, titleFs, infoW, titleMaxLines);
|
||||
var titleH = titleLines * titleFs * 1.4;
|
||||
// 摘要
|
||||
var abstractH = 0;
|
||||
if (v.abstractStyle && v.abstractStyle.support && v.abstractStyle.control && item.article_abstract) {
|
||||
var absFs = (v.abstractStyle && v.abstractStyle.fontSize) || 24;
|
||||
var absLines = this._estimateLines(item.article_abstract, absFs, infoW, 2);
|
||||
abstractH = absLines * absFs * 1.4;
|
||||
}
|
||||
// 标签
|
||||
var tagH = 0;
|
||||
if (v.tagStyle && v.tagStyle.support && v.tagStyle.control && item.tag_names) {
|
||||
var tags = (item.tag_names || '').split(',').filter(Boolean);
|
||||
if (tags.length > 0) {
|
||||
var tagFs = (v.tagStyle && v.tagStyle.fontSize) || 22;
|
||||
// 标签宽度估算:每标签按 3 字 + 内边距 28(14+14)+ 边框 4 = 3*tagFs + 32
|
||||
var tagW = 3 * tagFs + 32;
|
||||
var tagsPerLine = Math.max(1, Math.floor(infoW / tagW));
|
||||
var tagLines = Math.ceil(tags.length / tagsPerLine);
|
||||
// 标签自身:行高 ≈ tagFs + 2*padding 4 + margin-bottom 4 = tagFs + 12
|
||||
tagH = tagLines * (tagFs + 12);
|
||||
}
|
||||
}
|
||||
// 阅读区(默认 1 行,24rpx;不细分开关以保持公式简洁)
|
||||
var readH = 24;
|
||||
// 各元素之间的 margin-top(10rpx):有摘要 / 有标签 / 阅读区各算一个
|
||||
var margins = (abstractH > 0 ? 10 : 0) + (tagH > 0 ? 10 : 0) + 10;
|
||||
var contentH = titleH + abstractH + tagH + readH + margins;
|
||||
// 拼装 item 高度
|
||||
if (style === 'style-1') {
|
||||
if (hasImg) {
|
||||
// 图左文右:item 高度 = max(图, 内容) + padding
|
||||
return Math.max(imageH, contentH) + padding;
|
||||
} else {
|
||||
// 无图:CSS .item.no-image .info-wrap { min-height: 240rpx }
|
||||
return Math.max(240, contentH) + padding;
|
||||
}
|
||||
}
|
||||
// style-2/3:上图下文,item 高度 = 图 + 内容 + padding
|
||||
return imageH + contentH + padding;
|
||||
},
|
||||
// 估算 text 在指定字号 + 容器宽度下占几行
|
||||
// 启发式:1 char ≈ 1 * fontSize 宽度(中文为主场景,英文场景略多行但不溢出)
|
||||
// 返回值范围:0 到 maxLines
|
||||
_estimateLines(text, fontSize, containerWidth, maxLines) {
|
||||
if (!text) return 0;
|
||||
var chars = String(text).length;
|
||||
var charsPerLine = Math.max(1, Math.floor(containerWidth / fontSize));
|
||||
return Math.min(maxLines, Math.max(1, Math.ceil(chars / charsPerLine)));
|
||||
},
|
||||
// 标题字号(4 风格独立)
|
||||
_titleFsForStyle(style) {
|
||||
var v = this.value || {};
|
||||
if (style === 'style-1') return v.titleFontSize1 || 30;
|
||||
if (style === 'style-2') return v.titleFontSize2 || 30;
|
||||
if (style === 'style-3') return v.titleFontSize3 || 24;
|
||||
if (style === 'style-4') return v.titleFontSize4 || 28;
|
||||
return 30;
|
||||
},
|
||||
// list 为空时的兜底 maxItemHeight(全字段最坏情况,与 swiperHeight 公式配合:
|
||||
// swiperHeight = (maxItemHeight + itemGap) × N)
|
||||
_staticMaxItemHeight() {
|
||||
var style = this.value.style;
|
||||
switch (style) {
|
||||
case 'style-1': return 280; // 有图273/无图280
|
||||
case 'style-2': return 584; // 图320+info 224+padding40
|
||||
case 'style-3': return 316; // 图200+info 96+padding20
|
||||
case 'style-4': return 104; // 标题28+read24+5margin+40padding+8buffer
|
||||
default: return 280;
|
||||
}
|
||||
},
|
||||
// 把逗号分隔的 tag_names 转为数组(过滤空)
|
||||
tagArr(str) {
|
||||
return (str || '').split(',').filter(Boolean);
|
||||
@@ -406,16 +519,33 @@ export default {
|
||||
.auto-scroll-swiper {
|
||||
width: 100%;
|
||||
|
||||
// swiper-item-content 占满 swiper-item,不另加 padding(避免与 item padding 叠加导致裁切)
|
||||
// swiper-item-content:padding-bottom = itemGap 是制造 item 间恒定视觉间距的关键
|
||||
// 不管 item 自然高度多少,两条 item 之间的空白永远是 itemGap,
|
||||
// 而非「perRow - itemHeight」(后者随 item 高度变,间距不可控)。
|
||||
// 顶部不补 padding,避免与 item padding 叠加导致裁切。
|
||||
// 兜底:overflow:hidden 处理内容意外超过 maxItemHeight 的边界场景(防破坏布局)
|
||||
.swiper-item-content {
|
||||
padding: 0;
|
||||
padding-top: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: var(--article-item-gap, 16rpx);
|
||||
padding-left: 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// swiper 内 item 去掉 margin-bottom,避免被 swiper-item 高度裁切;
|
||||
// 行间距由 swiperHeight 多余空间制造(perRow × 3 + 30)
|
||||
// swiper 内 item:
|
||||
// 1. margin-bottom: 0 — 间距由 .swiper-item-content 的 padding-bottom 承担,避免双重 margin
|
||||
// 2. min-height = maxItemHeight(动态 CSS 变量)— 强制所有 item 视觉高度统一,短 item 自然撑高
|
||||
// 3. align-items: flex-start — 覆盖默认 stretch,避免 info-wrap 被拉伸导致短内容被 space-between 拉开
|
||||
.item {
|
||||
margin-bottom: 0 !important;
|
||||
min-height: var(--article-swiper-item-min-height, 200rpx);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
// style-1 有图时,图片在 min-height item 内垂直居中(避免贴顶导致下方空一大块)
|
||||
.item .article-img {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,6 +567,14 @@ export default {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
// 摘要(.desc)行高兜底:必须显式 1.4,防止被 main.scss 全局 view { line-height: 1.8 } 撑高。
|
||||
// style-1/2 的 .desc 已显式声明,此规则覆盖 style-3/4 等未单独定义 .desc 的风格。
|
||||
// 重要性:与 _itemHeight 公式保持一致(摘要行高 = 1.4),否则有摘要 item 实际高度超出 maxItemHeight,
|
||||
// 撑破 min-height 布局,导致有/无摘要的 item 视觉高度不一 → 间距看起来不一致。
|
||||
.desc {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
margin-top: 10rpx;
|
||||
line-height: 1;
|
||||
@@ -562,6 +700,11 @@ export default {
|
||||
color: var(--article-abstract-color, #888888);
|
||||
/* 摘要字号由 CSS 变量驱动 */
|
||||
font-size: var(--article-abstract-font-size, 24rpx);
|
||||
/* line-height 必须显式声明:全局 main.scss 的 view { line-height: 1.8 } 会让 .desc 撑高,
|
||||
与 _itemHeight 公式用的 1.4 不一致,导致有摘要 item 实际高度超过 min-height(maxItemHeight),
|
||||
而无摘要 item 被 min-height 撑高,两者视觉高度不一致 → 视觉间距不一致。
|
||||
显式 1.4 与 .title 对齐,确保实际渲染 = 估算值。*/
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
@@ -571,22 +714,17 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 无图紧凑样式:让 item 高度跟有图一致 (~200rpx) ==========
|
||||
// 背景:无图时 info-wrap 无 image 撑高,内容自然展开 ~232rpx + padding 40 = 272rpx,
|
||||
// 远超有图时的 200rpx,导致 swiper-item 内行间空白巨大或被裁。
|
||||
// 解决:无图时 info-wrap 显式 min-height: 160rpx(与有图时 image 撑高一致),
|
||||
// 标题/摘要单行截断 + 去掉多余 margin,内容 ~124rpx 在 160rpx 容器内用
|
||||
// space-between 上下分布(标题顶/摘要中/阅读底),与有图场景视觉高度统一。
|
||||
// ========== 无图 item 样式:与有图视觉等高 (~280rpx) ==========
|
||||
// 背景:开启全字段(标题+图+摘要+标签+阅读区含日期)后,有图 info 自然 233rpx +
|
||||
// padding 40 = 273rpx;无图无 image 撑高,info 自然 ~233rpx,需 min-height 拉齐
|
||||
// 到 240rpx,再加 padding 40 = 280rpx 与有图一致。
|
||||
// 早期版本曾用 clamp:1 + margin:0 把无图压到 160rpx(匹配 image 高度),现 perRow
|
||||
// 提到 280 后空间足够,放开 clamp:1 让无图也能展示完整标题/摘要,跟用户「内容
|
||||
// 全显示」诉求一致;tag-list / read-wrap 的 margin 仍清零,避免无图时因
|
||||
// space-between 分布与有图节奏不同。
|
||||
.item.no-image {
|
||||
.info-wrap {
|
||||
min-height: 160rpx;
|
||||
.title {
|
||||
-webkit-line-clamp: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.desc {
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
min-height: 240rpx;
|
||||
.tag-list {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -635,6 +773,9 @@ export default {
|
||||
color: var(--article-abstract-color, #888888);
|
||||
/* 摘要字号由 CSS 变量驱动 */
|
||||
font-size: var(--article-abstract-font-size, 24rpx);
|
||||
/* line-height 必须显式 1.4:与 .title 对齐,避免被 main.scss 全局 view { line-height: 1.8 } 撑高,
|
||||
导致有摘要 item 实际高度超过 _itemHeight 估算值,撑破 min-height 布局,视觉间距不一致。*/
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
|
||||
Reference in New Issue
Block a user