Files
2026-03-13 10:18:07 +08:00

293 lines
6.3 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="multi-select-search">
<view class="input-wrapper" @click.stop="toggleDropdown">
<view class="selected-tags" v-if="selectedItems.length > 0">
<view class="tag-item" v-for="(item, index) in selectedItems" :key="index">
<text class="tag-text">{{item.model}}</text>
<view class="tag-close" @click.stop="removeItem(index)">
<u-icon name="close" size="14" color="#999"></u-icon>
</view>
</view>
</view>
<input
class="search-input"
type="text"
v-model="searchText"
:placeholder="selectedItems.length > 0 ? '' : placeholder"
@input="onSearchInput"
@focus="onFocus"
@blur="onBlur"
@confirm="onConfirm"
/>
<view class="input-icon" @click.stop="toggleDropdown">
<u-icon :name="showDropdown ? 'arrow-up' : 'arrow-down'" size="16" color="#999"></u-icon>
</view>
</view>
<view class="dropdown" v-if="showDropdown" @click.stop>
<scroll-view scroll-y="true" class="dropdown-scroll">
<view
class="dropdown-item"
v-for="(item, index) in filteredOptions"
:key="index"
@click="selectItem(item)"
>
<view class="item-content">
<text>{{ item.model }}</text>
<u-icon v-if="isSelected(item)" name="checkmark" size="16" color="#206ff4"></u-icon>
</view>
</view>
<view class="dropdown-empty" v-if="filteredOptions.length === 0 && searchText">
<text class="empty-text">未找到匹配项,按回车键添加 {{ searchText }}</text>
</view>
<view class="dropdown-empty" v-if="filteredOptions.length === 0 && !searchText && options.length === 0">
<text class="empty-text">暂无选项,可直接输入型号</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
name: 'MultiSelectSearch',
props: {
value: {
type: Array,
default: () => []
},
options: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: '请输入型号'
}
},
data() {
return {
showDropdown: false,
searchText: '',
selectedItems: []
}
},
watch: {
value: {
handler(newVal) {
if (Array.isArray(newVal)) {
this.selectedItems = [...newVal]
}
},
immediate: true
}
},
computed: {
filteredOptions() {
// 先过滤掉已选中的项
const unselectedOptions = this.options.filter(item => !this.isSelected(item))
if (!this.searchText || !this.searchText.trim()) {
return unselectedOptions
}
const searchLower = this.searchText.trim().toLowerCase()
return unselectedOptions.filter(item => {
// 支持对象和字符串两种格式
const itemText = (typeof item === 'object' && item !== null && item.model)
? item.model
: String(item)
return itemText.toLowerCase().includes(searchLower)
})
}
},
methods: {
toggleDropdown() {
this.showDropdown = !this.showDropdown
},
onFocus() {
this.showDropdown = true
},
onBlur() {
setTimeout(() => {
this.showDropdown = false
}, 200)
},
onSearchInput() {
this.showDropdown = true
},
onConfirm() {
if (this.searchText && this.searchText.trim()) {
const trimmedText = this.searchText.trim()
// 如果用户直接输入,创建一个简单的对象或字符串
// 根据 options 的格式决定
const newItem = this.options.length > 0 && typeof this.options[0] === 'object'
? { model: trimmedText } // 如果 options 是对象数组,创建对象
: trimmedText // 否则使用字符串
if (!this.isSelected(newItem)) {
this.addItem(newItem)
this.searchText = ''
this.showDropdown = false
}
}
},
selectItem(item) {
this.addItem(item)
this.searchText = ''
this.showDropdown = false
},
addItem(item) {
if (!this.isSelected(item)) {
this.selectedItems.push(item)
this.$emit('input', [...this.selectedItems])
this.$emit('change', [...this.selectedItems])
}
},
removeItem(index) {
this.selectedItems.splice(index, 1)
this.$emit('input', [...this.selectedItems])
this.$emit('change', [...this.selectedItems])
},
isSelected(item) {
// 支持对象和字符串两种格式的比较
if (typeof item === 'object' && item !== null) {
// 如果是对象,比较 id 或 model
const itemId = item.id || item.model
return this.selectedItems.some(selected => {
if (typeof selected === 'object' && selected !== null) {
return (selected.id || selected.model) === itemId
}
return selected === itemId
})
} else {
// 如果是字符串,直接比较
return this.selectedItems.some(selected => {
if (typeof selected === 'object' && selected !== null) {
return (selected.id || selected.model) === item
}
return selected === item
})
}
}
}
}
</script>
<style lang="scss" scoped>
.multi-select-search {
position: relative;
width: 100%;
}
.input-wrapper {
position: relative;
min-height: 88rpx;
padding: 20rpx 80rpx 20rpx 24rpx;
border: 1px solid #DCDFE6;
border-radius: 12rpx;
background: #fff;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16rpx;
box-sizing: border-box;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag-item {
display: flex;
align-items: center;
background: #F0F2F5;
border-radius: 8rpx;
padding: 8rpx 16rpx;
gap: 12rpx;
}
.tag-text {
font-size: 28rpx;
color: #2F323A;
}
.tag-close {
display: flex;
align-items: center;
cursor: pointer;
}
.search-input {
flex: 1;
min-width: 200rpx;
font-size: 28rpx;
color: #2F323A;
height: 56rpx;
line-height: 56rpx;
}
.input-icon {
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
}
.dropdown {
position: absolute;
top: calc(100% + 16rpx);
left: 0;
right: 0;
background: #fff;
border: 1px solid #EBEEF5;
border-radius: 12rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.1);
z-index: 1000;
max-height: 600rpx;
}
.dropdown-scroll {
max-height: 600rpx;
}
.dropdown-item {
padding: 24rpx 32rpx;
border-bottom: 1px solid #F0F2F5;
cursor: pointer;
&:last-child {
border-bottom: none;
}
&:active {
background: #F7F8FB;
}
}
.item-content {
display: flex;
align-items: center;
justify-content: space-between;
text {
font-size: 28rpx;
color: #2F323A;
}
}
.dropdown-empty {
padding: 24rpx 32rpx;
text-align: center;
}
.empty-text {
font-size: 24rpx;
color: #999;
}
</style>