点赞效果优化

master
于肖磊 2025-12-03 16:56:34 +08:00
parent 20514d6d1e
commit 529be8d991
4 changed files with 441 additions and 24 deletions

View File

@ -0,0 +1,426 @@
<template>
<view class="simple-like-container flex-row box-sizing-border-box">
<view class="flex-1 pr">
<!-- 点赞动画元素 -->
<view
v-for="(like, index) in like_list"
:key="like.id"
class="like-item"
:ref="'likeItem' + like.id"
:style="{
left: like.x + 'px',
top: like.y + 'px',
color: like.color,
opacity: like.opacity
}"
>
<!-- 支持图片或自定义图标 -->
<image
v-if="like.image_src"
:src="like.image_src"
class="like-image"
mode="aspectFit"
></image>
<text v-else>{{ like.icon }}</text>
</view>
<!-- 连续点赞数量提示 -->
<text
v-if="show_like_count && like_count >= 3"
class="like-count"
ref="likeCount"
:style="{
left: like_count_position.x + 'px',
top: like_count_position.y + 'px',
color: like_count_color,
opacity: like_count_opacity,
transform: 'scale(' + like_count_scale + ')'
}"
>
x {{ like_count }}
</text>
</view>
</view>
</template>
<script>
// #ifdef APP-NVUE
const animation = uni.requireNativePlugin('animation');
// #endif
const COLORS = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#ff5252', '#66bb6a'];
const ICONS = [];
export default {
name: 'SimpleLikeEffect',
props: {
//
customIcons: {
type: Array,
default: () => []
},
//
customImages: {
type: Array,
default: () => []
}
},
data() {
return {
like_list: [],
//
like_count: 0,
show_like_count: false,
like_count_position: { x: 0, y: 0 },
like_count_color: '#ff6b6b',
like_count_opacity: 1,
like_count_scale: 1,
last_like_time: 0,
like_count_timer: null,
reset_timer: null, //
last_click_time: 0 //
}
},
computed: {
//
available_icons() {
return [...ICONS, ...this.customIcons];
},
//
available_images() {
return this.customImages;
}
},
methods: {
add_like(event, options = {}) {
//
const now = Date.now();
if (now - this.last_click_time < 200) {
return;
}
this.last_click_time = now;
//
let x, y;
//
if (event.touches && event.touches.length > 0) {
x = event.touches[0].pageX;
y = event.touches[0].pageY;
} else if (event.changedTouches && event.changedTouches.length > 0) {
x = event.changedTouches[0].pageX;
y = event.changedTouches[0].pageY;
} else {
x = event.pageX || event.detail?.x || 0;
y = event.pageY || event.detail?.y || 0;
}
// (-10px 10px)
const offset_x = Math.floor(Math.random() * 41) - 10;
const offset_y = Math.floor(Math.random() * 41) - 10;
// 20px
const clamped_offset_x = Math.max(-10, Math.min(10, offset_x));
const clamped_offset_y = Math.max(-10, Math.min(10, offset_y));
//
const adjusted_x = x + clamped_offset_x - 10;
const adjusted_y = y + clamped_offset_y - 10;
//
let icon, image_src, color;
//
if (this.available_images && this.available_images.length > 0) {
//
image_src = this.available_images[Math.floor(Math.random() * this.available_images.length)];
} else if (options.image_src) {
// 使
image_src = options.image_src;
}
// 使
if (!image_src) {
if (options.icon) {
// 使
icon = options.icon;
} else {
//
icon = this.available_icons[Math.floor(Math.random() * this.available_icons.length)];
}
}
//
if (options.color) {
color = options.color;
} else {
color = COLORS[Math.floor(Math.random() * COLORS.length)];
}
//
const new_like = {
id: Date.now() + Math.random(), // 使+ID
x: adjusted_x,
y: adjusted_y,
color: color,
icon: icon,
image_src: image_src,
opacity: 0 // opacity
};
//
this.like_list.push(new_like);
// #ifdef APP-NVUE
//
setTimeout(() => {
this.animate_like_item(new_like.id);
}, 0);
// #endif
// 2
setTimeout(() => {
this.remove_like(new_like.id);
}, 2000);
//
this.handle_like_count(x, y, color);
},
remove_like(id) {
this.like_list = this.like_list.filter(item => item.id !== id);
},
//
handle_like_count(x, y, color) {
const current_time = Date.now();
//
if (this.reset_timer) {
clearTimeout(this.reset_timer);
this.reset_timer = null;
}
//
if (this.like_count_timer) {
clearTimeout(this.like_count_timer);
this.like_count_timer = null;
}
// 1
if (current_time - this.last_like_time > 1000) {
this.like_count = 1;
this.show_like_count = false; //
} else {
//
this.like_count++;
}
//
this.last_like_time = current_time;
// 40px
this.like_count_position = {
x: x, //
y: y - 40 // 30px10px
};
this.like_count_color = color;
// 3
if (this.like_count >= 3) {
this.show_like_count = true;
this.like_count_opacity = 1;
this.like_count_scale = 1;
//
setTimeout(() => {
this.animate_like_count();
}, 0)
}
// 200ms
if (this.like_count >= 3) {
this.like_count_timer = setTimeout(() => {
this.hide_like_count();
}, 200);
}
// 1
this.reset_timer = setTimeout(() => {
this.like_count = 0;
this.show_like_count = false;
}, 1200);
},
//
hide_like_count() {
// #ifdef APP-NVUE
const ref = this.$refs['likeCount'];
if (ref) {
const el = this.is_array(ref) ? ref[0] : ref;
if (el) {
animation.transition(el, {
styles: {
opacity: 0,
transform: 'scale(0.5) translateY(-10px)'
},
duration: 500,
timingFunction: 'ease-out'
}, () => {
this.show_like_count = false;
});
}
}
// #endif
// #ifndef APP-NVUE
this.show_like_count = false;
// #endif
},
//
animate_like_item(id) {
// #ifdef APP-NVUE
const ref = this.$refs['likeItem' + id];
if (ref) {
const el = this.is_array(ref) ? ref[0] : ref;
if (el) {
//
this.like_list = this.like_list.map(item => {
if (item.id === id) {
item.opacity = 0;
}
return item;
});
//
animation.transition(el, {
styles: {
opacity: 1,
transform: 'scale(1.5) rotate(10deg)'
},
duration: 1000,
timingFunction: 'ease-out'
}, () => {
//
animation.transition(el, {
styles: {
opacity: 0,
transform: 'scale(1) rotate(20deg)'
},
duration: 1000,
timingFunction: 'ease-out'
});
});
}
}
// #endif
// #ifndef APP-NVUE
// nvue使CSS
this.like_list = this.like_list.map(item => {
if (item.id === id) {
item.opacity = 1;
}
return item;
});
// #endif
},
//
animate_like_count() {
// #ifdef APP-NVUE
const ref = this.$refs['likeCount'];
if (ref) {
const el = this.is_array(ref) ? ref[0] : ref;
if (el) {
//
this.like_count_opacity = 1;
this.like_count_scale = 1;
//
animation.transition(el, {
styles: {
opacity: 0,
transform: 'scale(0.5) translateY(-10px)'
},
duration: 500,
timingFunction: 'ease-out'
});
}
}
// #endif
},
//
is_array(data) {
return Array.isArray(data) || data instanceof Array || (data && typeof data === 'object' && data.length !== undefined);
}
}
}
</script>
<style scoped>
.simple-like-container {
position: absolute;
top: 0;
left: 0;
/* #ifndef APP-NVUE */
width: 100%;
height: 100%;
pointer-events: none;
/* #endif */
z-index: 9999;
}
.like-item {
position: absolute;
font-size: 20px;
}
.like-image {
width: 20px;
height: 20px;
}
.like-count {
position: absolute;
font-size: 16px;
font-weight: bold;
z-index: 10000;
text-wrap: nowrap;
}
/* #ifndef APP-NVUE */
.like-item {
opacity: 0;
animation: zoomInOut 2s ease-out forwards;
}
.like-count {
opacity: 1;
animation: shrinkUp 1s ease-out forwards;
}
@keyframes zoomInOut {
0% {
opacity: 0;
transform: scale(0) rotate(0deg);
}
50% {
opacity: 1;
transform: scale(1.5) rotate(10deg);
}
100% {
opacity: 0;
transform: scale(1) rotate(20deg);
}
}
@keyframes shrinkUp {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(-10px) scale(0.5);
}
}
/* #endif */
</style>

