初始化代码

This commit is contained in:
yiqiuyang
2025-09-03 18:05:48 +08:00
parent e5712b2e97
commit 4414fe517b
70 changed files with 2404 additions and 9 deletions

View File

@ -0,0 +1,342 @@
<script setup>
import {
ref,
computed,
watch,
nextTick,
getCurrentInstance,
onUnmounted,
} from 'vue'
/* -------------------- 组件接口定义 -------------------- */
const currentPage = defineModel({type: [Number, String], default: 1})
const props = defineProps({
id: {type: String, required: true},
title: {type: String, default: ''},
data: {type: Array, default: () => [1]},
pageSize: {type: [Number, String], default: 1},
showDot: {type: Boolean, default: true},
showPagination: {type: Boolean, default: true},
showHover: {type: Boolean, default: true},
autoPlay: {type: Boolean, default: false},
transitionDuration: {type: Number, default: 300},
sourceWidth: {type: Number, default: 276},
sourceHeight: {type: Number, default: 355},
sourceGap: {type: Number, default: 20},
})
const emit = defineEmits(['page-change', 'item-click', 'item-hover'])
/* -------------------- 响应式变量 -------------------- */
const instance = getCurrentInstance()
const $fontSize = instance.appContext.config.globalProperties.$fontSize
const trackRef = ref(null)
let autoPlayTimer = null
/* -------------------- 计算属性 -------------------- */
const itemWidth = computed(() => $fontSize(props.sourceWidth))
const itemHeight = computed(() => $fontSize(props.sourceHeight))
const itemGap = computed(() => $fontSize(props.sourceGap))
const itemTotalWidth = computed(() => itemWidth.value + itemGap.value)
const viewPortWidth = computed(
() =>
props.pageSize * itemWidth.value +
(props.pageSize - 1) * itemGap.value +
'px'
)
const trackWidth = computed(
() => props.data.length * itemTotalWidth.value - itemGap.value + 'px'
)
const totalPages = computed(() => props.data.length - props.pageSize + 1)
/* -------------------- 方法 -------------------- */
/**
* 执行平移动画
* @param {number} page - 目标页码
*/
const translate = (page) => {
if (!trackRef.value) return
trackRef.value.style.transition = `transform ${props.transitionDuration}ms ease`
trackRef.value.style.transform = `translateX(-${
itemTotalWidth.value * (page - 1)
}px)`
}
/**
* 切换到指定页码
* @param {number} page - 目标页码
*/
const goToPage = (page) => {
console.log('page===>', page)
currentPage.value = Number(page)
console.log('currentPage.value===>', currentPage.value, page)
}
/**
* 切换到下一页,循环播放
*/
const nextPage = () => {
const nextPage =
currentPage.value < totalPages.value ? currentPage.value + 1 : 1
goToPage(nextPage)
}
/**
* 切换到上一页,循环播放
*/
const prevPage = () => {
const prevPage =
currentPage.value > 1 ? currentPage.value - 1 : totalPages.value
goToPage(prevPage)
}
/**
* 处理项目点击事件
* @param {Object} item - 项目数据
* @param {number} index - 项目索引
*/
const handleItemClick = (item, index) => {
emit('item-click', item, index)
}
/**
* 处理项目悬停事件
* @param {Object} item - 项目数据
* @param {number} index - 项目索引
*/
const handleItemHover = (item, index) => {
clearInterval(autoPlayTimer)
autoPlayTimer = null
emit('item-hover', item, index)
}
/**
* 处理项目鼠标移出事件
* @param {Object} item - 项目数据
* @param {number} index - 项目索引
*/
const handleItemHoverLeave = () => {
startAutoPlay()
}
/**
* 启动自动播放
*/
const startAutoPlay = () => {
if (!props.autoPlay) return
stopAutoPlay()
autoPlayTimer = setInterval(nextPage, 3000)
}
/**
* 停止自动播放
*/
const stopAutoPlay = () => {
if (autoPlayTimer) {
clearInterval(autoPlayTimer)
autoPlayTimer = null
}
}
/* -------------------- 监听器 -------------------- */
// 监听页码变化,执行平移动画
watch(
currentPage,
(newVal, oldVal) => {
if (newVal !== oldVal) {
nextTick(() => translate(newVal))
}
},
{immediate: true}
)
// 监听自动播放设置变化
watch(
() => props.autoPlay,
(autoPlay) => {
autoPlay ? startAutoPlay() : stopAutoPlay()
},
{immediate: true}
)
// 监听数据变化,重置到第一页
watch(
() => props.data.length,
(newLen, oldLen) => {
if (newLen !== oldLen && currentPage.value !== 1) {
currentPage.value = 1
}
}
)
// 组件卸载时清理定时器
onUnmounted(stopAutoPlay)
</script>
<template>
<section :id="id" class="generic-carousel">
<!-- 标题 -->
<h2 v-if="title" class="carousel-title">{{ title }}</h2>
<!-- 分页指示器 -->
<div v-if="showDot" class="carousel-pagination">
<button
v-for="page in totalPages"
:key="page"
:class="['pagination-dot', {'is-active': page === currentPage}]"
@click="goToPage(page)"
:aria-label="`切换到第${page}页`"
/>
</div>
<!-- 导航按钮 -->
<div
v-if="showPagination && totalPages > 1"
class="carousel-navigation flex j-s"
>
<button class="nav-btn prev-btn" @click="prevPage" aria-label="上一页">
&lt;
</button>
<button class="nav-btn next-btn" @click="nextPage" aria-label="下一页">
&gt;
</button>
</div>
<!-- 轮播内容区域 -->
<div class="carousel-content">
<div class="carousel-viewport" :style="{width: viewPortWidth}">
<div
ref="trackRef"
class="carousel-track"
:class="{'carousel-track-hover': showHover}"
:style="{width: trackWidth}"
>
<div
v-for="(item, index) in data"
:key="index"
class="carousel-item"
:class="{'carousel-item-hover': showHover}"
:style="{
width: itemWidth + 'px',
height: itemHeight + 'px',
marginRight: index < data.length - 1 ? itemGap + 'px' : '0',
}"
@click="handleItemClick(item, index)"
@mouseenter="handleItemHover(item, index)"
@mouseleave="handleItemHoverLeave(item, index)"
>
<slot
:item="item"
:index="index"
:isActive="currentPage === Math.ceil((index + 1) / pageSize)"
/>
</div>
</div>
</div>
</div>
</section>
</template>
<style lang="scss" scoped>
.generic-carousel {
padding: 32px 0;
text-align: center;
position: relative;
.carousel-title {
font-family: 'PingFang SC';
font-weight: 600;
font-size: 38px;
color: #333;
}
.carousel-pagination {
width: 90px;
height: 5px;
margin: 20px auto 30px;
display: flex;
.pagination-dot {
flex: 1;
height: 100%;
background: #d9d9d9;
border: none;
cursor: pointer;
transition: background 0.3s ease;
&.is-active {
background: #0389ff;
}
}
}
.carousel-navigation {
position: absolute;
top: 50%;
left: 0;
right: 0;
padding: 0 50px;
z-index: 10;
pointer-events: none;
.nav-btn {
pointer-events: auto;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
color: #333;
font-size: 20px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
&:hover {
background: #0389ff;
color: white;
}
}
}
}
.carousel-content {
width: 100%;
overflow: hidden;
}
.carousel-viewport {
overflow: hidden;
position: relative;
left: 50%;
transform: translateX(-50%);
}
.carousel-track {
display: flex;
transition: transform 0.3s ease;
will-change: transform;
padding-top: 20px;
&-hover {
padding-top: 50px;
}
}
.carousel-item {
flex-shrink: 0;
transition: all 0.3s ease;
border-radius: 8px;
overflow: hidden;
&-hover {
cursor: pointer;
&:hover {
transform: translateY(-50px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
}
}
</style>