| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- <template>
- <view class="custom-select-container" :style="{ width: width, height: height }" @click.stop="toggleDropdown">
- <!-- 插槽或默认触发器 -->
- <slot>
- <!-- 单选默认样式 -->
- <view v-if="!multiple" class="select-modal-inputwrap">
- <view class="select-modal-static">
- {{ selectedLabel || placeholder }}
- </view>
- <view class="select-input-arrow"></view>
- </view>
- <!-- 多选默认样式 -->
- <view v-else class="select-modal-inputwrap">
- <view class="select-modal-static" :style="{ maxHeight: 'auto', overflowY: 'visible' }">
- <block v-if="Array.isArray(modelValue) && modelValue.length">
- <view :style="{ maxHeight: '100rpx', overflowY: 'auto', paddingRight: '40rpx' }">
- <text v-for="(item, i) in selectedOptions" :key="i" class="customer-chip">
- {{ item.label }}
- <text class="customer-chip-close" @click.stop="removeOption(item)">×</text>
- </text>
- </view>
- </block>
- <text v-else>{{ placeholder }}</text>
- <text v-if="modelValue && modelValue.length" class="customer-clear-all" @click.stop="clearAll">×</text>
- </view>
- <view class="select-input-arrow"></view>
- </view>
- </slot>
- <!-- 下拉列表 -->
- <view v-if="isOpen" class="select-modal-dropdown" :style="dropdownStyle" @click.stop>
- <!-- 搜索框 -->
- <view v-if="searchable" class="select-dropdown-search-row">
- <input class="select-dropdown-search-input" v-model="searchText" @input="onSearchInput"
- placeholder="搜索..." />
- </view>
- <!-- 列表 -->
- <scroll-view scroll-y :style="{ maxHeight: dropdownScrollHeight }" @scrolltolower="loadMore">
- <view class="select-modal-dropdown-item" :class="{ 'selected': isSelected(op) }"
- v-for="(op, i) in displayList" :key="i" @click.stop="onSelect(op)">
- {{ op.label }}
- </view>
- <view v-if="displayList.length === 0" class="select-modal-dropdown-item">{{ emptyText }}</view>
- <view v-if="hasMore" class="select-loading-row">
- <view class="select-loading-wrapper">
- <image class="select-loading-icon" src="../static/images/loading.png" />
- </view>
- </view>
- </scroll-view>
- </view>
- </view>
- </template>
- <script>
- import { getPaginatedList } from '../utils/utils.js';
- export default {
- name: 'Select',
- props: {
- // 下拉列表数据,格式为 {label: string, value: string}[]
- options: {
- type: Array,
- default: () => []
- },
- // 已选项
- modelValue: {
- type: [String, Number, Array],
- default: ''
- },
- // 控制下拉框显示方向(上或下) 'top' | 'bottom'
- direction: {
- type: String,
- default: 'bottom'
- },
- // 控制选择框宽度
- width: {
- type: String,
- default: '100%'
- },
- // 控制选择框高度
- height: {
- type: String,
- default: 'auto'
- },
- // 未选择时的提示文案
- placeholder: {
- type: String,
- default: '请选择'
- },
- // 下拉列表为空时的提示文案
- emptyText: {
- type: String,
- default: '暂无数据'
- },
- // 是否为多选
- multiple: {
- type: Boolean,
- default: false
- },
- // 下拉列表是否需要搜索框
- searchable: {
- type: Boolean,
- default: true
- },
- // 控制下拉框的宽度
- dropdownWidth: {
- type: String,
- default: '100%'
- },
- // 控制下拉框的高度
- dropdownHeight: {
- type: String,
- default: '320rpx'
- },
- // 下拉框显隐回调
- onVisibleChange: {
- type: Function,
- default: null
- },
- },
- emits: ['update:modelValue', 'change', 'loadMore'],
- data() {
- return {
- isOpen: false,
- searchText: '',
- displayList: [],
- page: 0,
- hasMore: true
- };
- },
- computed: {
- filteredOptions() {
- if (!this.searchable || !this.searchText) {
- return this.options;
- }
- const lowerSearch = this.searchText.toLowerCase();
- return this.options.filter(op =>
- String(op.label).toLowerCase().includes(lowerSearch)
- );
- },
- selectedLabel() {
- if (this.multiple) return '';
- const found = this.options.find(op => op.value === this.modelValue);
- return found ? found.label : '';
- },
- selectedOptions() {
- if (!this.multiple || !Array.isArray(this.modelValue)) return [];
- return this.modelValue.map(val => {
- const found = this.options.find(op => op.value === val);
- return found || { label: val, value: val };
- });
- },
- dropdownStyle() {
- const style = {
- width: this.dropdownWidth,
- maxHeight: this.dropdownHeight,
- zIndex: 999 // Ensure dropdown is on top
- };
- if (this.direction === 'top') {
- style.bottom = '100%';
- style.top = 'auto';
- style.marginBottom = '10rpx';
- } else {
- style.top = '100%';
- style.bottom = 'auto';
- style.marginTop = '10rpx';
- }
- return style;
- },
- dropdownScrollHeight() {
- // 简单估算,如果有搜索框,减去一部分高度
- return this.searchable ? 'calc(100% - 80rpx)' : '100%';
- }
- },
- watch: {
- isOpen(val) {
- if (this.onVisibleChange) {
- this.onVisibleChange(val);
- }
- // if (val) {
- // this.resetList();
- // } else {
- // this.searchText = '';
- // }
- },
- options: {
- handler() {
- if (this.isOpen) {
- this.resetList();
- }
- },
- deep: true
- },
- searchText() {
- this.resetList();
- }
- },
- methods: {
- toggleDropdown() {
- this.isOpen = !this.isOpen;
- },
- closeDropdown() {
- this.isOpen = false;
- },
- onSearchInput() {
- // 搜索逻辑由 watch searchText 处理
- },
- resetList() {
- this.page = 0;
- this.displayList = [];
- this.hasMore = true;
- this.loadMore();
- },
- loadMore() {
- if (!this.hasMore) return;
- const res = getPaginatedList({
- page: this.page,
- size: 10,
- initData: this.filteredOptions,
- data: this.displayList
- });
- if (res.data) {
- this.displayList = res.data;
- this.page = res.page;
- // 如果当前数据量等于总数据量,说明没有更多了
- this.hasMore = this.displayList.length < this.filteredOptions.length;
- // 触发外部事件,传递当前页码
- this.$emit('loadMore', this.page);
- } else {
- this.hasMore = false;
- }
- },
- onSelect(option) {
- if (this.multiple) {
- const currentVal = Array.isArray(this.modelValue) ? [...this.modelValue] : [];
- const idx = currentVal.indexOf(option.value);
- if (idx > -1) {
- currentVal.splice(idx, 1);
- } else {
- currentVal.push(option.value);
- }
- this.$emit('update:modelValue', currentVal);
- this.$emit('change', currentVal);
- // 多选时不自动关闭下拉框
- } else {
- this.$emit('update:modelValue', option.value);
- this.$emit('change', option.value);
- this.closeDropdown();
- }
- },
- removeOption(option) {
- if (!this.multiple) return;
- const currentVal = Array.isArray(this.modelValue) ? [...this.modelValue] : [];
- const idx = currentVal.indexOf(option.value);
- if (idx > -1) {
- currentVal.splice(idx, 1);
- this.$emit('update:modelValue', currentVal);
- this.$emit('change', currentVal);
- }
- },
- clearAll() {
- this.$emit('update:modelValue', []);
- this.$emit('change', []);
- },
- isSelected(option) {
- if (this.multiple) {
- return Array.isArray(this.modelValue) && this.modelValue.includes(option.value);
- }
- return this.modelValue === option.value;
- }
- },
- // 点击外部关闭下拉框需要页面配合,这里只能尽量在点击自身时处理
- // uni-app 中通常使用全屏透明遮罩或页面级点击事件来处理点击外部关闭
- // 这里简化处理,仅在组件内部逻辑中控制
- }
- </script>
- <style scoped>
- .custom-select-container {
- position: relative;
- box-sizing: border-box;
- }
- .select-modal-inputwrap {
- position: relative;
- background: #f5f7fa;
- border-radius: 8rpx;
- padding: 0 20rpx;
- min-height: 80rpx;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .select-modal-static {
- flex: 1;
- font-size: 28rpx;
- color: #333;
- padding: 16rpx 0;
- line-height: 1.4;
- /* 防止过长文本溢出,但这可能会影响多选 Tag 的显示,多选 Tag 有自己的样式控制 */
- }
- .select-input-arrow {
- width: 16rpx;
- height: 16rpx;
- border-right: 3rpx solid #c0c4cc;
- border-bottom: 3rpx solid #c0c4cc;
- transform: rotate(45deg);
- margin-top: -6rpx;
- flex-shrink: 0;
- margin-left: 10rpx;
- }
- .select-modal-dropdown {
- position: absolute;
- left: 0;
- right: 0;
- background: #fff;
- border: 1rpx solid #eef0f4;
- border-radius: 12rpx;
- box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
- overflow-y: hidden;
- /* scroll-view handles overflow */
- display: flex;
- flex-direction: column;
- }
- .select-dropdown-search-row {
- padding: 10rpx;
- flex-shrink: 0;
- }
- .select-dropdown-search-input {
- width: 100%;
- height: 60rpx;
- border: 1rpx solid #eaeef4;
- border-radius: 8rpx;
- box-sizing: border-box;
- padding: 0 16rpx;
- font-size: 28rpx;
- }
- .select-modal-dropdown-item {
- padding: 16rpx 20rpx;
- font-size: 28rpx;
- color: #333;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .selected {
- background: #f0f5ff;
- color: #2c69ff;
- }
- .customer-chip {
- display: inline-block;
- position: relative;
- line-height: 50rpx;
- border: 1rpx solid #eaeef4;
- border-radius: 8rpx;
- padding: 0rpx 44rpx 0rpx 16rpx;
- margin-right: 8rpx;
- margin-bottom: 8rpx;
- font-size: 22rpx;
- color: #333;
- background: #fff;
- }
- .customer-chip-close {
- position: absolute;
- right: 6rpx;
- top: 0;
- color: #666;
- font-size: 28rpx;
- padding: 0 8rpx;
- border-radius: 6rpx;
- }
- .customer-clear-all {
- position: absolute;
- right: 40rpx; /* 调整位置以免遮挡箭头 */
- top: 50%;
- transform: translateY(-50%);
- line-height: 50rpx;
- padding: 8rpx 10rpx;
- font-size: 40rpx;
- color: #999;
- z-index: 1;
- }
- .select-loading-row {
- display: flex;
- justify-content: center;
- padding: 20rpx;
- }
- .select-loading-wrapper {
- width: 40rpx;
- height: 40rpx;
- animation: rotate 1s linear infinite;
- }
- .select-loading-icon {
- width: 100%;
- height: 100%;
- }
- @keyframes rotate {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
- </style>
|