vr-shopxo-uniapp/pages/plugins/live/pull/components/goods/goods.vue

773 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view v-if="!is_loading" class="flex-row align-c jc-c pa-20 box-border-box oh bg-f9" :style="good_style">
<block v-if="good_list.length > 0">
<!-- 固定在顶部的商品操作栏 -->
<view class="goods-header-fixed" :style="'width:' + propWindowWidth + 'px;'">
<view class="flex-row align-c jc-e pa-10">
<view class="flex-col" @tap="goods_order">
<u-icon propName="list-setup" propSize="36rpx" propColor="#999"></u-icon>
<text class="mt-5 size-12 cr-9">订单</text>
</view>
<view class="flex-col ml-10" @tap="goods_cart">
<u-icon propName="cart-solid" propSize="36rpx" propColor="#999"></u-icon>
<text class="mt-5 size-12 cr-9">购物车</text>
</view>
</view>
</view>
<!-- nvue 使用 list进行列表渲染 -->
<!-- #ifdef APP-NVUE -->
<list class="scroll-type" :style="good_style + (propIsGoodsPopup ? '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 + (propIsGoodsPopup ? '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 box-border-box' + (item.id == explanation_id ? ' bg-red-light' : '')" :style="'width:' + (propWindowWidth - 24) + 'px;'" :data-index="index" :data-value="item.checkbox" :data-url="item.goods_url" @tap="goods_detail">
<view class="flex-1">
<view class="flex-row align-c">
<view class="pr goods-item-image-container">
<!-- #ifndef APP-NVUE -->
<image :class="propIsGoodsPopup ? 'goods-item-popup-image' : 'goods-item-image'" :src="item.images" mode="aspectFit"></image>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<image :class="propIsGoodsPopup ? '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="propIsGoodsPopup ? '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="ml-10 flex-1 flex-col jc-sb goods-item-popup-content">
<text class="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-14 cr-9">{{ item.show_price_symbol}}</text>
<text class="goods-item-price size-14 cr-price">{{ item.price }}</text>
<text class="ml-5 size-14 cr-9">{{ item.show_price_unit }}</text>
</view>
</view>
<view class="flex-row align-c mt-10 jc-sb">
<view class="flex-row align-c">
<text class="mr-5 size-14 cr-9">库存</text>
<text class="goods-item-inventory size-14 cr-9">{{ item.inventory }}</text>
</view>
<!-- 购买按钮 -->
<button type="primary" class="btn-block cr-main bg-main mr-0 ml-0 flex-row align-c jc-c pa-5" style="width: 100rpx;height:48rpx;border-radius: 10rpx" :data-id="item.id" :data-url="item.goods_url" @tap="goods_detail"><text class="size-14 cr-f">购买</text></button>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- #ifdef APP-NVUE -->
</cell>
<cell>
<component-bottom-line :propStatus="bottom_line_status" :propWidth="propWindowWidth"></component-bottom-line>
</cell>
</list>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
</view>
<component-bottom-line :propStatus="bottom_line_status"></component-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>
import componentBottomLine from '@/components/bottom-line/bottom-line.vue';
const app = getApp();
/**
* 商品展示组件
* 用于直播间商品列表展示,支持普通页面和弹窗两种模式
*/
export default {
name: 'Goods',
components: {
componentBottomLine
},
props: {
/**
* 是否是商品弹出框
* 弹出框一般是用在直播页显示的商品列表直播开始前isGoodsPopup是false显示的是一整个页面
*/
propIsGoodsPopup: {
type: Boolean,
default: true
},
/**
* 直播间ID
*/
propLiveRoomId: {
type: Number,
default: 0
},
propWindowWidth: {
type: Number,
default: 0
},
propWindowHeight: {
type: Number,
default: 0
},
},
data() {
return {
//#region 初始化
// 搜索按钮点击事件
page: 1,
is_loading: true,
//#endregion
//#region 打开商品弹出框
popup_goods_show: false,
good_list: [],
//#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.propIsGoodsPopup) {
return `width:${ this.propWindowWidth }px;height: ${ this.propWindowHeight }px;`;
} else {
console.log(this.propWindowHeight);
// 如果是弹出框模式的就不全屏显示
return `width:${ this.propWindowWidth }px;height: ${ this.propWindowHeight - 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() {
// 初始化商品列表
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.propIsGoodsPopup) {
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;
const new_data = res.data;
if (new_data.code == 0) {
this.good_list = new_data.data;
// 遍历获取讲解id
const explain_data = new_data.data.filter(item => item.live_room_goods_is_explain == 1);
if (explain_data.length > 0) {
this.explanation_id = explain_data[0].id;
}
}
},
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 音乐进度条相关逻辑
// #ifdef APP-NVUE
/**
* 启动第一个音频条动画
* 实现1.1秒周期的高度变化动画效果
* @param {Object} barRef 条形元素引用
* @param {Number} delay 动画延迟时间(ms)
*/
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.4秒周期的高度变化动画效果
* @param {Object} barRef 条形元素引用
* @param {Number} delay 动画延迟时间(ms)
*/
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.9秒周期的高度变化动画效果
* @param {Object} barRef 条形元素引用
* @param {Number} delay 动画延迟时间(ms)
*/
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);
},
/**
* 停止所有NVUE动画
* 清除动画定时器并重置所有音频条到初始状态
*/
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
/**
* 查看商品详情
* 点击商品图片或购买按钮时跳转到商品详情页
* @param {Object} e 事件对象
*/
goods_detail(e) {
const url = e.currentTarget.dataset.url + '&live_room_id=1';
app.globalData.url_open(url);
},
/**
* 查看订单
* 点击订单图标时跳转到用户订单页面
* @param {Object} e 事件对象
*/
goods_order (e) {
console.log('查看订单');
app.globalData.url_open('/pages/user-order/user-order');
},
/**
* 查看购物车
* 点击购物车图标时跳转到购物车页面
* @param {Object} e 事件对象
*/
goods_cart (e) {
console.log('查看购物车');
app.globalData.url_open('/pages/cart-page/cart-page');
},
/**
* 返回上一页
*/
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: 4rpx 2px 16rpx 2px;
padding: 30rpx 40rpx;
border-radius: 20rpx;
box-shadow: 2px 2px 4px 2px #0000000d;
.goods-item-image-container {
border-radius: 10rpx;
overflow: hidden;
}
.goods-item-title {
font-weight: 500;
font-size: 28rpx;
color: #333;
}
.goods-item-content {
height: 120rpx;
}
.goods-item-popup-content {
height: 200rpx;
}
}
.goods-item-popup-image {
width: 200rpx;
height: 200rpx;
}
.goods-item-image {
width: 120rpx;
height: 120rpx;
}
.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>