View File

@ -7,14 +7,14 @@
<!-- #endif -->
<!-- 顶部主播信息 -->
<view class="flex-row align-c jc-sb" :style="header_style">
<view class="top-header flex-row align-c pointer-events-auto" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="true">
<view class="top-header flex-row align-c pointer-events-auto">
<image :src="avatar" class="avatar" mode="aspectFill"></image>
<view class="ml-10 flex-col">
<text class="nickname text-line-1">{{ live_data && live_data.title ? live_data.title : '直播' }}</text>
<text class="level">{{ like_count }}本场点赞</text>
</view>
</view>
<view class="flex-row align-c pointer-events-auto" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="true">
<view class="flex-row align-c pointer-events-auto">
<view class="flex-row align-c pr" style="direction: rtl;">
<view v-for="(item, index) in viewers" :key="index" class="viewer-wrapper" :style="'z-index:' + (index + 1) + ';' + (index == 0 ? 'margin-right: 0;' : '')">
<image :src="item.avatar" class="viewer-avatar" mode="aspectFill"></image>
@ -28,7 +28,7 @@
<view class="flex-1 bottom-line-exclude-bottom flex-row">
<view class="flex-1 flex-col jc-e">
<view class="pr">
<view class="bulletin-area pr pointer-events-auto" :style="'width:' + (windowWidth - 150) + 'px;'" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="true">
<view class="bulletin-area pr pointer-events-auto" :style="'width:' + (windowWidth - 150) + 'px;'">
<!-- #ifdef APP-NVUE -->
<!-- nvue 使用 list进行列表渲染 -->
<list class="bulletin-area" :style="'width:' + (windowWidth - 150) + 'px;'" :show-scrollbar="false" loadmoreoffset="30" @scroll="scroll_event" @loadmore="scroll_to_lower_event">
@ -99,7 +99,7 @@
<text class="cr-10 cr-red">{{ message_num }}条新消息</text>
</view>
</view>
<view v-if="!isEmpty(explain_goods)" class="explain-goods" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="true">
<view v-if="!isEmpty(explain_goods)" class="explain-goods">
<image :src="explain_goods.goods_avatar" class="explain-goods-image" style="width: 198rpx;height: 198rpx;" mode="aspectFill"></image>
<view class="explain-goods-content mt-10" style="padding: 8rpx;box-sizing: border-box;">
<text class="explain-goods-name text-line-2 size-12">{{ explain_goods.goods_name }}</text>
@ -108,14 +108,14 @@
</view>
</view>
<!-- 底部谁来了的提示-->
<view v-if="is_user_comes" class="flex-row mt-3 pointer-events-auto" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="true" :style="'max-width:' + (windowWidth - 100) + 'px;'">
<view v-if="is_user_comes" class="flex-row mt-3 pointer-events-auto" :style="'max-width:' + (windowWidth - 100) + 'px;'">
<view class="user-comes flex-row">
<text class="user-name cr-blue">{{ commons_name }}</text>
<text class="user-name cr-d">来了</text>
</view>
</view>
<!-- 底部交互区域 -->
<view class="flex-row align-c mt-5 pointer-events-auto" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="true">
<view class="flex-row align-c mt-5 pointer-events-auto">
<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" @confirm="comment_input_confirm" />
</view>
@ -134,7 +134,7 @@
</view>
</view>
<!-- 添加评论 -->
<view v-if="is_add_comment" class="keyboard-input pointer-events-auto" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="true" :style="'width:' + windowWidth + 'px;bottom:' + listener_height + 'px;'">
<view v-if="is_add_comment" class="keyboard-input pointer-events-auto" :style="'width:' + windowWidth + 'px;bottom:' + listener_height + 'px;'">
<view class="input">
<input :value="comment_value" :focus="is_add_comment" type="text" confirm-type="done" :adjust-position="false" :auto-blur="true" placeholder="说点什么" @input="(e) => comment_value = e.detail.value" @blur="() => is_add_comment = false" @confirm="comment_input_confirm" />
</view>
@ -589,16 +589,6 @@
//
like_button_click(e) {
this.like_count++;
},
handle_touch_end(event) {
//#ifdef APP-NVUE
this.$emit("handleTouchEnd", event);
//#endif
},
handle_double_click(event) {
//#ifdef APP-NVUE
this.$emit("handleDoubleClick", event);
//#endif
}
}
}

