Merge pull request #2165 from zhangdeyu/feature/support-select-search
feat: Select 和 GroupSelector 组件支持自动搜索
This commit is contained in:
commit
a1106e8167
@ -5,7 +5,24 @@
|
|||||||
<span class="font-normal text-gray-400">{{ t('common.selectedCount', { count: modelValue.length }) }}</span>
|
<span class="font-normal text-gray-400">{{ t('common.selectedCount', { count: modelValue.length }) }}</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class="grid max-h-32 grid-cols-2 gap-1 overflow-y-auto rounded-lg border border-gray-200 bg-gray-50 p-2 dark:border-dark-600 dark:bg-dark-800"
|
v-if="isSearchable"
|
||||||
|
class="flex items-center gap-2 rounded-t-lg border border-b-0 border-gray-200 bg-gray-50 px-3 py-2 dark:border-dark-600 dark:bg-dark-800"
|
||||||
|
>
|
||||||
|
<Icon name="search" size="sm" class="shrink-0 text-gray-400" />
|
||||||
|
<input
|
||||||
|
v-model="searchText"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('common.searchPlaceholder')"
|
||||||
|
class="flex-1 bg-transparent text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none dark:text-gray-100 dark:placeholder:text-dark-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'grid max-h-32 grid-cols-2 gap-1 overflow-y-auto p-2',
|
||||||
|
isSearchable
|
||||||
|
? 'rounded-b-lg border border-t-0 border-gray-200 bg-gray-50 dark:border-dark-600 dark:bg-dark-800'
|
||||||
|
: 'rounded-lg border border-gray-200 bg-gray-50 dark:border-dark-600 dark:bg-dark-800'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-for="group in filteredGroups"
|
v-for="group in filteredGroups"
|
||||||
@ -40,9 +57,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import GroupBadge from './GroupBadge.vue'
|
import GroupBadge from './GroupBadge.vue'
|
||||||
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
import type { AdminGroup, GroupPlatform } from '@/types'
|
import type { AdminGroup, GroupPlatform } from '@/types'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@ -52,26 +70,44 @@ interface Props {
|
|||||||
groups: AdminGroup[]
|
groups: AdminGroup[]
|
||||||
platform?: GroupPlatform // Optional platform filter
|
platform?: GroupPlatform // Optional platform filter
|
||||||
mixedScheduling?: boolean // For antigravity accounts: allow anthropic/gemini groups
|
mixedScheduling?: boolean // For antigravity accounts: allow anthropic/gemini groups
|
||||||
|
searchable?: boolean | 'auto'
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
searchable: 'auto'
|
||||||
|
})
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: number[]]
|
'update:modelValue': [value: number[]]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const searchText = ref('')
|
||||||
|
|
||||||
|
const isSearchable = computed(() => {
|
||||||
|
if (props.searchable === 'auto') return props.groups.length > 5
|
||||||
|
return props.searchable
|
||||||
|
})
|
||||||
|
|
||||||
// Filter groups by platform if specified
|
// Filter groups by platform if specified
|
||||||
const filteredGroups = computed(() => {
|
const filteredGroups = computed(() => {
|
||||||
if (!props.platform) {
|
let result: AdminGroup[] = props.groups
|
||||||
return props.groups
|
if (props.platform) {
|
||||||
|
// antigravity 账户启用混合调度后,可选择 anthropic/gemini 分组
|
||||||
|
if (props.platform === 'antigravity' && props.mixedScheduling) {
|
||||||
|
result = result.filter(
|
||||||
|
(g) => g.platform === 'antigravity' || g.platform === 'anthropic' || g.platform === 'gemini'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 默认:只能选择同 platform 的分组
|
||||||
|
result = result.filter((g) => g.platform === props.platform)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// antigravity 账户启用混合调度后,可选择 anthropic/gemini 分组
|
if (isSearchable.value && searchText.value) {
|
||||||
if (props.platform === 'antigravity' && props.mixedScheduling) {
|
const q = searchText.value.toLowerCase()
|
||||||
return props.groups.filter(
|
result = result.filter(
|
||||||
(g) => g.platform === 'antigravity' || g.platform === 'anthropic' || g.platform === 'gemini'
|
(g) => g.name.toLowerCase().includes(q) || g.description?.toLowerCase().includes(q)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// 默认:只能选择同 platform 的分组
|
return result
|
||||||
return props.groups.filter((g) => g.platform === props.platform)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleChange = (groupId: number, checked: boolean) => {
|
const handleChange = (groupId: number, checked: boolean) => {
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
@keydown="onDropdownKeyDown"
|
@keydown="onDropdownKeyDown"
|
||||||
>
|
>
|
||||||
<!-- Search input -->
|
<!-- Search input -->
|
||||||
<div v-if="searchable" class="select-search">
|
<div v-if="isSearchable" class="select-search">
|
||||||
<Icon name="search" size="sm" class="text-gray-400" />
|
<Icon name="search" size="sm" class="text-gray-400" />
|
||||||
<input
|
<input
|
||||||
ref="searchInputRef"
|
ref="searchInputRef"
|
||||||
@ -128,7 +128,7 @@ interface Props {
|
|||||||
placeholder?: string
|
placeholder?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
error?: boolean
|
error?: boolean
|
||||||
searchable?: boolean
|
searchable?: boolean | 'auto'
|
||||||
searchPlaceholder?: string
|
searchPlaceholder?: string
|
||||||
emptyText?: string
|
emptyText?: string
|
||||||
valueKey?: string
|
valueKey?: string
|
||||||
@ -145,7 +145,7 @@ interface Emits {
|
|||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
error: false,
|
error: false,
|
||||||
searchable: false,
|
searchable: 'auto',
|
||||||
creatable: false,
|
creatable: false,
|
||||||
creatablePrefix: '',
|
creatablePrefix: '',
|
||||||
valueKey: 'value',
|
valueKey: 'value',
|
||||||
@ -170,6 +170,11 @@ const placeholderText = computed(() => props.placeholder ?? t('common.selectOpti
|
|||||||
const searchPlaceholderText = computed(() => props.searchPlaceholder ?? t('common.searchPlaceholder'))
|
const searchPlaceholderText = computed(() => props.searchPlaceholder ?? t('common.searchPlaceholder'))
|
||||||
const emptyTextDisplay = computed(() => props.emptyText ?? t('common.noOptionsFound'))
|
const emptyTextDisplay = computed(() => props.emptyText ?? t('common.noOptionsFound'))
|
||||||
|
|
||||||
|
const isSearchable = computed(() => {
|
||||||
|
if (props.searchable === 'auto') return props.options.length > 5
|
||||||
|
return props.searchable
|
||||||
|
})
|
||||||
|
|
||||||
// Computed style for teleported dropdown
|
// Computed style for teleported dropdown
|
||||||
const dropdownStyle = computed(() => {
|
const dropdownStyle = computed(() => {
|
||||||
if (!triggerRect.value) return {}
|
if (!triggerRect.value) return {}
|
||||||
@ -236,7 +241,7 @@ const selectedLabel = computed(() => {
|
|||||||
|
|
||||||
const filteredOptions = computed(() => {
|
const filteredOptions = computed(() => {
|
||||||
let opts = props.options as any[]
|
let opts = props.options as any[]
|
||||||
if (props.searchable && searchQuery.value) {
|
if (isSearchable.value && searchQuery.value) {
|
||||||
const query = searchQuery.value.toLowerCase()
|
const query = searchQuery.value.toLowerCase()
|
||||||
opts = opts.filter((opt) => {
|
opts = opts.filter((opt) => {
|
||||||
// Match label
|
// Match label
|
||||||
@ -328,7 +333,7 @@ watch(isOpen, (open) => {
|
|||||||
: initialIdx
|
: initialIdx
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.searchable) {
|
if (isSearchable.value) {
|
||||||
nextTick(() => searchInputRef.value?.focus())
|
nextTick(() => searchInputRef.value?.focus())
|
||||||
}
|
}
|
||||||
// Add scroll listener to update position
|
// Add scroll listener to update position
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user