新增页面播放逻辑

master
于肖磊 2025-11-26 16:50:58 +08:00
parent ed96b0158b
commit b51cb05497
4 changed files with 719 additions and 23 deletions

View File

@ -1334,6 +1334,9 @@
.pa-10 {
padding: 20rpx;
}
.pa-14 {
padding: 28rpx;
}
.pa-5 {
padding: 10rpx;
}

View File

@ -0,0 +1,707 @@
<template>
<view v-if="!is_loading" class="flex-row align-c jc-c pa-20 box-border-box bg-f9 oh" :style="good_style">
<block v-if="good_list.length > 0">
<view class="goods-header-fixed" :style="'width:' + windowWidth + 'px;'">
<view class="flex-row align-c jc-sb pa-10">
<text class="flex-1 size-12 cr-b mr-10">{{ isGoodsPopup ? '' : '以下商品开播后自动添加到小黄车' }}</text>
<view class="flex-row align-c">
<button v-if="!isGoodsPopup" type="primary" plain class="btn-block cr-main flex-row align-c jc-c pa-5" style="width: 150rpx;height:68rpx;border-radius: 10rpx;"><text class="size-14 cr-main"></text></button>
<button type="primary" class="btn-block cr-main ml-10 flex-row align-c jc-c pa-5" style="width: 130rpx;height:68rpx;border-radius: 10rpx;border: 2rpx solid #0079ff;"><text class="size-14 cr-f">添加</text></button>
</view>
</view>
</view>
<!-- nvue 使用 list进行列表渲染 -->
<!-- #ifdef APP-NVUE -->
<list class="scroll-type" :style="good_style + (isGoodsPopup ? 'padding-bottom: 20rpx;' : '' )" :show-scrollbar="false">
<cell v-for="(item, index) in good_list" :key="item.id">
<!-- #endif -->
<!-- scroll-view 只有非nvue的页面使用 -->
<!-- #ifndef APP-NVUE -->
<scroll-view scroll-y class="scroll-type" :style="good_style + (isGoodsPopup ? 'padding-bottom: 20rpx;' : '' )" :show-scrollbar="false">
<view v-for="(item, index) in good_list" :key="item.id">
<!-- #endif -->
<view :class="'goods-item flex-row align-c' + (item.id == explanation_id ? ' bg-red-light' : '')" :style="'width:' + (windowWidth - 20) + 'px;'" :data-index="index" :data-value="item.checkbox">
<view class="flex-1">
<view class="flex-row align-c">
<view class="re goods-item-image-container">
<!-- #ifndef APP-NVUE -->
<image :class="isGoodsPopup ? 'goods-item-popup-image' : 'goods-item-image'" :src="item.images" mode="aspectFit"></image>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<image :class="isGoodsPopup ? 'goods-item-popup-image' : 'goods-item-image'" :src="item.images" mode="aspectFit"></image>
<!-- #endif -->
<text class="image-top-index">{{ index + 1 }}</text>
<!-- 音乐进度条 -->
<view v-if="item.id == explanation_id" class="music-progress-container flex-row align-c jc-c" :style="isGoodsPopup ? 'width: 200rpx;' : 'width: 120rpx;'">
<!-- #ifndef APP-NVUE -->
<view class="music-progress-bars mr-5">
<view class="music-bar bar1"></view>
<view class="music-bar bar2"></view>
<view class="music-bar bar3"></view>
</view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view class="music-progress-bars mr-5">
<view :ref="(el) => { if(item.id == explanation_id) bar1Ref[index] = el }" class="music-bar nvue-bar"></view>
<view :ref="(el) => { if(item.id == explanation_id) bar2Ref[index] = el }" class="music-bar nvue-bar"></view>
<view :ref="(el) => { if(item.id == explanation_id) bar3Ref[index] = el }" class="music-bar nvue-bar"></view>
</view>
<!-- #endif -->
<text class="size-12 cr-f">讲解中</text>
</view>
</view>
<view :class="isGoodsPopup ? 'ml-10 flex-1 flex-col jc-sb goods-item-popup-content' : 'ml-10 flex-1 flex-col jc-sb goods-item-content'" >
<template v-if="!isGoodsPopup">
<text class="goods-item-title text-line-2">{{ item.title }}</text>
<view class="flex-row align-c jc-sb">
<view class="flex-row align-c">
<text class="mr-5 size-10 cr-9">{{ item.show_price_symbol}}</text>
<text class="goods-item-price size-10 cr-price">{{ item.price }}</text>
<text class="ml-5 size-10 cr-9">{{ item.show_price_unit }}</text>
</view>
<view class="flex-row align-c">
<text class="mr-5 size-10 cr-9">库存</text>
<text class="goods-item-inventory size-10 cr-9">{{ item.inventory }}</text>
</view>
</view>
</template>
<template v-else>
<text class="flex-1 goods-item-title text-line-2">{{ item.title }}</text>
<view class="flex-1 mt-10">
<view class="flex-1 flex-row align-c jc-sb">
<view class="flex-row align-c">
<text class="mr-5 size-10 cr-9">{{ item.show_price_symbol}}</text>
<text class="goods-item-price size-10 cr-price">{{ item.price }}</text>
<text class="ml-5 size-10 cr-9">{{ item.show_price_unit }}</text>
</view>
<view class="flex-row align-c">
<text class="mr-5 size-10 cr-9">库存</text>
<text class="goods-item-inventory size-10 cr-9">{{ item.inventory }}</text>
</view>
</view>
<view class="flex-row align-c mt-10 jc-e">
<button v-if="index !== 0" type="primary" plain class="btn-block cr-main mr-10 ml-0 flex-row align-c jc-c pa-5" style="width: 100rpx;height:48rpx;border-radius: 10rpx" :data-id="item.id" @tap="top_good"><text class="size-14 cr-main"></text></button>
<button type="primary" class="btn-block bg-red mr-10 ml-0 flex-row align-c jc-c pa-5" style="width: 100rpx;height:48rpx;border-radius: 10rpx;border: 2rpx solid #e22c08;" :data-id="item.id" @tap="delete_good"><text class="size-14 cr-f">删除</text></button>
<button v-if="isGoodsPopup" type="primary" class="btn-block cr-main mr-0 ml-0 flex-row align-c jc-c pa-5" style="width: 100rpx;height:48rpx;border-radius: 10rpx;border: 2rpx solid #0079ff;" :data-id="item.id" @tap="explanation"><text class="size-14 cr-f">{{ item.id == explanation_id ? '' : '' }}</text></button>
</view>
</view>
</template>
</view>
</view>
<view v-if="!isGoodsPopup" class="flex-row align-c jc-e mt-10">
<button v-if="index !== 0" type="primary" plain class="btn-block cr-main mr-10 ml-0 flex-row align-c jc-c pa-5" style="width: 100rpx;height:48rpx;border-radius: 10rpx" :data-id="item.id" @tap="top_good"><text class="size-14 cr-main"></text></button>
<button type="primary" class="btn-block bg-red mr-10 ml-0 flex-row align-c jc-c pa-5" style="width: 100rpx;height:48rpx;border-radius: 10rpx;border: 2rpx solid #e22c08;" :data-id="item.id" @tap="delete_good"><text class="size-14 cr-f">删除</text></button>
<button v-if="isGoodsPopup" type="primary" class="btn-block cr-main mr-0 ml-0 flex-row align-c jc-c pa-5" style="width: 100rpx;height:48rpx;border-radius: 10rpx;border: 2rpx solid #0079ff;" :data-id="item.id" @tap="explanation"><text class="size-14 cr-f">{{ item.id == explanation_id ? '' : '' }}</text></button>
</view>
</view>
</view>
<!-- #ifdef APP-NVUE -->
</cell>
<cell>
<u-bottom-line :status="bottom_line_status" :width="windowWidth"></u-bottom-line>
</cell>
</list>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
</view>
<u-bottom-line :status="bottom_line_status"></u-bottom-line>
</scroll-view>
<!-- #endif -->
</block>
<block v-else>
<view class="flex-1 flex-col align-c">
<text class="tip-title">暂无商品</text>
</view>
</block>
</view>
</template>
<script>
const app = getApp();
export default {
name: 'Goods',
props: {
// isGoodsPopup isGoodsPopupfalse
isGoodsPopup: {
type: Boolean,
default: true
}
},
data() {
return {
//#region
//
page: 1,
is_loading: true,
//#endregion
//#region
popup_goods_show: false,
good_list: [],
// , vuenvue使100%
windowWidth: 0,
windowHeight: 0,
//#endregion
//#region
bottom_line_status: true,
explanation_id: '',
//#endregion
//#region
// #ifdef APP-NVUE
// nvue使transition
nvue_animation: null,
bar1Ref: [],
bar2Ref: [],
bar3Ref: [],
animationTimers: [],
// #endif
//#endregion
}
},
computed: {
//#region
good_style() {
//
if (!this.isGoodsPopup) {
return `width:${ this.windowWidth }px;height: ${ this.windowHeight }px;`;
} else {
//
return `width:${ this.windowWidth }px;height: ${ this.windowHeight - 300}px;`;
}
},
//#endregion
//
checkbox_number() {
return this.good_list.filter(item => item.checkbox).length;
}
},
watch: {
//#region
// #ifdef APP-NVUE
// ID
explanation_id: {
handler(newVal, oldVal) {
//
this.stop_nvue_animation();
if (newVal) {
//
setTimeout(() => {
//
const index = this.good_list.findIndex(item => item.id == newVal);
if (index !== -1) {
this.start_nvue_height_animation(this.bar1Ref[index], 100);
this.start_nvue_height_animation2(this.bar2Ref[index], 300);
this.start_nvue_height_animation3(this.bar3Ref[index], 500);
}
}, 50);
}
}
}
// #endif
//#endregion
},
mounted() {
const data = uni.getWindowInfo();
this.windowWidth = data.windowWidth;
this.windowHeight = data.windowHeight;
this.init();
// #ifdef APP-NVUE
this.nvue_animation = uni.requireNativePlugin('animation');
// #endif
},
onShow() {
this.init();
},
beforeDestroy() {
// #ifdef APP-NVUE
this.stop_nvue_animation();
// #endif
},
methods: {
//#region
init() {
if (!this.isGoodsPopup) {
this.is_loading = true;
} else {
this.is_loading = false;
}
uni.showLoading({
title: '加载中...',
mask: true
});
uni.request({
url: app.globalData.get_request_url('index,roomgoods,live'),
method: 'POST',
data: {},
dataType: 'json',
success: (res) => {
uni.hideLoading();
this.is_loading = false;
if (res.code == 0) {
this.good_list = res.data;
}
},
fail: () => {
uni.hideLoading();
this.is_loading = false;
}
});
},
//#endregion
//#region
//
add_goods() {
this.popup_goods_show = false;
this.$nextTick(() => {
this.popup_goods_show = true;
this.$refs.popupGoodsRef.open();
})
},
//
submitEvent() {
this.$refs.popupGoodsRef.close();
//
this.init();
},
//#endregion
//#region
//
explanation(e) {
//
const id = e.currentTarget.dataset.id;
if (id != null && id != this.explanation_id) {
this.explanation_id = id;
} else {
this.explanation_id = '';
}
},
//#endregion
//#region
// #ifdef APP-NVUE
// vueCSS
start_nvue_height_animation(barRef, delay) {
if (!barRef) return;
const animateBar = () => {
if (!barRef) return;
//
this.nvue_animation.transition(barRef, {
styles: {
transform: 'scaleY(2.8)'
},
duration: 165, // 1.1s * 0.15
timingFunction: 'ease-in-out'
}, () => {
if (!barRef) return;
//
this.nvue_animation.transition(barRef, {
styles: {
transform: 'scaleY(1.2)'
},
duration: 165, // 1.1s * 0.15
timingFunction: 'ease-in-out'
}, () => {
if (!barRef) return;
//
this.nvue_animation.transition(barRef, {
styles: {
transform: 'scaleY(3.5)'
},
duration: 165, // 1.1s * 0.15
timingFunction: 'ease-in-out'
}, () => {
if (!barRef) return;
//
this.nvue_animation.transition(barRef, {
styles: {
transform: 'scaleY(1.8)'
},
duration: 165, // 1.1s * 0.15
timingFunction: 'ease-in-out'
}, () => {
if (!barRef) return;
//
this.nvue_animation.transition(barRef, {
styles: {
transform: 'scaleY(2.2)'
},
duration: 165, // 1.1s * 0.15
timingFunction: 'ease-in-out'
}, () => {
if (!barRef) return;
//
this.nvue_animation.transition(barRef, {
styles: {
transform: 'scaleY(1)'
},
duration: 165, // 1.1s * 0.15
timingFunction: 'ease-in-out'
}, () => {
//
const randomDelay = 100 + Math.random() * 500;
const timer = setTimeout(animateBar, randomDelay);
this.animationTimers.push(timer);
});
});
});
});
});
});
};
//
const timer = setTimeout(animateBar, delay);
this.animationTimers.push(timer);
},
// 1.4s
start_nvue_height_animation2(barRef, delay) {
if (!barRef) return;
const animateBar = () => {
if (!barRef) return;
// CSS
const steps = [
{ scale: 1.5, duration: 140 }, // 1.4s * 0.1
{ scale: 3, duration: 140 }, // 1.4s * 0.1
{ scale: 2, duration: 140 }, // 1.4s * 0.1
{ scale: 4, duration: 140 }, // 1.4s * 0.1
{ scale: 1.2, duration: 140 }, // 1.4s * 0.1
{ scale: 2.5, duration: 140 }, // 1.4s * 0.1
{ scale: 1.7, duration: 140 }, // 1.4s * 0.1
{ scale: 3.2, duration: 140 }, // 1.4s * 0.1
{ scale: 1.4, duration: 140 }, // 1.4s * 0.1
{ scale: 1, duration: 140 } // 1.4s * 0.1
];
let stepIndex = 0;
const executeStep = () => {
if (stepIndex >= steps.length) {
//
const randomDelay = 100 + Math.random() * 500;
const timer = setTimeout(animateBar, randomDelay);
this.animationTimers.push(timer);
return;
}
const { scale, duration } = steps[stepIndex];
this.nvue_animation.transition(barRef, {
styles: {
transform: `scaleY(${scale})`
},
duration: duration,
timingFunction: 'ease-in-out'
}, () => {
stepIndex++;
executeStep();
});
};
executeStep();
};
//
const timer = setTimeout(animateBar, delay);
this.animationTimers.push(timer);
},
// 0.9s
start_nvue_height_animation3(barRef, delay) {
if (!barRef) return;
const animateBar = () => {
if (!barRef) return;
// CSS
const steps = [
{ scale: 3.8, duration: 225 }, // 0.9s * 0.25
{ scale: 1.6, duration: 225 }, // 0.9s * 0.25
{ scale: 2.6, duration: 225 }, // 0.9s * 0.25
{ scale: 1, duration: 225 } // 0.9s * 0.25
];
let stepIndex = 0;
const executeStep = () => {
if (stepIndex >= steps.length) {
//
const randomDelay = 100 + Math.random() * 500;
const timer = setTimeout(animateBar, randomDelay);
this.animationTimers.push(timer);
return;
}
const { scale, duration } = steps[stepIndex];
this.nvue_animation.transition(barRef, {
styles: {
transform: `scaleY(${scale})`
},
duration: duration,
timingFunction: 'ease-in-out'
}, () => {
stepIndex++;
executeStep();
});
};
executeStep();
};
//
const timer = setTimeout(animateBar, delay);
this.animationTimers.push(timer);
},
stop_nvue_animation() {
//
this.animationTimers.forEach(timer => {
clearTimeout(timer);
});
this.animationTimers = [];
//
const resetBar = (barRef) => {
if (barRef) {
this.nvue_animation.transition(barRef, {
styles: {
transform: 'scaleY(1)'
},
duration: 100
});
}
};
//
this.bar1Ref.forEach(resetBar);
this.bar2Ref.forEach(resetBar);
this.bar3Ref.forEach(resetBar);
},
// #endif
//#endregion
back() {
app.globalData.page_back_prev_event();
}
}
}
</script>
<style lang="scss" scoped>
.tip-title {
font-size: 44rpx;
font-weight: bold;
color: #000;
}
.goods-header-fixed {
position: absolute;
top: 0;
left:0;
z-index: 9;
}
.goods-bottom-fixed {
position: absolute;
left: 0;
bottom:0;
z-index: 8;
}
.scroll-type {
padding: 124rpx 10px;
box-sizing: border-box;
}
.checkbox {
transform: scale(0.85);
}
.goods-item {
background: #fff;
margin-bottom: 20rpx;
padding: 30rpx 40rpx;
border-radius: 20rpx;
.goods-item-image-container {
border-radius: 10rpx;
overflow: hidden;
}
.goods-item-popup-image {
width: 200rpx;
height: 200rpx;
}
.goods-item-image {
width: 120rpx;
height: 120rpx;
}
.goods-item-title {
font-weight: 500;
font-size: 28rpx;
color: #333;
}
.goods-item-content {
height: 120rpx;
}
.goods-item-popup-content {
height: 200rpx;
}
}
.image-top-index {
position: absolute;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 24rpx;
padding: 4rpx 16rpx;
border-top-left-radius: 10rpx;
border-bottom-right-radius: 10rpx;
z-index: 3;
}
/* #ifdef APP-NVUE */
.select-all {
margin-left: 6rpx;
}
/* #endif */
.music-progress-container {
position: absolute;
left: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
padding: 10rpx 0;
border-bottom-left-radius: 10rpx;
border-bottom-right-radius: 10rpx;
z-index: 3;
}
.music-progress-bars {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-end; /* 底部对齐 */
height: 20rpx;
}
.music-bar {
width: 4rpx;
height: 4rpx; /* 初始高度较小 */
background-color: #fff;
margin: 0 4rpx;
transform-origin: bottom; /* 以底部为变换中心 */
}
.nvue-bar {
width: 4rpx;
height: 6rpx;
background-color: #fff;
margin: 0 4rpx;
transform-origin: bottom; /* 以底部为变换中心 */
}
// #ifndef APP-NVUE
.bar1 {
animation: beat1 1.1s infinite ease-in-out;
}
.bar2 {
animation: beat2 1.4s infinite ease-in-out;
}
.bar3 {
animation: beat3 0.9s infinite ease-in-out;
}
@keyframes beat1 {
0% {
transform: scaleY(1);
}
15% {
transform: scaleY(2.8);
}
30% {
transform: scaleY(1.2);
}
45% {
transform: scaleY(3.5);
}
60% {
transform: scaleY(1.8);
}
75% {
transform: scaleY(2.2);
}
100% {
transform: scaleY(1);
}
}
@keyframes beat2 {
0% {
transform: scaleY(1);
}
10% {
transform: scaleY(1.5);
}
20% {
transform: scaleY(3);
}
30% {
transform: scaleY(2);
}
40% {
transform: scaleY(4);
}
50% {
transform: scaleY(1.2);
}
60% {
transform: scaleY(2.5);
}
70% {
transform: scaleY(1.7);
}
80% {
transform: scaleY(3.2);
}
90% {
transform: scaleY(1.4);
}
100% {
transform: scaleY(1);
}
}
@keyframes beat3 {
0% {
transform: scaleY(1);
}
25% {
transform: scaleY(3.8);
}
50% {
transform: scaleY(1.6);
}
75% {
transform: scaleY(2.6);
}
100% {
transform: scaleY(1);
}
}
// #endif
</style>

