| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- <template>
- <view ref="marqueeRef" class="cl-marquee" :class="[pt.className]">
- <view
- class="cl-marquee__list"
- :class="[
- pt.list?.className,
- {
- 'is-vertical': direction == 'vertical',
- 'is-horizontal': direction == 'horizontal'
- }
- ]"
- :style="listStyle"
- >
- <!-- 渲染两份图片列表实现无缝滚动 -->
- <view
- class="cl-marquee__item"
- v-for="(item, index) in duplicatedList"
- :key="`${item.url}-${index}`"
- :class="[pt.item?.className]"
- :style="itemStyle"
- >
- <slot name="item" :item="item" :index="item.originalIndex">
- <image
- :src="item.url"
- mode="aspectFill"
- class="cl-marquee__image"
- :class="[pt.image?.className]"
- ></image>
- </slot>
- </view>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- import { computed, ref, onMounted, onUnmounted, type PropType, watch } from "vue";
- import { AnimationEngine, createAnimation, getPx, parsePt } from "@/cool";
- import type { PassThroughProps } from "../../types";
- type MarqueeItem = {
- url: string;
- originalIndex: number;
- };
- defineOptions({
- name: "cl-marquee"
- });
- defineSlots<{
- item(props: { item: MarqueeItem; index: number }): any;
- }>();
- const props = defineProps({
- // 透传属性
- pt: {
- type: Object,
- default: () => ({})
- },
- // 图片列表
- list: {
- type: Array as PropType<string[]>,
- default: () => []
- },
- // 滚动方向
- direction: {
- type: String as PropType<"horizontal" | "vertical">,
- default: "horizontal"
- },
- // 一次滚动的持续时间
- duration: {
- type: Number,
- default: 5000
- },
- // 图片高度
- itemHeight: {
- type: [Number, String],
- default: 200
- },
- // 图片宽度 (仅横向滚动时生效,纵向为100%)
- itemWidth: {
- type: [Number, String],
- default: 300
- },
- // 间距
- gap: {
- type: [Number, String],
- default: 20
- }
- });
- // 透传属性类型定义
- type PassThrough = {
- className?: string;
- list?: PassThroughProps;
- item?: PassThroughProps;
- image?: PassThroughProps;
- };
- const pt = computed(() => parsePt<PassThrough>(props.pt));
- /** 跑马灯引用 */
- const marqueeRef = ref<UniElement | null>(null);
- /** 当前偏移量 */
- const currentOffset = ref(0);
- /** 重复的图片列表(用于无缝滚动) */
- const duplicatedList = computed<MarqueeItem[]>(() => {
- if (props.list.length == 0) return [];
- const originalItems = props.list.map(
- (url, index) =>
- ({
- url,
- originalIndex: index
- }) as MarqueeItem
- );
- // 复制一份用于无缝滚动
- const duplicatedItems = props.list.map(
- (url, index) =>
- ({
- url,
- originalIndex: index
- }) as MarqueeItem
- );
- return [...originalItems, ...duplicatedItems] as MarqueeItem[];
- });
- /** 容器样式 */
- const listStyle = computed(() => {
- const isVertical = props.direction == "vertical";
- return {
- transform: isVertical
- ? `translateY(${currentOffset.value}px)`
- : `translateX(${currentOffset.value}px)`
- };
- });
- /** 图片项样式 */
- const itemStyle = computed(() => {
- const style = {};
- const gap = `${getPx(props.gap)}px`;
- if (props.direction == "vertical") {
- style["height"] = `${getPx(props.itemHeight)}px`;
- style["marginBottom"] = gap;
- } else {
- style["width"] = `${getPx(props.itemWidth)}px`;
- style["marginRight"] = gap;
- }
- return style;
- });
- /** 单个项目的尺寸(包含间距) */
- const itemSize = computed(() => {
- const size = props.direction == "vertical" ? props.itemHeight : props.itemWidth;
- return getPx(size) + getPx(props.gap);
- });
- /** 总的滚动距离 */
- const totalScrollDistance = computed(() => {
- return props.list.length * itemSize.value;
- });
- /** 动画实例 */
- let animation: AnimationEngine | null = null;
- /**
- * 开始动画
- */
- function start() {
- if (props.list.length <= 1) return;
- animation = createAnimation(marqueeRef.value, {
- duration: props.duration,
- timingFunction: "linear",
- loop: -1,
- frame: (progress: number) => {
- currentOffset.value = -progress * totalScrollDistance.value;
- }
- });
- animation!.play();
- }
- /**
- * 播放动画
- */
- function play() {
- if (animation != null) {
- animation!.play();
- }
- }
- /**
- * 暂停动画
- */
- function pause() {
- if (animation != null) {
- animation!.pause();
- }
- }
- /**
- * 停止动画
- */
- function stop() {
- if (animation != null) {
- animation!.stop();
- }
- }
- /**
- * 重置动画
- */
- function reset() {
- currentOffset.value = 0;
- if (animation != null) {
- animation!.stop();
- animation!.reset();
- }
- }
- onMounted(() => {
- setTimeout(() => {
- start();
- }, 300);
- watch(
- computed(() => [props.duration, props.itemHeight, props.itemWidth, props.gap, props.list]),
- () => {
- reset();
- start();
- }
- );
- });
- onUnmounted(() => {
- stop();
- });
- defineExpose({
- start,
- stop,
- reset,
- pause,
- play
- });
- </script>
- <style lang="scss" scoped>
- .cl-marquee {
- @apply relative;
- &__list {
- @apply flex h-full overflow-visible;
- &.is-horizontal {
- @apply flex-row;
- }
- &.is-vertical {
- @apply flex-col;
- }
- }
- &__item {
- @apply relative h-full;
- }
- &__image {
- @apply w-full h-full rounded-xl;
- }
- }
- </style>
|