2025-09-23 14:18:04 +02:00
|
|
|
<template>
|
2025-10-03 16:51:44 +02:00
|
|
|
<div class="o-select" @click="clickSelect" ref="componentRef">
|
|
|
|
|
<FInput
|
|
|
|
|
hide-details
|
|
|
|
|
:clearable="props.clearable"
|
|
|
|
|
:label="props.label ?? undefined"
|
|
|
|
|
:selectedable="false"
|
|
|
|
|
:focus="showList"
|
2025-10-07 19:26:08 +02:00
|
|
|
:modelValue="`${selectedYear} % yearly`"
|
2025-10-03 16:51:44 +02:00
|
|
|
readonly
|
|
|
|
|
>
|
|
|
|
|
<template #info v-if="slots.info">
|
|
|
|
|
<slot name="info"></slot>
|
|
|
|
|
</template>
|
|
|
|
|
<template #suffix>
|
|
|
|
|
<Icon class="toggle-collapse" v-if="showList" icon="mdi:chevron-down"></Icon>
|
|
|
|
|
<Icon class="toggle-collapse" v-else icon="mdi:chevron-up"></Icon>
|
|
|
|
|
</template>
|
|
|
|
|
</FInput>
|
|
|
|
|
<div class="select-list-wrapper" v-show="showList" ref="selectList">
|
|
|
|
|
<div class="select-list-inner" @click.stop>
|
|
|
|
|
<div
|
|
|
|
|
class="select-list-item"
|
|
|
|
|
v-for="(item, index) in props.items"
|
2025-10-07 19:26:08 +02:00
|
|
|
:key="item.index"
|
|
|
|
|
:class="{ active: selectedIndex === item.index, hovered: activeIndex === index }"
|
2025-10-03 16:51:44 +02:00
|
|
|
@click.stop="clickItem(item)"
|
|
|
|
|
@mouseenter="mouseEnter($event, index)"
|
|
|
|
|
@mouseleave="mouseLeave($event, index)"
|
|
|
|
|
>
|
|
|
|
|
<div class="circle">
|
2025-10-07 19:26:08 +02:00
|
|
|
<div class="active" v-if="selectedIndex === item.index"></div>
|
2025-10-03 16:51:44 +02:00
|
|
|
<div class="hovered" v-else-if="activeIndex === index"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="yearly">
|
|
|
|
|
<div class="value">{{ item.year }} %</div>
|
|
|
|
|
<div class="label">yearly</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="daily">
|
|
|
|
|
<div class="value">{{ item.daily.toFixed(4) }} %</div>
|
|
|
|
|
<div class="label">daily</div>
|
|
|
|
|
</div>
|
2025-09-23 14:18:04 +02:00
|
|
|
</div>
|
2025-10-03 16:51:44 +02:00
|
|
|
</div>
|
2025-09-23 14:18:04 +02:00
|
|
|
</div>
|
2025-10-03 16:51:44 +02:00
|
|
|
</div>
|
|
|
|
|
<slot :style="[!props.editor ? { display: 'none' } : {}]"></slot>
|
2025-09-23 14:18:04 +02:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-10-03 16:51:44 +02:00
|
|
|
import { ref, computed, onMounted, useSlots, withDefaults } from 'vue';
|
|
|
|
|
import FInput from '@/components/fcomponents/FInput.vue';
|
|
|
|
|
import useClickOutside from '@/composables/useClickOutside';
|
|
|
|
|
import { Icon } from '@iconify/vue';
|
2025-09-23 14:18:04 +02:00
|
|
|
|
|
|
|
|
interface Item {
|
2025-10-07 19:26:08 +02:00
|
|
|
index: number;
|
2025-10-03 16:51:44 +02:00
|
|
|
year: number;
|
|
|
|
|
daily: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
items?: Item[];
|
|
|
|
|
clearable?: boolean;
|
|
|
|
|
itemTitle?: string;
|
|
|
|
|
itemValue?: string;
|
|
|
|
|
label?: string | null;
|
|
|
|
|
modelValue?: number | null;
|
|
|
|
|
value?: string | null;
|
|
|
|
|
editor?: boolean;
|
2025-09-23 14:18:04 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-03 16:51:44 +02:00
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
items: () => [],
|
|
|
|
|
clearable: false,
|
|
|
|
|
itemTitle: 'label',
|
|
|
|
|
itemValue: 'value',
|
|
|
|
|
label: null,
|
|
|
|
|
modelValue: null,
|
|
|
|
|
value: null,
|
|
|
|
|
editor: false,
|
2025-09-23 14:18:04 +02:00
|
|
|
});
|
|
|
|
|
|
2025-10-03 16:51:44 +02:00
|
|
|
interface Emits {
|
|
|
|
|
(e: 'update:modelValue', value: number): void;
|
|
|
|
|
}
|
|
|
|
|
const emit = defineEmits<Emits>();
|
2025-09-23 14:18:04 +02:00
|
|
|
|
2025-10-03 16:51:44 +02:00
|
|
|
const slots = useSlots();
|
2025-09-23 14:18:04 +02:00
|
|
|
|
2025-10-03 16:51:44 +02:00
|
|
|
const showList = ref<boolean>(false);
|
|
|
|
|
const _slotElements = ref<unknown[]>([]);
|
|
|
|
|
const componentRef = ref<HTMLElement | null>(null);
|
|
|
|
|
const selectList = ref<HTMLElement | null>(null);
|
|
|
|
|
const activeIndex = ref();
|
2025-09-23 14:18:04 +02:00
|
|
|
|
2025-10-03 16:51:44 +02:00
|
|
|
useClickOutside(componentRef, () => {
|
|
|
|
|
showList.value = false;
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-07 19:26:08 +02:00
|
|
|
const selectedIndex = computed({
|
2025-09-23 14:18:04 +02:00
|
|
|
get() {
|
2025-10-07 19:26:08 +02:00
|
|
|
if (typeof props.modelValue === 'number') {
|
|
|
|
|
return props.modelValue;
|
|
|
|
|
}
|
|
|
|
|
return props.items[0]?.index ?? 0;
|
2025-09-23 14:18:04 +02:00
|
|
|
},
|
|
|
|
|
set(newValue: number) {
|
2025-10-03 16:51:44 +02:00
|
|
|
emit('update:modelValue', newValue);
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-09-23 14:18:04 +02:00
|
|
|
|
2025-10-07 19:26:08 +02:00
|
|
|
const selectedOption = computed(() => props.items.find(item => item.index === selectedIndex.value) ?? props.items[0]);
|
|
|
|
|
const selectedYear = computed(() => selectedOption.value?.year ?? 0);
|
|
|
|
|
|
2025-10-03 16:51:44 +02:00
|
|
|
function mouseEnter(event: MouseEvent, index: number) {
|
|
|
|
|
const target = event.target as HTMLElement;
|
|
|
|
|
activeIndex.value = index;
|
|
|
|
|
target.classList.add('active');
|
2025-09-23 14:18:04 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-03 16:51:44 +02:00
|
|
|
function mouseLeave(_event: MouseEvent, _index: number) {
|
|
|
|
|
const elements = selectList.value?.querySelectorAll('.select-list-item');
|
|
|
|
|
elements?.forEach(element => {
|
|
|
|
|
(element as HTMLElement).classList.remove('active');
|
|
|
|
|
});
|
2025-09-23 14:18:04 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-03 16:51:44 +02:00
|
|
|
onMounted(() => {});
|
2025-09-23 14:18:04 +02:00
|
|
|
|
2025-10-03 16:51:44 +02:00
|
|
|
function clickSelect(_event: unknown) {
|
|
|
|
|
showList.value = !showList.value;
|
2025-09-23 14:18:04 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-07 19:26:08 +02:00
|
|
|
function clickItem(item: { index: number }) {
|
|
|
|
|
selectedIndex.value = item.index;
|
2025-10-03 16:51:44 +02:00
|
|
|
showList.value = false;
|
|
|
|
|
// emit('input', item)
|
2025-09-23 14:18:04 +02:00
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="sass">
|
|
|
|
|
.o-select
|
|
|
|
|
position: relative
|
|
|
|
|
display: inline-block
|
|
|
|
|
.toggle-collapse
|
|
|
|
|
color: black
|
|
|
|
|
font-size: 20px
|
|
|
|
|
&:active, &:focus, &:hover
|
|
|
|
|
cursor: pointer
|
|
|
|
|
.select-list-wrapper
|
|
|
|
|
position: absolute
|
|
|
|
|
box-shadow: 0 1px 6px 0 #20212447
|
|
|
|
|
background-color: var(--color-card-bg)
|
|
|
|
|
z-index: 10
|
|
|
|
|
padding: 16px
|
|
|
|
|
border-radius: 16px
|
|
|
|
|
border: 1px solid black
|
|
|
|
|
right: 0
|
|
|
|
|
margin-top: 4px
|
|
|
|
|
.select-list-inner
|
|
|
|
|
height: 330px
|
|
|
|
|
overflow-y: scroll
|
|
|
|
|
display: grid
|
|
|
|
|
.select-list-item
|
|
|
|
|
display: grid
|
|
|
|
|
grid-template-columns: 15px 110px 126px
|
|
|
|
|
place-content: end
|
|
|
|
|
margin-right: 8px
|
|
|
|
|
-webkit-touch-callout: none
|
|
|
|
|
-webkit-user-select: none
|
|
|
|
|
-khtml-user-select: none
|
|
|
|
|
-moz-user-select: none
|
|
|
|
|
-ms-user-select: none
|
|
|
|
|
user-select: none
|
|
|
|
|
padding: 4px 0
|
|
|
|
|
&:active, &:focus, &:hover
|
|
|
|
|
cursor: pointer
|
|
|
|
|
.daily, .yearly
|
|
|
|
|
display: flex
|
|
|
|
|
gap: 4px
|
|
|
|
|
justify-content: end
|
|
|
|
|
.daily
|
|
|
|
|
color: var(--color-grey)
|
|
|
|
|
.circle
|
|
|
|
|
height: 14px
|
|
|
|
|
width: 14px
|
|
|
|
|
.active
|
|
|
|
|
height: 14px
|
|
|
|
|
width: 14px
|
|
|
|
|
background-color: var(--color-based-blue)
|
|
|
|
|
border-radius: 14px
|
|
|
|
|
.hovered
|
|
|
|
|
height: 14px
|
|
|
|
|
width: 14px
|
|
|
|
|
background-color: var(--color-based-blue)
|
|
|
|
|
opacity: .5
|
|
|
|
|
border-radius: 14px
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.select-list-inner
|
|
|
|
|
&::-webkit-scrollbar-track
|
|
|
|
|
border-radius: 10px
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar
|
|
|
|
|
width: 5px
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-thumb
|
|
|
|
|
border-radius: 10px
|
|
|
|
|
background-color: lightgrey
|
2025-10-03 16:51:44 +02:00
|
|
|
</style>
|