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>
|
|||
|
|
|