View File

@ -20,7 +20,7 @@
<image :src="item.avatar" class="viewer-avatar" mode="aspectFill"></image>
</view>
</view>
<view class="viewer-back ml-5 flex-row align-c jc-c" @tap="live_back">
<view class="viewer-back ml-5 flex-row align-c jc-c bg-white" @tap="live_back">
<u-icon name="close-fillup" class="viewer-back-icon" size="50rpx" color="#fff"></u-icon>
</view>
</view>
@ -108,7 +108,7 @@
<!-- 底部交互区域 -->
<view class="flex-row align-c mt-5">
<view class="flex-1 bottom-actions-input">
<input :value="comment_value" type="text" confirm-type="done" :adjust-position="false" placeholder="说点什么" @focus="add_comment" @input="(e) => comment_value = e.detail.value" />
<input :value="comment_value" type="text" confirm-type="done" :adjust-position="false" placeholder="说点什么" @focus="add_comment" @input="(e) => comment_value = e.detail.value" @confirm="comment_input_confirm" />
</view>
<view class="bottom-actions-icon" @tap="add_goods">
<u-icon name="shopping-cart-tall" color="#fff" size="32rpx"></u-icon>
@ -131,29 +131,19 @@
</view>
</view>
<!-- 商品弹出框 -->
<component-popup v-if="goods_popup_status" :propShow="goods_popup_status" propPosition="bottom" propStyle="background: #F6F6F6;" @onclose="goods_popup_close_event">
<view class="discount_detail-popup" :style="'width:' + windowWidth + 'px;'">
<view class="oh tc discount_detail-popup-title padding-main">
<text class="text-size">添加商品</text>
<view class="fr" @tap.stop="goods_popup_close_event">
<iconfont name="icon-close-line" size="28rpx" color="#999"></iconfont>
</view>
</view>
<s-goods isGoodsPopup></s-goods>
</view>
</component-popup>
<u-popup ref="popupGoodsRef" mode="bottom" title="添加商品" :closeable="true">
<s-goods isGoodsPopup></s-goods>
</u-popup>
</view>
</template>
<script>
import componentPopup from "@/components/popup/popup";
import SGoods from "@/pages/plugins/live/pull/components/goods";
import sGoods from "@/pages/plugins/live/pull/components/goods";
const app = getApp();
export default {
name: 'LiveContent',
components: {
componentPopup,
SGoods
sGoods
},
props: {
liveConfig: {
@ -541,12 +531,8 @@
//#region
add_goods() {
this.goods_popup_status = true;
this.$refs.popupGoodsRef.open();
},
goods_popup_close_event(e) {
// nvue setData
this.goods_popup_status = false;
}
//#endregion
}
}

View File

@ -6,7 +6,7 @@
<live-player :src="src" autoplay class="video-size" @statechange="statechange" @error="error" />
<!-- #endif -->
<!-- #ifdef APP -->
<video :src="src" autoplay :is-video="true" :style="{width: windowWidth + 'px', height: windowHeight + 'px'}"></video>
<video :src="src" autoplay :is-video="true" :controls="false" :style="{width: windowWidth + 'px', height: windowHeight + 'px'}"></video>
<!-- #endif -->
</template>