View File

@ -1,13 +1,14 @@
<template>
<view :class="theme_view + ' flex-row pr'" :style="'width:' + windowWidth + 'px;height:' + windowHeight + 'px;'">
<view class="flex-1 pr">
<view class="flex-1 pr" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="false">
<live-video ref="live-video" :src="live_config.pull_flv_url || 'http://live-pull-all.shopxo.vip/68f764013572f9240ca7ce6c/shopxo122.m3u8'" @ended="ended"></live-video>
<!-- 简化版点赞效果组件 -->
<full-screen-like-effect ref="fullScreenLikeEffect" :custom-images="like_show_imgs" :style="'width:' + windowWidth + 'px;height:' + windowHeight + 'px;'"></full-screen-like-effect>
</view>
<view v-if="!is_loading" class="live-content" :style="'width:' + windowWidth + 'px;height:' + windowHeight + 'px;'" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="false">
<view v-if="!is_loading" class="live-content" :style="'width:' + windowWidth + 'px;height:' + windowHeight + 'px;'">
<template v-if="!is_live_ended">
<live-content ref="liveContent" :live-config="live_config" :live-show-imgs="like_show_imgs" @live-back="live_back" @handleDoubleClick="handle_double_click" @handleTouchEnd="handle_touch_end"></live-content>
<!-- 简化版点赞效果组件 -->
<full-screen-like-effect ref="fullScreenLikeEffect" :custom-images="like_show_imgs" :style="'width:' + windowWidth + 'px;height:' + windowHeight + 'px;'"></full-screen-like-effect>
</template>
<template v-else>
<view class="live-ended flex-row align-c jc-c" :style="'width:' + windowWidth + 'px;height:' + windowHeight + 'px;'" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="true">

