This commit is contained in:
2026-03-25 14:54:15 +08:00
commit ab4646b43a
827 changed files with 123812 additions and 0 deletions
@@ -0,0 +1,49 @@
import type { ExtractPropTypes, PropType } from 'vue';
import type { ImgMode, SwiperIndicatorPosition, SwiperMode } from '../../types/global';
import { baseProps } from '../common/props';
/**
* SwiperProps 轮播图 props 类型定义
* @description 轮播图,支持多种指示器、3D、自动播放等
*/
export const SwiperProps = {
...baseProps,
/** 轮播数据列表,数组对象 */
list: { type: Array as PropType<Array<Record<string, any>>>, default: () => [] },
/** 是否显示标题 */
title: { type: Boolean, default: false },
/** 指示器配置对象 */
indicator: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
/** 圆角,单位rpx */
borderRadius: { type: [Number, String] as PropType<number | string>, default: 8 },
/** 自动切换时间间隔,单位ms */
interval: { type: [String, Number] as PropType<number | string>, default: 3000 },
/** 指示器类型,round/dot/line等 */
mode: { type: String as PropType<SwiperMode>, default: 'round' },
/** 轮播高度,单位rpx */
height: { type: [Number, String] as PropType<number | string>, default: 250 },
/** 指示器位置 */
indicatorPos: { type: String as PropType<SwiperIndicatorPosition>, default: 'bottomCenter' },
/** 是否开启3D效果 */
effect3d: { type: Boolean, default: false },
/** 3D模式下前一项的边距,单位rpx */
effect3dPreviousMargin: { type: [Number, String] as PropType<number | string>, default: 50 },
/** 是否自动播放 */
autoplay: { type: Boolean, default: true },
/** 切换动画时长,单位ms */
duration: { type: [Number, String] as PropType<number | string>, default: 500 },
/** 是否循环播放 */
circular: { type: Boolean, default: true },
/** 图片裁剪模式 */
imgMode: { type: String as PropType<ImgMode>, default: 'aspectFill' },
/** 轮播项对象的图片字段名 */
name: { type: String, default: 'image' },
/** 轮播背景色 */
bgColor: { type: String, default: 'var(--u-bg-color)' },
/** 当前激活项索引 */
current: { type: [Number, String] as PropType<number | string>, default: 0 },
/** 标题样式对象 */
titleStyle: { type: Object as PropType<Record<string, any>>, default: () => ({}) }
};
export type SwiperProps = ExtractPropTypes<typeof SwiperProps>;
@@ -0,0 +1,308 @@
<template>
<view
class="u-swiper-wrap"
:style="$u.toStyle({ borderRadius: `${borderRadius}rpx` }, customStyle)"
:class="customClass"
>
<swiper
:current="elCurrent"
@change="change"
@animationfinish="animationfinish"
:interval="interval"
:circular="circular"
:duration="duration"
:autoplay="autoplay"
:previous-margin="effect3d ? effect3dPreviousMargin + 'rpx' : '0'"
:next-margin="effect3d ? effect3dPreviousMargin + 'rpx' : '0'"
:style="{ height: height + 'rpx', backgroundColor: bgColor }"
>
<swiper-item class="u-swiper-item" v-for="(item, index) in list" :key="index">
<view
class="u-list-image-wrap"
@tap.stop.prevent="listClick(index)"
:class="[uCurrent != index ? 'u-list-scale' : '']"
:style="{
borderRadius: `${borderRadius}rpx`,
transform: effect3d && uCurrent != index ? 'scaleY(0.9)' : 'scaleY(1)',
margin: effect3d && uCurrent != index ? '0 20rpx' : 0
}"
>
<image class="u-swiper-image" :src="item[name] || item" :mode="imgMode"></image>
<view
v-if="title && item.title"
class="u-swiper-title u-line-1"
:style="[{ 'padding-bottom': titlePaddingBottom }, titleStyle]"
>
{{ item.title }}
</view>
</view>
</swiper-item>
</swiper>
<view
class="u-swiper-indicator"
:style="{
top:
indicatorPos == 'topLeft' || indicatorPos == 'topCenter' || indicatorPos == 'topRight'
? '12rpx'
: 'auto',
bottom:
indicatorPos == 'bottomLeft' || indicatorPos == 'bottomCenter' || indicatorPos == 'bottomRight'
? '12rpx'
: 'auto',
justifyContent: justifyContent,
padding: `0 ${effect3d ? '74rpx' : '24rpx'}`
}"
>
<block v-if="mode == 'rect'">
<view
class="u-indicator-item-rect"
:class="{ 'u-indicator-item-rect-active': index == uCurrent }"
v-for="(item, index) in list"
:key="index"
></view>
</block>
<block v-if="mode == 'dot'">
<view
class="u-indicator-item-dot"
:class="{ 'u-indicator-item-dot-active': index == uCurrent }"
v-for="(item, index) in list"
:key="index"
></view>
</block>
<block v-if="mode == 'round'">
<view
class="u-indicator-item-round"
:class="{ 'u-indicator-item-round-active': index == uCurrent }"
v-for="(item, index) in list"
:key="index"
></view>
</block>
<block v-if="mode == 'number'">
<view class="u-indicator-item-number">{{ uCurrent + 1 }}/{{ list.length }}</view>
</block>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'u-swiper',
options: {
addGlobalClass: true,
// #ifndef MP-TOUTIAO
virtualHost: true,
// #endif
styleIsolation: 'shared'
}
};
</script>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { SwiperProps } from './types';
import { $u } from '../..';
/**
* swiper 轮播图
* @description 该组件一般用于导航轮播,广告展示等场景,可开箱即用
* @tutorial https://uviewpro.cn/zh/components/swiper.html
* @property {Array} list 轮播图数据,见官网"基本使用"说明
* @property {Boolean} title 是否显示标题文字,需要配合list参数,见官网说明(默认false)
* @property {String} mode 指示器模式,见官网说明(默认round)
* @property {String|Number} height 轮播图组件高度,单位rpx(默认250)
* @property {String} indicator-pos 指示器的位置(默认bottomCenter
* @property {Boolean} effect3d 是否开启3D效果(默认false
* @property {Boolean} autoplay 是否自动播放(默认true
* @property {String|Number} interval 自动轮播时间间隔,单位ms(默认2500)
* @property {Boolean} circular 是否衔接播放,见官网说明(默认true)
* @property {String} bg-color 背景颜色(默认var(--u-bg-color)
* @property {String|Number} border-radius 轮播图圆角值,单位rpx(默认8)
* @property {Object} title-style 自定义标题样式
* @property {String|Number} effect3d-previous-margin mode = true模式的情况下,激活项与前后项之间的距离,单位rpx(默认50)
* @property {String} img-mode 图片的裁剪模式,详见image组件裁剪模式(默认aspectFill
* @event {Function} click 点击轮播图时触发
* @example <u-swiper :list="list" mode="dot" indicator-pos="bottomRight"></u-swiper>
*/
const props = defineProps(SwiperProps);
const emit = defineEmits(['click', 'change']);
// 当前活跃的swiper-item的index
const uCurrent = ref(Number(props.current));
// 监听list变化,重置uCurrent值,避免溢出
watch(
() => props.list,
(nVal, oVal) => {
if (nVal.length !== oVal.length) uCurrent.value = 0;
}
);
// 监听外部current的变化,实时修改内部依赖于此测uCurrent值
watch(
() => props.current,
n => {
uCurrent.value = Number(n);
}
);
// 容器 justifyContent
const justifyContent = computed(() => {
if (props.indicatorPos == 'topLeft' || props.indicatorPos == 'bottomLeft') return 'flex-start';
if (props.indicatorPos == 'topCenter' || props.indicatorPos == 'bottomCenter') return 'center';
if (props.indicatorPos == 'topRight' || props.indicatorPos == 'bottomRight') return 'flex-end';
return 'center';
});
// 标题下边距
const titlePaddingBottom = computed(() => {
if (props.mode == 'none') return '12rpx';
if (['bottomLeft', 'bottomCenter', 'bottomRight'].includes(props.indicatorPos) && props.mode == 'number') {
return '60rpx';
} else if (['bottomLeft', 'bottomCenter', 'bottomRight'].includes(props.indicatorPos) && props.mode != 'number') {
return '40rpx';
} else {
return '12rpx';
}
});
// swiper组件current参数只接受Number类型
const elCurrent = computed(() => Number(props.current));
/**
* 点击轮播图项
*/
function listClick(index: number) {
emit('click', index);
}
/**
* swiper change事件
*/
function change(e: any) {
const current = e.detail.current;
uCurrent.value = current;
emit('change', current);
}
/**
* swiper animationfinish事件
* 头条小程序不支持animationfinish事件,改由change事件
* 暂不监听此事件,因为不再给swiper绑定uCurrent属性
*/
function animationfinish(e: any) {
// #ifndef MP-TOUTIAO
// uCurrent.value = e.detail.current
// #endif
}
defineExpose({ listClick, change, animationfinish });
</script>
<style lang="scss" scoped>
@import '../../libs/css/style.components.scss';
.u-swiper-wrap {
position: relative;
overflow: hidden;
transform: translateY(0);
}
.u-swiper-image {
width: 100%;
will-change: transform;
height: 100%;
/* #ifndef APP-NVUE */
display: block;
/* #endif */
/* #ifdef H5 */
pointer-events: none;
/* #endif */
}
.u-swiper-indicator {
padding: 0 24rpx;
position: absolute;
@include vue-flex;
width: 100%;
z-index: 1;
}
.u-indicator-item-rect {
width: 26rpx;
height: 8rpx;
margin: 0 6rpx;
transition: all 0.5s;
background-color: rgba(0, 0, 0, 0.3);
}
.u-indicator-item-rect-active {
background-color: rgba(255, 255, 255, 0.8);
}
.u-indicator-item-dot {
width: 14rpx;
height: 14rpx;
margin: 0 6rpx;
border-radius: 20rpx;
transition: all 0.5s;
background-color: rgba(0, 0, 0, 0.3);
}
.u-indicator-item-dot-active {
background-color: rgba(255, 255, 255, 0.8);
}
.u-indicator-item-round {
width: 14rpx;
height: 14rpx;
margin: 0 6rpx;
border-radius: 20rpx;
transition: all 0.5s;
background-color: rgba(0, 0, 0, 0.3);
}
.u-indicator-item-round-active {
width: 34rpx;
background-color: rgba(255, 255, 255, 0.8);
}
.u-indicator-item-number {
padding: 6rpx 16rpx;
line-height: 1;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 100rpx;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.u-list-scale {
transform-origin: center center;
}
.u-list-image-wrap {
width: 100%;
height: 100%;
flex: 1;
transition: all 0.5s;
overflow: hidden;
box-sizing: content-box;
position: relative;
}
.u-swiper-title {
position: absolute;
background-color: rgba(0, 0, 0, 0.3);
bottom: 0;
left: 0;
width: 100%;
font-size: 28rpx;
padding: 12rpx 24rpx;
color: rgba(255, 255, 255, 0.9);
}
.u-swiper-item {
@include vue-flex;
overflow: hidden;
align-items: center;
}
</style>