chore(视频号组件): 更新视频号样式,控制标题行数,播放按钮等样式

This commit is contained in:
2026-01-10 17:03:41 +08:00
parent f87c7d963e
commit 13166132c7
3 changed files with 467 additions and 92 deletions

View File

@@ -71,13 +71,13 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
width: var(--channel-play-btn-size); width: var(--channel-play-btn-size);
height: var(--channel-play-btn-size); height: var(--channel-play-btn-size);
background-color: var(--channel-play-btn-bg); background-color: var(--channel-play-btn-bg);
border-radius: 50%; border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.play-icon { .play-icon {
width: var(--channel-play-btn-icon-size); width: var(--channel-play-btn-icon-size);

View File

@@ -1,14 +1,17 @@
<template> <template>
<view :style="[componentStyle, { '--row-count': value.rowCount }]"> <view :style="[componentStyle, { '--row-count': value.rowCount }]">
<!-- 固定布局模式 --> <!-- 固定布局模式 -->
<view v-if="value.showStyle == 'fixed'" :class="['channel-list', 'row1-of' + value.rowCount]" <view v-if="value.showStyle == 'fixed'" :class="['channel-list', 'row1-of' + value.rowCount]">
style="padding:20rpx;">
<view v-for="(item, index) in value.list" :key="index" class="channel-item"> <view v-for="(item, index) in value.list" :key="index" class="channel-item">
<diy-channel-video <diy-channel-video
:value="item" :value="item"
@video-play="playVideo(item)" @video-play="playVideo(item)"
:list-mode="true" :list-mode="true"
:video-height="value.rowCount === 3 ? 180 : 240" :video-height="value.rowCount === 3 ? 180 : 240"
:title-line-clamp="value.titleLineClamp"
:show-play-btn="value.showPlayBtn"
:cover-style="value.coverStyle"
:play-btn-style="value.playBtnStyle"
/> />
</view> </view>
</view> </view>
@@ -23,6 +26,10 @@
@video-play="playVideo(item)" @video-play="playVideo(item)"
:list-mode="true" :list-mode="true"
:video-height="value.rowCount === 3 ? 180 : 240" :video-height="value.rowCount === 3 ? 180 : 240"
:title-line-clamp="value.titleLineClamp"
:show-play-btn="value.showPlayBtn"
:cover-style="value.coverStyle"
:play-btn-style="value.playBtnStyle"
/> />
</view> </view>
</view> </view>
@@ -34,9 +41,32 @@
import DiyMinx from './minx.js' import DiyMinx from './minx.js'
import { wechatChannelUtil, wechatChannelConfig } from './js/wechat-channel.js' import { wechatChannelUtil, wechatChannelConfig } from './js/wechat-channel.js'
/**
* 微信视频号列表组件
* 支持多种布局模式,包括固定布局和滚动布局
* 可配置列数、视频高度、标题行数等属性
*/
export default { export default {
name: 'diy-channel-list', name: 'diy-channel-list',
props: { props: {
/**
* 组件配置数据
* @type {Object}
* @default () => ({})
* @property {string} showStyle - 显示样式可选值fixed, singleSlide
* @property {number} rowCount - 每行显示的视频数量
* @property {Array} list - 视频列表数据
* @property {string} componentBgColor - 组件背景色
* @property {string} componentAngle - 组件圆角类型
* @property {number} topAroundRadius - 顶部圆角半径
* @property {number} bottomAroundRadius - 底部圆角半径
* @property {Object} ornament - 装饰效果配置
* @property {number} titleLineClamp - 标题显示行数
* @property {boolean} showPlayBtn - 是否显示播放按钮
* @property {Object} coverStyle - 视频封面图样式
* @property {Object} playBtnStyle - 播放按钮样式
*/
value: { value: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
@@ -59,6 +89,11 @@ export default {
} }
}, },
computed: { computed: {
/**
* 组件样式
* 根据配置动态生成样式字符串
* @returns {string}
*/
componentStyle() { componentStyle() {
let style = ''; let style = '';
if (this.value?.componentBgColor) { if (this.value?.componentBgColor) {
@@ -78,6 +113,11 @@ export default {
return style; return style;
}, },
/**
* 轮播高度
* 根据模式和配置计算轮播高度
* @returns {string}
*/
swiperHeight() { swiperHeight() {
let height = 0; let height = 0;
@@ -92,13 +132,22 @@ export default {
return 'height:' + (2 * height) + 'rpx'; return 'height:' + (2 * height) + 'rpx';
}, },
/**
* 是否显示指示器
* 根据轮播配置和列表长度判断是否显示指示器
* @returns {boolean}
*/
isIndicatorDots() { isIndicatorDots() {
return this.value?.carousel?.type != 'hide' && return this.value?.carousel?.type != 'hide' &&
1 != Math.ceil(this.value?.list?.length / (this.value?.pageCount * this.value?.rowCount)); 1 != Math.ceil(this.value?.list?.length / (this.value?.pageCount * this.value?.rowCount));
} }
}, },
methods: { methods: {
// 播放视频 /**
* 播放视频
* 触发 video-play 事件,并在微信小程序中调用视频播放接口
* @param {Object} item - 视频数据对象
*/
async playVideo(item) { async playVideo(item) {
await this.__$emitEvent({ await this.__$emitEvent({
eventName: 'video-play', data: item, promiseCallback: async (event, handler, awaitedResult) => { eventName: 'video-play', data: item, promiseCallback: async (event, handler, awaitedResult) => {
@@ -114,7 +163,11 @@ export default {
}) })
}, },
// 图片加载错误处理 /**
* 图片加载错误处理
* 当图片加载失败时,设置默认图片
* @param {number} index - 图片索引
*/
imgError(index) { imgError(index) {
// 图片加载失败的处理逻辑 // 图片加载失败的处理逻辑
console.log('图片加载失败:', index); console.log('图片加载失败:', index);
@@ -134,51 +187,65 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import './css/common-channel.scss'; @import './css/common-channel.scss';
// 优化列表布局样式 /**
* 列表布局样式
* 支持 2 列和 3 列布局
*/
.channel-list { .channel-list {
&.row1-of3 {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16rpx; gap: 16rpx;
padding: 16rpx;
box-sizing: border-box;
.channel-item { .channel-item {
flex: 0 0 calc(33.3333333% - 10rpx); flex: 0 0 calc(33.3333333% - 10rpx);
box-sizing: border-box; box-sizing: border-box;
&:nth-child(3n) {
margin-right: 0;
} }
} }
// 2 列布局
&.row1-of2 { &.row1-of2 {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.channel-item { .channel-item {
flex: 0 0 calc(50% - 8rpx); flex: 0 0 calc(50% - 8rpx);
box-sizing: border-box;
&:nth-child(3n) {
margin-right: 16rpx;
}
&:nth-child(2n) {
margin-right: 0;
}
} }
} }
} }
/**
* 导航布局样式
* 支持固定布局和滚动布局
*/
.channel-nav { .channel-nav {
padding: 16rpx; padding: 16rpx;
box-sizing: border-box; box-sizing: border-box;
&.fixed-layout {
.uni-scroll-view-content { .uni-scroll-view-content {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16rpx; gap: 16rpx;
} }
}
// 单滑动模式
&.singleSlide { &.singleSlide {
.uni-scroll-view-content { .uni-scroll-view-content {
display: flex; flex-wrap: nowrap;
gap: 16rpx;
} }
.channel-nav-item { .channel-nav-item {
flex-shrink: 0; flex-shrink: 0;
width: 280rpx;
} }
} }
@@ -186,35 +253,79 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
width: calc(100% / var(--row-count, 3) - 12rpx); flex: 0 0 calc(33.3333333% - 10rpx);
&:nth-child(3n) {
margin-right: 0;
}
}
// 2 列布局
&.row1-of2 {
.channel-nav-item {
flex: 0 0 calc(50% - 8rpx);
&:nth-child(3n) {
margin-right: 16rpx;
}
&:nth-child(2n) {
margin-right: 0;
}
}
} }
} }
// 响应式调整 /**
* 确保所有视频卡片高度一致
*/
.channel-item, .channel-nav-item {
display: flex;
flex-direction: column;
}
/**
* 响应式调整
* 在小屏幕设备上调整布局和间距
*/
@media (max-width: 375px) { @media (max-width: 375px) {
.channel-list { .channel-list,
&.row1-of3 {
gap: 12rpx;
.channel-item {
flex: 0 0 calc(33.3333333% - 8rpx);
}
}
&.row1-of2 {
gap: 12rpx;
.channel-item {
flex: 0 0 calc(50% - 6rpx);
}
}
}
.channel-nav { .channel-nav {
gap: 12rpx;
padding: 12rpx; padding: 12rpx;
.channel-item,
.channel-nav-item { .channel-nav-item {
width: calc(100% / var(--row-count, 3) - 8rpx); flex: 0 0 calc(33.3333333% - 8rpx);
&:nth-child(3n) {
margin-right: 0;
}
}
// 小屏幕上的 2 列布局
&.row1-of2 {
.channel-item,
.channel-nav-item {
flex: 0 0 calc(50% - 6rpx);
&:nth-child(3n) {
margin-right: 12rpx;
}
&:nth-child(2n) {
margin-right: 0;
}
}
}
}
// 小屏幕上的单滑动模式
.channel-nav {
&.singleSlide {
.channel-nav-item {
width: 240rpx;
}
} }
} }
} }

View File

@@ -14,17 +14,20 @@
<!-- 跳转式视频播放 --> <!-- 跳转式视频播放 -->
<view v-else @tap="playVideo" class="video-container"> <view v-else @tap="playVideo" class="video-container">
<view class="video-cover-wrap"> <view class="video-cover-wrap" :style="coverStyle">
<image class="video-cover" :src="$util.img(value.coverUrl)" mode="aspectFill"></image> <image class="video-cover" :src="$util.img(value.coverUrl)" mode="aspectFill"></image>
<view class="channel-play-btn"> <view class="channel-play-btn" v-if="showPlayBtn" :style="playBtnStyle">
<image class="play-icon" v-if="playIcon" :src="$util.img(playIcon)" mode="aspectFill"></image> <image class="play-icon" v-if="playIcon" :src="$util.img(playIcon)" mode="aspectFill"></image>
<view class="play-icon-css" v-else></view>
</view> </view>
<view class="video-stats-overlay" v-if="value.showViewCount"> <view class="video-stats-overlay" v-if="value.showViewCount">
{{ value.viewCount }}次观看 {{ value.viewCount }}次观看
</view> </view>
</view> </view>
<view class="video-info"> <view class="video-info">
<view class="video-title">{{ value.videoTitle }}</view> <!-- 视频标题支持多行显示控制 -->
<view class="video-title" :style="{ '--title-line-clamp': titleLineClamp }">{{ value.videoTitle }}</view>
<!-- 视频统计信息非列表模式下显示 -->
<view class="video-stats" v-if="value.showViewCount && !listMode">{{ value.viewCount }}次观看</view> <view class="video-stats" v-if="value.showViewCount && !listMode">{{ value.viewCount }}次观看</view>
</view> </view>
</view> </view>
@@ -35,35 +38,114 @@
import { wechatChannelUtil, wechatChannelConfig } from './js/wechat-channel.js' import { wechatChannelUtil, wechatChannelConfig } from './js/wechat-channel.js'
/**
* 微信视频号视频卡片组件
* 支持嵌入式播放和跳转式播放两种模式
* 可配置列表模式、视频高度、标题行数等属性
*/
export default { export default {
name: 'diy-channel-video', name: 'diy-channel-video',
props: { props: {
/**
* 视频数据对象
* @type {Object}
* @required
* @property {string} feedId - 视频 feedId
* @property {string} finderUserName - 视频号用户名
* @property {string} feedToken - 视频 token
* @property {string} coverUrl - 视频封面图
* @property {string} videoTitle - 视频标题
* @property {number} viewCount - 观看次数
* @property {boolean} showViewCount - 是否显示观看次数
* @property {boolean} embedMode - 是否启用嵌入式播放
*/
value: { value: {
type: Object, type: Object,
required: true required: true
}, },
/**
* 是否为列表模式
* @type {boolean}
* @default false
*/
listMode: { listMode: {
type: Boolean, type: Boolean,
default: false default: false
}, },
/**
* 视频高度(仅适用于嵌入式播放)
* @type {number}
* @default 220
*/
videoHeight: { videoHeight: {
type: Number, type: Number,
default: 220 default: 220
} },
/**
* 标题显示行数
* @type {number}
* @default 1
*/
titleLineClamp: {
type: Number,
default: 1
},
/** 是否显示播放按钮 */
showPlayBtn: {
type: Boolean,
default: true
},
/**
* 视频封面图样式
* 采用 16:9 比例的响应式高度
*/
coverStyle: {
type: Object,
default: () => ({
width: '100%',
height: '0',
paddingTop: '56.25%' // 16:9 比例
})
},
/**
* 播放按钮样式
*/
playBtnStyle: {
type: Object,
default: () => ({
width: '60rpx',
height: '60rpx',
borderRadius: '30rpx',
backgroundColor: 'rgba(0, 0, 0, 0.6)'
})
},
}, },
computed: { computed: {
/**
* 是否支持嵌入式播放
* @returns {boolean}
*/
canUseEmbedMode() { canUseEmbedMode() {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
return this.value?.embedMode && wechatChannelUtil.isEmbedModeSupported(); return this.value?.embedMode && wechatChannelUtil.isEmbedModeSupported();
// #endif // #endif
return false return false
}, },
/**
* 播放按钮图标
* @returns {string}
*/
playIcon() { playIcon() {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
return wechatChannelConfig.icon.playIcon return wechatChannelConfig.icon.playIcon
// #endif // #endif
return '' return ''
}, },
/**
* 嵌入式视频样式
* @returns {Object}
*/
embedVideoStyle() { embedVideoStyle() {
return { return {
width: '100%', width: '100%',
@@ -72,6 +154,10 @@ export default {
} }
}, },
methods: { methods: {
/**
* 播放视频
* 触发 video-play 事件,由父组件处理具体播放逻辑
*/
async playVideo() { async playVideo() {
this.$emit('video-play', this.value); this.$emit('video-play', this.value);
} }
@@ -82,114 +168,292 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
@import './css/common-channel.scss'; @import './css/common-channel.scss';
/**
* 视频卡片容器样式
* 包含卡片基础样式、悬停效果和列表模式样式
*/
.channel-video { .channel-video {
position: relative; position: relative;
background-color: #fff; background-color: #fff;
border-radius: 12rpx; border-radius: 12rpx;
overflow: hidden; overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
height: 100%;
// 悬停效果
&:hover { &:hover {
transform: translateY(-2rpx); transform: translateY(-4rpx);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12); box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
} }
// 列表模式样式调整
&.list-mode { &.list-mode {
border-radius: 8rpx; border-radius: 10rpx;
box-shadow: 0 1rpx 8rpx rgba(0, 0, 0, 0.06); box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.08);
.video-info { .video-info {
padding: 12rpx; padding: 14rpx;
.video-title { .video-title {
font-size: 24rpx; font-size: 24rpx;
margin-bottom: 4rpx; margin-bottom: 6rpx;
} }
} }
} }
} }
/**
* 视频容器样式
* 用于跳转式播放模式
*/
.video-container { .video-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
} }
/**
* 视频封面容器
* 采用 16:9 比例的响应式高度
*/
.video-cover-wrap { .video-cover-wrap {
position: relative; position: relative;
width: 100%; width: 100%;
height: 320rpx; padding-top: 56.25%; /* 16:9 比例 */
height: 0;
overflow: hidden;
border-radius: 12rpx 12rpx 0 0;
transition: transform 0.3s ease;
// 悬停时缩放效果
.channel-video:hover & {
transform: scale(1.02);
}
// 列表模式下的边框圆角调整
.channel-video.list-mode & { .channel-video.list-mode & {
height: 180rpx; border-radius: 10rpx 10rpx 0 0;
} }
} }
/**
* 视频封面图片
* 绝对定位填充整个容器
*/
.video-cover { .video-cover {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
border-radius: 12rpx 12rpx 0 0; transition: transform 0.3s ease;
.channel-video.list-mode & {
border-radius: 8rpx 8rpx 0 0;
}
} }
/**
* 视频统计信息遮罩
* 显示在视频封面底部
*/
.video-stats-overlay { .video-stats-overlay {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.6)); background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
padding: 8rpx 12rpx; padding: 12rpx 16rpx;
color: #fff; color: #fff;
font-size: 20rpx; font-size: 20rpx;
text-align: right; text-align: right;
font-weight: 500;
transition: opacity 0.3s ease;
// 悬停时的透明度变化
.channel-video:hover & {
opacity: 1;
}
.channel-video:not(:hover) & {
opacity: 0.8;
}
} }
/**
* 视频信息区域
* 包含标题和统计信息
*/
.video-info { .video-info {
padding: 16rpx; padding: 16rpx;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
} background-color: #fff;
transition: background-color 0.3s ease;
.video-title { // 悬停时的背景色变化
font-size: 28rpx; .channel-video:hover & {
font-weight: 500; background-color: #fafafa;
color: #333;
margin-bottom: 8rpx;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
.channel-video.list-mode & {
-webkit-line-clamp: 2;
font-size: 24rpx;
margin-bottom: 4rpx;
} }
} }
/**
* 视频标题
* 支持多行显示控制和渐变遮罩效果
*/
.video-title {
font-size: 28rpx;
font-weight: 600;
color: #222;
margin-bottom: 10rpx;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: var(--title-line-clamp, 2);
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.3s ease;
position: relative;
word-break: break-word;
// 悬停时的颜色变化
.channel-video:hover & {
color: #000;
}
// 列表模式下的样式调整
.channel-video.list-mode & {
-webkit-line-clamp: var(--title-line-clamp, 1);
font-size: 24rpx;
margin-bottom: 6rpx;
font-weight: 500;
color: #333;
line-height: 1.3;
}
// 添加渐变遮罩效果,让过长的文字有柔和的结束
&::after {
content: '';
position: absolute;
bottom: 0;
right: 0;
width: 40rpx;
height: 1.4em;
background: linear-gradient(to right, transparent, #fff 90%);
pointer-events: none;
}
// 列表模式下的遮罩高度调整
.channel-video.list-mode &::after {
height: 1.3em;
}
// 根据行数调整遮罩高度
&[style*="--title-line-clamp: 1"]::after {
height: 1.4em;
}
&[style*="--title-line-clamp: 3"]::after {
height: 4.2em;
}
&[style*="--title-line-clamp: 4"]::after {
height: 5.6em;
}
// 列表模式下的多行遮罩高度调整
.channel-video.list-mode &[style*="--title-line-clamp: 1"]::after {
height: 1.3em;
}
.channel-video.list-mode &[style*="--title-line-clamp: 3"]::after {
height: 3.9em;
}
.channel-video.list-mode &[style*="--title-line-clamp: 4"]::after {
height: 5.2em;
}
}
/**
* 视频统计信息
* 显示观看次数等数据
*/
.video-stats { .video-stats {
font-size: 22rpx; font-size: 22rpx;
color: #999; color: #999;
margin-top: 4rpx; margin-top: 6rpx;
transition: color 0.3s ease;
// 悬停时的颜色变化
.channel-video:hover & {
color: #666;
}
} }
// 确保播放按钮在列表模式下更小 /**
* 列表模式下的播放按钮样式
* 更小的尺寸和悬停效果
*/
.channel-video.list-mode .channel-play-btn { .channel-video.list-mode .channel-play-btn {
width: 50rpx; width: 50rpx;
height: 50rpx; height: 50rpx;
background-color: rgba(0, 0, 0, 0.5);
transition: all 0.3s ease;
// 悬停效果
&:hover {
background-color: rgba(0, 0, 0, 0.7);
transform: scale(1.1);
}
// 播放图标尺寸
.play-icon { .play-icon {
width: 25rpx; width: 25rpx;
height: 25rpx; height: 25rpx;
} }
} }
/**
* 通用播放按钮样式优化
* 添加悬停效果
*/
.channel-play-btn {
transition: all 0.3s ease;
// 悬停效果
&:hover {
background-color: rgba(0, 0, 0, 0.6);
transform: scale(1.05);
}
}
/**
* 纯 CSS 播放按钮图标
* 当没有 playIcon 时使用
*/
.play-icon-css {
width: 0;
height: 0;
border-top: 12rpx solid transparent;
border-bottom: 12rpx solid transparent;
border-left: 18rpx solid #fff;
margin-left: 4rpx;
transition: transform 0.3s ease;
// 悬停时的缩放效果
.channel-play-btn:hover & {
transform: scale(1.1);
}
// 列表模式下的尺寸调整
.channel-video.list-mode & {
border-top: 10rpx solid transparent;
border-bottom: 10rpx solid transparent;
border-left: 15rpx solid #fff;
margin-left: 3rpx;
}
}
</style> </style>