Files
Auto_Bangumi/webui/src/components/basic/ab-select.vue
Estrella Pan cba4988e52 perf: comprehensive performance optimization for backend and frontend
Backend: shared HTTP connection pool, concurrent RSS/torrent/notification
operations, TMDB/Mikan result caching, database indexes, pre-compiled
regex, __slots__ on dataclasses, O(1) set-based dedup, frozenset lookups,
batch RSS enable/disable, asyncio.to_thread for blocking calls.

Frontend: shallowRef for large arrays, computed table columns, watch
instead of watchEffect, scoped style fix, typed emits, noImplicitAny,
useIntervalFn lifecycle management, shared useClipboard instance,
shallow clone for shared template objects.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 20:46:45 +01:00

169 lines
3.6 KiB
Vue

<script lang="ts" setup>
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
} from '@headlessui/vue';
import { Down, Up } from '@icon-park/vue-next';
import { isObject, isString } from 'radash';
import type { SelectItem } from '#/components';
const props = withDefaults(
defineProps<{
modelValue?: SelectItem | string;
items: Array<SelectItem | string>;
}>(),
{}
);
const emit = defineEmits(['update:modelValue']);
const selected = ref<SelectItem | string>(
props.modelValue || (props.items?.[0] ?? '')
);
const otherItems = computed(() => {
return (
props.items.filter((e) => {
if (isString(e) && isString(selected.value)) {
return e !== selected.value;
} else if (isObject(e) && isObject(selected.value)) {
return e.id !== selected.value.id;
} else {
return false;
}
}) ?? []
);
});
const label = computed(() => {
if (isString(selected.value)) {
return selected.value;
} else {
return selected.value.label ?? selected.value.value;
}
});
function getLabel(item: SelectItem | string) {
if (isString(item)) {
return item;
} else {
return item.label ?? item.value;
}
}
function getDisabled(item: SelectItem | string) {
return isString(item) ? false : item.disabled;
}
watch(selected, (val) => {
emit('update:modelValue', val);
});
</script>
<template>
<Listbox v-slot="{ open }" v-model="selected">
<div class="select-wrapper">
<ListboxButton class="select-button">
<div class="select-value">{{ label }}</div>
<div :class="[{ hidden: open }]">
<Down :size="14" />
</div>
</ListboxButton>
<ListboxOptions class="select-options">
<div class="select-options-inner">
<div class="select-options-list">
<ListboxOption
v-for="item in otherItems"
v-slot="{ active }"
:key="isString(item) ? item : item.id"
:value="item"
:disabled="getDisabled(item)"
>
<div
class="select-option"
:class="[
active && 'select-option--active',
getDisabled(item) && 'select-option--disabled',
]"
>
{{ getLabel(item) }}
</div>
</ListboxOption>
</div>
<div :class="[{ hidden: !open }]"><Up :size="14" /></div>
</div>
</ListboxOptions>
</div>
</Listbox>
</template>
<style lang="scss" scoped>
.select-wrapper {
position: relative;
display: inline-flex;
flex-direction: column;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
font-size: 12px;
padding: 4px 12px;
transition: border-color var(--transition-fast);
&:hover {
border-color: var(--color-primary);
}
}
.select-button {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
background: transparent;
border: none;
cursor: pointer;
color: var(--color-text);
padding: 0;
}
.select-value {
color: var(--color-text);
}
.select-options {
margin-top: 8px;
}
.select-options-inner {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
}
.select-options-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.select-option {
cursor: pointer;
user-select: none;
color: var(--color-text-secondary);
transition: color var(--transition-fast);
&--active {
color: var(--color-primary);
}
&--disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
</style>