|
|
@@ -0,0 +1,149 @@
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ class="relative w-full h-full box-border overflow-hidden lvgl-roller"
|
|
|
+ :style="styleMap?.mainStyle"
|
|
|
+ >
|
|
|
+ <ImageBg
|
|
|
+ v-if="styleMap?.mainStyle?.imageSrc"
|
|
|
+ :src="styleMap.mainStyle.imageSrc"
|
|
|
+ :imageStyle="styleMap.mainStyle.imageStyle"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div class="absolute left-0 right-0 pointer-events-none" :style="indicatorInlineStyle">
|
|
|
+ <div class="w-full h-full" :style="styleMap?.selectedStyle">
|
|
|
+ <ImageBg
|
|
|
+ v-if="styleMap?.selectedStyle?.imageSrc"
|
|
|
+ :src="styleMap.selectedStyle.imageSrc"
|
|
|
+ :imageStyle="styleMap.selectedStyle.imageStyle"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="absolute left-0 right-0 top-0 bottom-0">
|
|
|
+ <div
|
|
|
+ class="w-full flex flex-col items-stretch"
|
|
|
+ :style="{
|
|
|
+ transform: `translateY(${offsetY}px)`,
|
|
|
+ transition: 'transform 200ms ease-out'
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in displayOptions"
|
|
|
+ :key="`${index}-${item}`"
|
|
|
+ class="flex h-40px items-center justify-center box-border"
|
|
|
+ >
|
|
|
+ <span
|
|
|
+ class="truncate w-full text-center"
|
|
|
+ :style="index === effectiveSelectedIndex ? selectedTextStyle : undefined"
|
|
|
+ >
|
|
|
+ {{ item }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, type CSSProperties } from 'vue'
|
|
|
+import { useWidgetStyle } from '../hooks/useWidgetStyle'
|
|
|
+import ImageBg from '../ImageBg.vue'
|
|
|
+
|
|
|
+const ITEM_HEIGHT = 40
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ width: number
|
|
|
+ height: number
|
|
|
+ styles: any[]
|
|
|
+ state?: string
|
|
|
+ part?: string
|
|
|
+ options: string[]
|
|
|
+ visibleRowCount?: number
|
|
|
+ selected: number
|
|
|
+ direction: 'infinite' | 'normal'
|
|
|
+}>()
|
|
|
+
|
|
|
+const styleMap = useWidgetStyle({
|
|
|
+ widget: 'lv_roller',
|
|
|
+ props
|
|
|
+})
|
|
|
+
|
|
|
+const normalizedOptions = computed(() => {
|
|
|
+ return Array.isArray(props.options) && props.options.length ? props.options : ['']
|
|
|
+})
|
|
|
+
|
|
|
+const infiniteSideCopies = computed(() => {
|
|
|
+ const optionCount = normalizedOptions.value.length
|
|
|
+ if (props.direction !== 'infinite' || optionCount <= 1) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ const visibleCount = Math.max(1, Math.ceil((props.height || ITEM_HEIGHT) / ITEM_HEIGHT))
|
|
|
+ return Math.max(1, Math.ceil(visibleCount / optionCount) + 1)
|
|
|
+})
|
|
|
+
|
|
|
+const displayOptions = computed(() => {
|
|
|
+ const opts = normalizedOptions.value
|
|
|
+ if (props.direction !== 'infinite' || opts.length <= 1) {
|
|
|
+ return opts
|
|
|
+ }
|
|
|
+
|
|
|
+ return Array.from({ length: infiniteSideCopies.value * 2 + 1 }, () => opts).flat()
|
|
|
+})
|
|
|
+
|
|
|
+const effectiveSelectedIndex = computed(() => {
|
|
|
+ const optionCount = normalizedOptions.value.length
|
|
|
+ const selected = props.selected ?? 0
|
|
|
+
|
|
|
+ if (!optionCount) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ if (props.direction !== 'infinite' || optionCount === 1) {
|
|
|
+ return Math.max(0, Math.min(selected, optionCount - 1))
|
|
|
+ }
|
|
|
+
|
|
|
+ const normalized = ((selected % optionCount) + optionCount) % optionCount
|
|
|
+ return infiniteSideCopies.value * optionCount + normalized
|
|
|
+})
|
|
|
+
|
|
|
+const centerTop = computed(() => {
|
|
|
+ return (props.height || ITEM_HEIGHT) / 2 - ITEM_HEIGHT / 2
|
|
|
+})
|
|
|
+
|
|
|
+const selectedTextStyle = computed<CSSProperties>(() => {
|
|
|
+ const selectedStyle = styleMap.value?.selectedStyle
|
|
|
+ if (!selectedStyle) {
|
|
|
+ return {}
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ color: selectedStyle.color,
|
|
|
+ fontSize: selectedStyle.fontSize,
|
|
|
+ fontFamily: selectedStyle.fontFamily,
|
|
|
+ fontStyle: selectedStyle.fontStyle,
|
|
|
+ fontWeight: selectedStyle.fontWeight,
|
|
|
+ textAlign: selectedStyle.textAlign,
|
|
|
+ textDecoration: selectedStyle.textDecoration,
|
|
|
+ letterSpacing: selectedStyle.letterSpacing,
|
|
|
+ lineHeight: selectedStyle.lineHeight
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const offsetY = computed(() => {
|
|
|
+ return centerTop.value - effectiveSelectedIndex.value * ITEM_HEIGHT
|
|
|
+})
|
|
|
+
|
|
|
+const indicatorInlineStyle = computed(() => {
|
|
|
+ return {
|
|
|
+ top: `${centerTop.value}px`,
|
|
|
+ height: `${ITEM_HEIGHT}px`
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.lvgl-roller {
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+</style>
|