123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- <!-- 单选选择器 -->
- <template>
- <view>
- <!-- 表单输入框 -->
- <view v-if="formInput">
- <uni-easyinput
- v-model="inputText"
- type="text"
- :placeholder="placeholder"
- :clearable="clearable"
- :readonly="readonly"
- @clear="handleClear"
- @focus.stop="handleOpen('focus')"
- ></uni-easyinput>
- </view>
- <!-- 插槽 -->
- <view v-else @tap="handleOpen('slot')">
- <slot></slot>
- </view>
- <!-- 弹窗 -->
- <uni-popup ref="popup">
- <view class="popup-content" :style="popupStyle">
- <view class="popup-header">
- <view class="popup-title">{{ label }}</view>
- <uni-icons
- type="closeempty"
- size="24"
- color="#999"
- @tap="closePopup"
- />
- </view>
-
- <view class="popup-search" v-if="filter">
- <uni-easyinput
- v-model="inputKeyword"
- type="text"
- :placeholder="searchPlaceholder"
- @input="inputChange"
- suffixIcon="search"
- clearable
- ></uni-easyinput>
- </view>
-
- <scroll-view class="popup-body" scroll-y :style="{ height: bodyHeight }">
- <view class="level-container">
- <view v-if="!showList?.length" class="empty-state">
- <uni-icons type="info" size="24" color="#999" />
- <text class="empty-text">{{ inputKeyword ? '没有匹配的数据' : '暂无数据' }}</text>
- </view>
- <template v-else>
- <view
- v-for="item in showList"
- :key="item[itemValue]"
- class="item-container"
- :class="calcClass(item)"
- @tap="handleItemClick(item)"
- >
- <text class="item-label">{{ item[itemLabel] }}</text>
- <view class="item-icons">
- <uni-icons
- v-if="isItemActive(item)"
- type="checkmarkempty"
- color="#00B760"
- size="16"
- />
- </view>
- </view>
- </template>
- </view>
- </scroll-view>
- </view>
- </uni-popup>
- </view>
- </template>
- <script setup>
- import { ref, watch, computed } from 'vue'
- import { debounce } from 'lodash-es'
- const emit = defineEmits(['update:modelValue', 'change', 'search'])
- const props = defineProps({
- // 基础属性
- modelValue: [String, Number, Array, Object],
- text: String,
- items: {
- type: Array,
- default: () => []
- },
- label: {
- type: String,
- default: '请选择'
- },
- placeholder: {
- type: String,
- default: '请选择'
- },
-
- // 字段映射
- itemLabel: {
- type: String,
- default: 'label'
- },
- itemValue: {
- type: String,
- default: 'value'
- },
- children: {
- type: String,
- default: 'children'
- },
- bodyHeight: {
- type: String,
- default: '60vh'
- },
-
- // 功能开关
- multiple: Boolean,
- filter: Boolean, // 可检索
- clearable: Boolean,
- readonly: Boolean,
- hideChildren: Boolean, // 不展示子级
- footer: Boolean, // 显示底部按钮
- // 表单右侧输入框
- formInput: {
- type: Boolean,
- default: true
- },
- // 搜索相关
- searchPlaceholder: {
- type: String,
- default: '请输入'
- },
- searchDebounceTime: {
- type: Number,
- default: 500
- },
-
- // 样式
- popupStyle: [String, Object],
-
- // 文本自定义
- clearText: {
- type: String,
- default: '清除'
- },
- submitText: {
- type: String,
- default: '确定'
- }
- })
- // 组件引用
- const popup = ref()
- // 数据状态
- const inputText = ref('')
- const inputValue = ref(null)
- const inputKeyword = ref('')
- const showList = ref([])
- // 计算属性
- const filteredItems = computed(() => {
- if (!inputKeyword.value) {
- return props.items
- }
- return props.items?.filter(item =>
- item[props.itemLabel]?.toLowerCase().includes(inputKeyword.value.toLowerCase())
- )
- })
- // 使用 lodash 的防抖
- const debouncedSearch = debounce(() => {
- showList.value = filteredItems.value
- emit('search', inputKeyword.value)
- }, props.searchDebounceTime)
- const inputChange = () => {
- debouncedSearch()
- }
- const isItemActive = (item) => {
- return inputValue.value === item[props.itemValue]
- }
- // 方法
- const calcClass = (item) => {
- return {
- active: isItemActive(item),
- // 'has-children': item[props.children]?.length > 0
- }
- }
- const handleOpen = async () => {
- popup.value.open('bottom')
- }
- const handleClear = () => {
- inputValue.value = null
- inputText.value = ''
- emit('update:modelValue', null)
- emit('change', null, null)
- }
- const closePopup = () => {
- popup.value.close()
- }
- const handleItemClick = (item) => {
- inputText.value = item[props.itemLabel]
- inputValue.value = item[props.itemValue]
- emit('update:modelValue', item[props.itemValue])
- emit('change', item[props.itemValue], item)
- closePopup()
- }
- // 回显
- const getInputText = () => {
- if (inputValue.value && props.items?.length) {
- const item = props.items.find(i => i[props.itemValue] === inputValue.value)
- inputText.value = item?.[props.itemLabel] || ''
- }
- }
- const initShowList = () => {
- showList.value = filteredItems.value
- getInputText()
- }
- // 监听props变化
- watch(() => props.items, initShowList, { immediate: true })
- // 实现回显逻辑
- watch(() => props.modelValue, (newVal) => {
- inputValue.value = newVal || null
- getInputText()
- }, { immediate: true })
- </script>
- <style lang="scss" scoped>
- .popup-content {
- background: #fff;
- border-radius: 16rpx 16rpx 0 0;
- padding: 24rpx;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- }
- .popup-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-bottom: 16rpx;
- border-bottom: 1rpx solid #eee;
- }
- .popup-title {
- font-size: 32rpx;
- font-weight: bold;
- color: #333;
- }
- .popup-search {
- padding: 24rpx 0;
- }
- .level-container {
- margin-bottom: 16rpx;
- }
- .empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40rpx 0;
-
- .empty-text {
- margin-top: 16rpx;
- color: #999;
- font-size: 28rpx;
- }
- }
- .item-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20rpx 16rpx;
- border-radius: 8rpx;
- margin-bottom: 8rpx;
- transition: all 0.3s ease;
-
- &.active {
- background-color: #f5fff9;
- color: #00B760;
- }
- }
- .item-label {
- flex: 1;
- font-size: 28rpx;
- }
- .item-icons {
- margin-left: 16rpx;
- }
- .popup-footer {
- display: flex;
- justify-content: space-between;
- padding-top: 24rpx;
- border-top: 1rpx solid #eee;
-
- .btn {
- flex: 1;
- height: 80rpx;
- line-height: 80rpx;
- font-size: 28rpx;
- border-radius: 8rpx;
- text-align: center;
-
- &.cancel {
- background: #f5f5f5;
- color: #666;
- margin-right: 16rpx;
- }
-
- &.confirm {
- background: #00B760;
- color: #fff;
-
- &[disabled] {
- opacity: 0.6;
- }
- }
- }
- }
- </style>
|