滑动特效

This commit is contained in:
zpc 2025-07-25 02:50:17 +08:00
parent a103146953
commit 87c6fe7112
4 changed files with 332 additions and 56 deletions

2
components.d.ts vendored
View File

@ -16,6 +16,8 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Search: typeof import('./components/guyu/home/search.vue')['default']
SlideLabel: typeof import('./components/guyu/page/slide-label.vue')['default']
SlideLable: typeof import('./components/guyu/page/slide-lable.vue')['default']
Swiper: typeof import('./components/guyu/home/swiper.vue')['default']
Tabs: typeof import('./components/guyu/home/tabs.vue')['default']
}

View File

@ -1,26 +1,21 @@
<template>
<view class="tabs-container">
<view class="tabs-wrapper">
<template v-for="(item, index) in list" :key="index">
<view class="tab-item-wrapper" :style="{ width: getLableWidth(item.name) }">
<view class="tab-item" @click="clickTab(index)">
<template v-for="(item, index) in slideLableList" :key="index">
<view class="tab-item-wrapper" :style="{ width: item.width }">
<SlideLabel ref="slideLabels"
style="width: 100%;height:40rpx;border-radius: 50rpx;border: 1rpx solid #9A8F79;"
:defaultColor="item.active ? '#F5D677' : '#fff'" @click="clickTab(index)">
<text class="tab-text myZt-500w">{{ item.name }}</text>
</view>
<view style="position: relative;width:50%;height: 100%;overflow: hidden;top: -100%;"
:style="{ width: getNextWidth(index) }">
<view style="" :style="{ width: getLableWidth(item.name) }">
<view class="tab-item tab-item-active" @click="clickTab(index)">
<text class="tab-text myZt-500w">{{ item.name }}</text>
</view>
</view>
</view>
</SlideLabel>
</view>
</template>
</view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch, nextTick } from 'vue';
import SlideLabel from '@/components/guyu/page/slide-label.vue';
const props = defineProps({
list: {
@ -30,11 +25,68 @@ const props = defineProps({
current: {
type: Number,
default: 0
},
});
// 1-10
let slideDirection = ref(0);
//
let slideLableList = ref([]);
//
let slideLabels = ref([]);
// slideLableList
const initSlideLableList = () => {
slideLableList.value = props.list.map((item, index) => {
return {
name: item.name,
width: getLableWidth(item.name),
active: index === props.current,
data: item
}
});
// DOMSlideLabel
nextTick(() => {
updateLabelsState();
});
}
// props.list
watch(() => props.list, () => {
initSlideLableList();
}, { deep: true });
// current
watch(() => props.current, (newCurrent, oldCurrent) => {
if (newCurrent !== oldCurrent && slideLableList.value.length > 0) {
//
slideLableList.value.forEach((item, index) => {
item.active = index === newCurrent;
});
// nextTick(() => {
// updateLabelsState();
// });
}
});
onMounted(() => {
//
const updateLabelsState = () => {
if (!slideLabels.value || slideLabels.value.length === 0) return;
//
slideLabels.value.forEach((label, index) => {
if (label && label.reset) {
label.reset();
}
});
}
onMounted(() => {
initSlideLableList();
})
const getLableWidth = (name) => {
let t = name.length * 20 + 40;
if (t < 80) {
@ -42,43 +94,72 @@ const getLableWidth = (name) => {
}
return t + "rpx";
}
const getNextWidth = (currendIndex) => {
if (swiperDx.value == 0) {
return "0%";
}
if (swiperDx.value < 0) {
}
if (currendIndex == props.current) {
return ((swiperDx.value / systemWidth).toFixed(2) * 100) + "%";
}
return "0%";
}
// computed()
const systemWidth = parseInt(uni.getSystemInfoSync().screenWidth * 0.96);
const emit = defineEmits(['change']);
//
let isClick = false;
//
const clickTab = (index) => {
emit("change", index)
};
console.log('点击标签');
if (index == props.current) {
return;
}
isClick = true;
emit("change", index);
}
let swiperDx = ref(0);
let throttleTimer = null;
//
const setDx = (dx) => {
if (!throttleTimer) {
swiperDx.value = parseInt(dx);
console.log(swiperDx.value, dx, systemWidth, getNextWidth(props.current));
throttleTimer = setTimeout(() => {
throttleTimer = null;
}, 20);
if (!slideLabels.value || slideLabels.value.length === 0 || isClick) return;
// console.log('');
// (0-100)
const absDx = Math.abs(dx);
const percent = Math.min(100, Math.round((absDx / systemWidth) * 100));
//
const direction = dx > 0 ? 1 : (dx < 0 ? -1 : 0);
slideDirection.value = direction;
const currentIndex = props.current;
if (direction > 0 && currentIndex + 1 < slideLableList.value.length) {
// -
const nextIndex = currentIndex + 1;
//
slideLabels.value[currentIndex]?.coverFromLeft('#fff', percent);
//
slideLabels.value[nextIndex]?.coverFromLeft('#F5D677', percent);
} else if (direction < 0 && currentIndex > 0) {
// -
const prevIndex = currentIndex - 1;
//
slideLabels.value[currentIndex]?.coverFromRight('#fff', percent);
//
slideLabels.value[prevIndex]?.coverFromRight('#F5D677', percent);
}
}
//
const unlockDx = () => {
swiperDx.value = 0;
slideDirection.value = 0;
if (isClick) {
isClick = false;
}
//
nextTick(() => {
updateLabelsState();
//
slideLableList.value.forEach((item, index) => {
item.active = index === props.current;
});
});
}
//
@ -105,19 +186,6 @@ defineExpose({
text-align: center;
line-height: 40rpx;
height: 50rpx;
overflow: hidden;
}
.tab-item {
border-radius: 50rpx;
background-color: transparent;
border: 1rpx solid #9A8F79;
}
.tab-item-active {
background-color: #F5D677;
border: 1rpx solid transparent;
}
.tab-text {

View File

@ -0,0 +1,206 @@
<template>
<view class="slide-label" :style="containerStyle">
<!-- 默认颜色背景 -->
<view class="color-bg default-color" :style="{ background: defaultColor }"></view>
<!-- 右侧覆盖颜色 -->
<view class="color-bg right-cover" :style="rightCoverStyle" v-if="showRightCover"></view>
<!-- 左侧覆盖颜色 -->
<view class="color-bg left-cover" :style="leftCoverStyle" v-if="showLeftCover"></view>
<!-- 内容层 -->
<view class="content-layer">
<slot>{{ content }}</slot>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
//
const props = defineProps({
//
defaultColor: {
type: String,
default: '#FFFFFF'
},
//
content: {
type: String,
default: ''
},
//
height: {
type: [Number, String],
default: '80rpx'
},
//
borderRadius: {
type: [Number, String],
default: '8rpx'
},
//
textColor: {
type: String,
default: '#000000'
}
});
//
const emit = defineEmits(['coverChange']);
//
const rightCoverColor = ref('');
const rightCoverPercent = ref(0);
const showRightCover = ref(false);
//
const leftCoverColor = ref('');
const leftCoverPercent = ref(0);
const showLeftCover = ref(false);
//
const containerStyle = computed(() => {
return {
height: typeof props.height === 'number' ? `${props.height}rpx` : props.height,
borderRadius: typeof props.borderRadius === 'number' ? `${props.borderRadius}rpx` : props.borderRadius,
color: props.textColor
};
});
//
const rightCoverStyle = computed(() => {
return {
background: rightCoverColor.value,
width: `${rightCoverPercent.value}%`,
right: 0
};
});
//
const leftCoverStyle = computed(() => {
return {
background: leftCoverColor.value,
width: `${leftCoverPercent.value}%`,
left: 0
};
});
/**
* 从右侧覆盖颜色
* @param {String} color 新颜色
* @param {Number} percent 覆盖百分比(0-100)
*/
function coverFromRight(color, percent = 0) {
//
const safePercent = Math.max(0, Math.min(100, percent));
//
rightCoverColor.value = color;
rightCoverPercent.value = safePercent;
//
showRightCover.value = safePercent > 0;
// 0
if (safePercent === 0) {
showRightCover.value = false;
}
//
emit('coverChange', {
direction: 'right',
color: color,
percent: safePercent
});
}
/**
* 从左侧覆盖颜色
* @param {String} color 新颜色
* @param {Number} percent 覆盖百分比(0-100)
*/
function coverFromLeft(color, percent = 0) {
//
const safePercent = Math.max(0, Math.min(100, percent));
//
leftCoverColor.value = color;
leftCoverPercent.value = safePercent;
//
showLeftCover.value = safePercent > 0;
// 0
if (safePercent === 0) {
showLeftCover.value = false;
}
//
emit('coverChange', {
direction: 'left',
color: color,
percent: safePercent
});
}
/**
* 重置所有覆盖层
*/
function reset() {
rightCoverPercent.value = 0;
leftCoverPercent.value = 0;
showRightCover.value = false;
showLeftCover.value = false;
}
//
defineExpose({
coverFromRight,
coverFromLeft,
reset
});
</script>
<style scoped>
.slide-label {
position: relative;
width: 100%;
overflow: hidden;
}
.color-bg {
position: absolute;
top: 0;
bottom: 0;
transition: width 0.3s ease-out;
}
.default-color {
width: 100%;
height: 100%;
left: 0;
}
.right-cover {
height: 100%;
top: 0;
}
.left-cover {
height: 100%;
top: 0;
}
.content-layer {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -10,12 +10,12 @@
<!-- 轮播图 -->
<guyu-home-swiper :advert-list="homeData?.advertList" style="width: 100%;" />
<!-- 推荐位 -->
<guyu-home-recommend :rec-list="homeData?.recList" @item-click="toDetails" style="width: 100%;" />
<guyu-home-recommend :rec-list="homeData?.recList" style="width: 100%;" />
<!-- 小程序中直接修改组件style为position: sticky;无效需要在组件外层套一层view -->
<view class="bar-view" style="z-index:97;position: sticky;top :0;background-color: #fff;">
<view :style="{ height: tabsBarHeight + 'px' }" class="status-bar"> </view>
<guyu-home-tabs ref="tabs" :list="homeData.categories" @change="tabsChange" :current="current" />
<guyu-home-tabs ref="tabs" :list="homeData?.categories" @change="tabsChange" :current="current" />
</view>
<swiper class="swiper" :style="[{ height: swiperHeight + 'px' }]" :current="current"
@transition="swiperTransition" @animationfinish="swiperAnimationfinish">