View File

@ -1,13 +1,13 @@
<template>
<view :class="theme_view + ' live-bg'">
<view class="w h" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="false">
<view class="w h pr" @dblclick="handle_double_click" @touchend="handle_touch_end" :data-ignore="false">
<live-video ref="liveVideo" :src="live_config.pull_flv_url || 'http://live-pull-all.shopxo.vip/68f764013572f9240ca7ce6c/shopxo122.m3u8'" @ended="ended" @mutedAutoPlaySuccess="muted_auto_play_success"></live-video>
<!-- 简化版点赞效果组件 -->
<full-screen-like-effect ref="fullScreenLikeEffect" :custom-images="like_show_imgs"></full-screen-like-effect>
</view>
<view v-if="!is_loading" class="live-content pointer-events-none">
<view v-if="!is_loading" :class="'live-content ' + (!is_live_ended ? 'pointer-events-none' : '')">
<template v-if="!is_live_ended">
<live-content ref="liveContent" :live-config="live_config" :live-show-imgs="like_show_imgs" @live-back="live_back" @handleDoubleClick="handle_double_click" @handleTouchEnd="handle_touch_end"></live-content>
<live-content ref="liveContent" :live-config="live_config" :live-show-imgs="like_show_imgs" @live-back="live_back"></live-content>
</template>
<template v-else>
<view class="live-ended flex-row align-c jc-c">