293 lines
6.3 KiB
Vue
293 lines
6.3 KiB
Vue
<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>
|
||
|