组件使用使用
<template>
<view class="container">
<view class="title">带搜索功能的下拉框示例</view>
<view class="dropdown-wrapper">
<SearchableDropdown
:options="options"
:value="selectedValue"
placeholder="请选择城市"
searchPlaceholder="搜索城市..."
@change="handleChange"
@search="handleSearch"
@loadMore="handleLoadMore"
/>
</view>
<view class="result" v-if="selectedValue">
你选择的城市是:{{ selectedCity }}
</view>
</view>
</template>
<script>
import SearchableDropdown from '@/components/SearchableDropdown.vue';
export default {
components: {
SearchableDropdown
},
data() {
return {
selectedValue: null,
options: [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
{ value: 'hangzhou', label: '杭州' },
{ value: 'nanjing', label: '南京' },
{ value: 'chengdu', label: '成都' },
{ value: 'chongqing', label: '重庆' },
{ value: 'wuhan', label: '武汉' },
{ value: 'xi_an', label: '西安' }
]
};
},
computed: {
selectedCity() {
const city = this.options.find(item => item.value === this.selectedValue);
return city ? city.label : '';
}
},
methods: {
handleChange(value) {
console.log('选中值:', value);
this.selectedValue = value;
},
handleSearch(keyword) {
console.log('搜索关键词:', keyword);
// 这里可以根据关键词进行远程搜索
// this.fetchOptions(keyword);
},
handleLoadMore(keyword) {
console.log('加载更多:', keyword);
// 模拟加载更多数据
setTimeout(() => {
const newOptions = [
{ value: 'qingdao', label: '青岛' },
{ value: 'suzhou', label: '苏州' },
{ value: 'xiamen', label: '厦门' }
];
// 通知组件加载完成并添加新数据
this.$refs.dropdown.finishLoadMore(newOptions);
}, 1000);
}
}
};
</script>
<style>
.container {
padding: 20px;
}
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.dropdown-wrapper {
margin-bottom: 20px;
padding: 0 20px;
}
.result {
padding: 10px 20px;
background-color: #f5f7fa;
border-radius: 4px;
font-size: 16px;
color: #333;
}
</style>
如果是使用符合uniapp esaycom 规范的组件, 直接 放到 compontents中即可,会自动引入
<template>
<view class="container">
<view class="title">带搜索功能的下拉框示例</view>
<view class="dropdown-wrapper">
<SearchableDropdown
:options="options"
:value="selectedValue"
placeholder="请选择城市"
searchPlaceholder="搜索城市..."
@change="handleChange"
@search="handleSearch"
@loadMore="handleLoadMore"
/>
</view>
<view class="result" v-if="selectedValue">
你选择的城市是:{{ selectedCity }}
</view>
</view>
</template>
<script>
export default {
data() {
return {
selectedValue: null,
options: [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
{ value: 'hangzhou', label: '杭州' },
{ value: 'nanjing', label: '南京' },
{ value: 'chengdu', label: '成都' },
{ value: 'chongqing', label: '重庆' },
{ value: 'wuhan', label: '武汉' },
{ value: 'xi_an', label: '西安' }
]
};
},
computed: {
selectedCity() {
const city = this.options.find(item => item.value === this.selectedValue);
return city ? city.label : '';
}
},
methods: {
handleChange(value) {
console.log('选中值:', value);
this.selectedValue = value;
},
handleSearch(keyword) {
console.log('搜索关键词:', keyword);
// 这里可以根据关键词进行远程搜索
// this.fetchOptions(keyword);
},
handleLoadMore(keyword) {
console.log('加载更多:', keyword);
// 模拟加载更多数据
setTimeout(() => {
const newOptions = [
{ value: 'qingdao', label: '青岛' },
{ value: 'suzhou', label: '苏州' },
{ value: 'xiamen', label: '厦门' }
];
// 通知组件加载完成并添加新数据
this.$refs.dropdown.finishLoadMore(newOptions);
}, 1000);
}
}
};
</script>
<style>
.container {
padding: 20px;
}
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.dropdown-wrapper {
margin-bottom: 20px;
padding: 0 20px;
}
.result {
padding: 10px 20px;
background-color: #f5f7fa;
border-radius: 4px;
font-size: 16px;
color: #333;
}
</style>
组件封装如下
SearchableDropdown.vue
<template>
<view class="searchable-dropdown">
<!-- 下拉框头部 -->
<view class="dropdown-header" @click="toggleDropdown">
<view class="selected-value">
{{ selectedLabel || placeholder }}
</view>
<view class="arrow-icon">
<text class="fa" :class="isOpen ? 'fa-chevron-up' : 'fa-chevron-down'"></text>
</view>
</view>
<!-- 下拉内容区域 -->
<view class="dropdown-content" v-show="isOpen">
<!-- 搜索框 -->
<view class="search-container">
<text class="fa fa-search search-icon"></text>
<input
type="text"
class="search-input"
:placeholder="searchPlaceholder"
v-model="searchValue"
@input="handleSearch"
@confirm="handleSearchConfirm"
@focus="setInputFocus(true)"
@blur="setInputFocus(false)"
/>
<text
class="clear-icon fa fa-times-circle"
v-show="searchValue && isInputFocused"
@click="clearSearch"
></text>
</view>
<!-- 选项列表 -->
<scroll-view
class="options-container"
scroll-y
:style="{ maxHeight: maxHeight }"
@scrolltolower="onScrollToLower"
>
<view
class="option-item"
:class="{ 'option-selected': option.value === selectedValue }"
v-for="option in filteredOptions"
:key="option.value"
@click="selectOption(option)"
>
<view class="option-label">{{ option.label }}</view>
<view class="option-check" v-if="option.value === selectedValue">
<text class="fa fa-check"></text>
</view>
</view>
<!-- 加载更多提示 -->
<view class="loading-more" v-show="isLoadingMore">
<text class="fa fa-spinner fa-spin"></text>
<text>加载更多...</text>
</view>
<!-- 无结果提示 -->
<view class="no-result" v-show="!hasOptions">
<text>{{ noResultText }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
name: 'SearchableDropdown',
props: {
// 选项列表,格式:[{ value: '1', label: '选项1' }]
options: {
type: Array,
default: () => []
},
// 当前选中的值
value: {
type: [String, Number, Object],
default: null
},
// 占位文本
placeholder: {
type: String,
default: '请选择'
},
// 搜索框占位文本
searchPlaceholder: {
type: String,
default: '搜索...'
},
// 无结果文本
noResultText: {
type: String,
default: '没有找到匹配项'
},
// 下拉框最大高度
maxHeight: {
type: String,
default: '200px'
},
// 是否启用加载更多
enableLoadMore: {
type: Boolean,
default: false
},
// 自定义搜索函数
customSearch: {
type: Function,
default: null
},
// 是否显示搜索框
showSearch: {
type: Boolean,
default: true
}
},
data() {
return {
isOpen: false, // 下拉框是否打开
searchValue: '', // 搜索框值
filteredOptions: [], // 过滤后的选项
isLoadingMore: false, // 是否正在加载更多
isInputFocused: false, // 搜索框是否聚焦
selectedValue: this.value // 当前选中值的本地副本
};
},
computed: {
// 当前选中项的标签
selectedLabel() {
if (this.selectedValue === null || this.selectedValue === undefined) {
return '';
}
const selectedOption = this.options.find(
option => option.value === this.selectedValue
);
return selectedOption ? selectedOption.label : '';
},
// 是否有选项
hasOptions() {
return this.filteredOptions.length > 0;
}
},
watch: {
// 监听外部传入的value变化
value(newVal) {
this.selectedValue = newVal;
this.filterOptions();
},
// 监听选项列表变化
options() {
this.filterOptions();
}
},
created() {
// 初始化过滤选项
this.filterOptions();
},
methods: {
// 切换下拉框打开/关闭状态
toggleDropdown() {
if (this.isOpen) {
this.closeDropdown();
} else {
this.openDropdown();
}
},
// 打开下拉框
openDropdown() {
this.isOpen = true;
this.filterOptions();
// 触发打开事件
this.$emit('open');
// 添加点击外部关闭下拉框的事件
setTimeout(() => {
document.addEventListener('click', this.handleOutsideClick);
}, 0);
},
// 关闭下拉框
closeDropdown() {
this.isOpen = false;
this.isInputFocused = false;
// 触发关闭事件
this.$emit('close');
// 移除点击外部关闭下拉框的事件
document.removeEventListener('click', this.handleOutsideClick);
},
// 处理点击外部关闭下拉框
handleOutsideClick(event) {
const dropdown = this.$el;
if (!dropdown.contains(event.target)) {
this.closeDropdown();
}
},
// 过滤选项
filterOptions() {
if (this.customSearch && typeof this.customSearch === 'function') {
// 使用自定义搜索函数
this.filteredOptions = this.customSearch(this.options, this.searchValue);
} else {
// 默认搜索逻辑:根据label模糊匹配
this.filteredOptions = this.options.filter(option => {
if (!this.searchValue) return true;
const label = option.label.toString().toLowerCase();
const search = this.searchValue.toLowerCase();
return label.includes(search);
});
}
},
// 处理搜索输入
handleSearch() {
this.filterOptions();
// 触发搜索事件
this.$emit('search', this.searchValue);
},
// 处理搜索确认
handleSearchConfirm() {
// 触发搜索确认事件
this.$emit('searchConfirm', this.searchValue);
},
// 清除搜索内容
clearSearch() {
this.searchValue = '';
this.filterOptions();
},
// 设置搜索框聚焦状态
setInputFocus(focused) {
this.isInputFocused = focused;
},
// 选择选项
selectOption(option) {
this.selectedValue = option.value;
this.closeDropdown();
// 触发选中事件
this.$emit('change', option.value, option);
},
// 滚动到底部加载更多
onScrollToLower() {
if (this.enableLoadMore && !this.isLoadingMore) {
this.isLoadingMore = true;
// 触发加载更多事件
this.$emit('loadMore', this.searchValue);
}
},
// 完成加载更多(由父组件调用)
finishLoadMore(newOptions = []) {
// 添加新选项
this.options = [...this.options, ...newOptions];
// 重新过滤选项
this.filterOptions();
// 重置加载状态
this.isLoadingMore = false;
}
},
beforeDestroy() {
// 确保移除事件监听
document.removeEventListener('click', this.handleOutsideClick);
}
};
</script>
<style lang="scss" scoped>
.searchable-dropdown {
position: relative;
width: 100%;
.dropdown-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 12px;
border: 1px solid #e5e5e5;
border-radius: 4px;
background-color: #fff;
cursor: pointer;
.selected-value {
flex: 1;
font-size: 14px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow-icon {
margin-left: 8px;
color: #999;
transition: transform 0.2s ease;
&.fa-chevron-up {
transform: rotate(180deg);
}
}
&:hover {
border-color: #007AFF;
}
}
.dropdown-content {
position: absolute;
top: 45px;
left: 0;
right: 0;
z-index: 100;
border: 1px solid #e5e5e5;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
// 下拉动画
animation: fadeIn 0.2s ease;
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
}
.search-container {
display: flex;
align-items: center;
height: 40px;
padding: 0 12px;
border-bottom: 1px solid #e5e5e5;
.search-icon {
margin-right: 8px;
color: #999;
}
.search-input {
flex: 1;
height: 100%;
font-size: 14px;
color: #333;
&::placeholder {
color: #999;
}
}
.clear-icon {
margin-left: 8px;
color: #999;
cursor: pointer;
&:hover {
color: #666;
}
}
}
.options-container {
max-height: 200px;
overflow-y: auto;
// 自定义滚动条样式
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 2px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
}
.option-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 12px;
font-size: 14px;
color: #333;
cursor: pointer;
&:hover {
background-color: #f5f7fa;
}
.option-selected {
background-color: #e6f4ff;
color: #007AFF;
}
.option-check {
color: #007AFF;
}
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
font-size: 14px;
color: #999;
}
.no-result {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
font-size: 14px;
color: #999;
}
}
</style>