添加 cl-marquee 跑马灯组件
This commit is contained in:
283
uni_modules/cool-ui/components/cl-marquee/cl-marquee.uvue
Normal file
283
uni_modules/cool-ui/components/cl-marquee/cl-marquee.uvue
Normal file
@@ -0,0 +1,283 @@
|
||||
<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
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["item-click"]);
|
||||
|
||||
// 透传属性类型定义
|
||||
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>
|
||||
19
uni_modules/cool-ui/components/cl-marquee/props.ts
Normal file
19
uni_modules/cool-ui/components/cl-marquee/props.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { PassThroughProps } from "../../types";
|
||||
|
||||
export type ClMarqueePassThrough = {
|
||||
className?: string;
|
||||
list?: PassThroughProps;
|
||||
item?: PassThroughProps;
|
||||
image?: PassThroughProps;
|
||||
};
|
||||
|
||||
export type ClMarqueeProps = {
|
||||
className?: string;
|
||||
pt?: ClMarqueePassThrough;
|
||||
list?: string[];
|
||||
direction?: "horizontal" | "vertical";
|
||||
duration?: number;
|
||||
itemHeight?: any;
|
||||
itemWidth?: any;
|
||||
gap?: any;
|
||||
};
|
||||
2
uni_modules/cool-ui/index.d.ts
vendored
2
uni_modules/cool-ui/index.d.ts
vendored
@@ -38,6 +38,7 @@ import type { ClListItemProps, ClListItemPassThrough } from "./components/cl-lis
|
||||
import type { ClListViewProps, ClListViewPassThrough } from "./components/cl-list-view/props";
|
||||
import type { ClLoadingProps, ClLoadingPassThrough } from "./components/cl-loading/props";
|
||||
import type { ClLoadmoreProps, ClLoadmorePassThrough } from "./components/cl-loadmore/props";
|
||||
import type { ClMarqueeProps, ClMarqueePassThrough } from "./components/cl-marquee/props";
|
||||
import type { ClNoticebarProps, ClNoticebarPassThrough } from "./components/cl-noticebar/props";
|
||||
import type { ClPageProps } from "./components/cl-page/props";
|
||||
import type { ClPageThemeProps } from "./components/cl-page-theme/props";
|
||||
@@ -116,6 +117,7 @@ declare module "vue" {
|
||||
"cl-list-view": (typeof import('./components/cl-list-view/cl-list-view.uvue')['default']) & import('vue').DefineComponent<ClListViewProps>;
|
||||
"cl-loading": (typeof import('./components/cl-loading/cl-loading.uvue')['default']) & import('vue').DefineComponent<ClLoadingProps>;
|
||||
"cl-loadmore": (typeof import('./components/cl-loadmore/cl-loadmore.uvue')['default']) & import('vue').DefineComponent<ClLoadmoreProps>;
|
||||
"cl-marquee": (typeof import('./components/cl-marquee/cl-marquee.uvue')['default']) & import('vue').DefineComponent<ClMarqueeProps>;
|
||||
"cl-noticebar": (typeof import('./components/cl-noticebar/cl-noticebar.uvue')['default']) & import('vue').DefineComponent<ClNoticebarProps>;
|
||||
"cl-page": (typeof import('./components/cl-page/cl-page.uvue')['default']) & import('vue').DefineComponent<ClPageProps>;
|
||||
"cl-page-theme": (typeof import('./components/cl-page-theme/cl-page-theme.uvue')['default']) & import('vue').DefineComponent<ClPageThemeProps>;
|
||||
|
||||
8
uni_modules/cool-ui/types/component.d.ts
vendored
8
uni_modules/cool-ui/types/component.d.ts
vendored
@@ -230,3 +230,11 @@ declare type ClCalendarComponentPublicInstance = {
|
||||
open(cb: ((value: string | string[]) => void) | null = null): void;
|
||||
close(): void;
|
||||
};
|
||||
|
||||
declare type ClMarqueeComponentPublicInstance = {
|
||||
play(): void;
|
||||
pause(): void;
|
||||
start(): void;
|
||||
stop(): void;
|
||||
reset(): void;
|
||||
};
|
||||
|
||||
@@ -210,3 +210,30 @@ export type ClCalendarDateConfig = {
|
||||
disabled?: boolean;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export type ClMarqueeDirection = "horizontal" | "vertical";
|
||||
|
||||
export type ClMarqueeItem = {
|
||||
url: string;
|
||||
originalIndex: number;
|
||||
};
|
||||
|
||||
export type ClMarqueePassThrough = {
|
||||
className?: string;
|
||||
container?: PassThroughProps;
|
||||
item?: PassThroughProps;
|
||||
image?: PassThroughProps;
|
||||
};
|
||||
|
||||
export type ClMarqueeProps = {
|
||||
className?: string;
|
||||
pt?: ClMarqueePassThrough;
|
||||
list?: string[];
|
||||
direction?: ClMarqueeDirection;
|
||||
speed?: number;
|
||||
pause?: boolean;
|
||||
pauseOnHover?: boolean;
|
||||
itemHeight?: number;
|
||||
itemWidth?: number;
|
||||
gap?: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user