<template>
<!-- 蒙层 -->
<view v-if="visible" class="zy-popup-select-mask-layer" @click="handleMaskClick">
<view :class="{ 'zy-popup-select-mask-false': !mask, 'zy-popup-select-mask': mask }" style="height: 100%" />
</view>
<!-- 弹窗 -->
<view v-if="visible" class="zy-popup-select-popup-wrapper">
<view class="zy-popup-select-popup" :style="{ 'min-height': optionsHeight + optionsHeightPx }">
<!-- 滚动区域 -->
<scroll-view
class="zy-popup-select-options"
:class="{ 'zy-popup-select-options-scrollable': options.length > maxVisibleItems }"
scroll-y
:style="{ '--max-visible-height': maxVisibleHeight }"
@scroll="handleScroll"
>
<!-- 🔹 插槽:header - 滚动区域内顶部 -->
<view v-if="$slots.header" class="zy-popup-select-slot-header">
<slot
name="header"
:selected-count="selectedValues.length"
:total-count="options.length"
:clear-selection="clearSelection"
:select-all="selectAll"
/>
</view>
<!-- 渲染选项 -->
<view
v-for="(item, index) in options"
:key="getValue(item)"
class="zy-popup-select-option-item"
:class="{ 'zy-popup-select-option-selected': isSelected(item) }"
:style="{ height: optionsHeight + optionsHeightPx }"
@click="toggleOption(item)"
>
<!-- 🔹 插槽:option - 自定义每个选项 -->
<slot
name="option"
:item="item"
:index="index"
:value="getValue(item)"
:label="getLabel(item)"
:selected="isSelected(item)"
>
<!-- 默认选项 -->
<text
class="zy-popup-select-option-text"
:class="{ 'zy-popup-select-option-text-selected': isSelected(item) }"
>
{{ getLabel(item) }}
</text>
<uni-icons
v-if="isSelected(item)"
class="zy-popup-select-checkmark"
type="checkmarkempty"
color="#0f56d5"
/>
</slot>
</view>
<!-- 🔹 插槽:footer - 滚动区底部 -->
<view v-if="$slots.footer && options.length > 0" class="zy-popup-select-slot-footer">
<slot
name="footer"
:selected-values="selectedValues"
:options="options"
:clear="clearSelection"
:select-all="selectAll"
/>
</view>
<!-- 🔹 插槽:empty - 数据为空时展示 -->
<view v-if="options.length === 0" class="zy-popup-select-slot-empty">
<slot name="empty">
<text class="zy-popup-select-empty-text">暂无可用选项</text>
</slot>
</view>
</scroll-view>
<!-- 假滚动条 -->
<view
:style="{ height: optionsHeight * 1.5 + optionsHeightPx }"
v-if="showFakeScrollbar && options.length > maxVisibleItems"
class="zy-popup-select-fake-scrollbar"
/>
<!-- 分割线 -->
<view class="zy-popup-select-divider" />
<!-- 🔹 插槽:cancel - 可替换取消按钮 -->
<view class="zy-popup-select-cancel" :style="{ height: optionsHeight + optionsHeightPx }" @click="handleCancel">
<slot name="cancel">
<text class="zy-popup-select-cancel-text">取消</text>
</slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'ZyPopupSelect',
props: {
modelValue: { type: Boolean, default: false },
options: { type: Array, required: true },
labelKey: { type: String, default: 'label' },
valueKey: { type: String, default: 'value' },
maxSelected: { type: Number, default: 1 },
defaultSelected: { type: Array, default: () => [] },
mask: { type: Boolean, default: true },
optionsHeight: { type: Number, default: 57 },
optionsHeightPx: { type: String, default: 'px' },
maxVisibleItems: { type: Number, default: 3 },
isNeedSelected: { type: Boolean, default: true }
},
emits: ['update:modelValue', 'change', 'confirm'],
data() {
return {
selectedValues: [...this.defaultSelected],
showFakeScrollbar: true // 默认显示假滚动条
}
},
computed: {
visible() {
return this.modelValue
},
maxVisibleHeight() {
return `${this.maxVisibleItems * this.optionsHeight}${this.optionsHeightPx}`
}
},
mounted() {},
methods: {
handleScroll(e) {
if (this.visible) {
this.showFakeScrollbar = false
}
},
// 清空所有选择
clearSelection() {
this.selectedValues = []
this.$emit('change', { item: null, selected: false })
},
// 全选功能(受 maxSelected 限制)
selectAll() {
if (this.maxSelected === 1) return
const restCount = this.maxSelected - this.selectedValues.length
if (restCount <= 0) {
uni.showToast({ title: `已达上限${this.maxSelected}项`, icon: 'none' })
return
}
const newValues = this.options
.map(this.getValue)
.filter(val => !this.selectedValues.includes(val))
.slice(0, restCount)
this.selectedValues.push(...newValues)
this.$emit('change', { item: null, selected: true })
},
getLabel(item) {
return item[this.labelKey]
},
getValue(item) {
return item[this.valueKey]
},
isSelected(item) {
return this.selectedValues.includes(this.getValue(item)) && this.isNeedSelected
},
toggleOption(item) {
const value = this.getValue(item)
const index = this.selectedValues.indexOf(value)
if (index > -1 || !this.isNeedSelected) {
console.log('🚀 ~ toggleOption ~ item:', item)
this.selectedValues.splice(index, 1)
this.$emit('change', { item, selected: false })
console.log('🚀 ~ toggleOption ~ item:', item)
} else {
if (this.selectedValues.length >= this.maxSelected) {
uni.showToast({
title: `最多选中 ${this.maxSelected} 项`,
icon: 'none',
duration: 1500
})
return
}
this.selectedValues.push(value)
this.$emit('change', { item, selected: true })
}
},
handleCancel() {
this.selectedValues = [...this.defaultSelected] // 恢复默认
this.close(true)
},
handleMaskClick() {
this.close(false) // 点击蒙层视为非取消关闭
},
close(fromCancel) {
this.showFakeScrollbar = true
this.$emit('update:modelValue', false)
this.$emit('confirm', {
selected: this.selectedValues.map(val => this.options.find(opt => this.getValue(opt) === val)).filter(Boolean),
fromCancel
})
}
}
}
</script>
<style lang="scss">
/* 蒙层外层:覆盖全屏,但只显示上半部分 */
.zy-popup-select-mask-layer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 998; /* 蒙层层级 */
pointer-events: auto;
}
.zy-popup-select-mask {
width: 100%;
background-color: #000000;
opacity: 0.5;
}
.zy-popup-select-mask-false {
opacity: 0;
width: 100%;
}
/* 弹窗外层:固定在底部,浮在蒙层之上 */
.zy-popup-select-popup-wrapper {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 999; /* 高于蒙层 */
background-color: #fff;
/* 确保不侵入底部安全区 */
padding-bottom: env(safe-area-inset-bottom);
/* ✅ 关键:顶部圆角 */
border-radius: 8px 8px 0 0;
/* ✅ 确保子元素不溢出圆角边界 */
overflow: hidden;
}
/* 弹窗整体布局改为 flex 列布局 */
.zy-popup-select-popup {
background-color: #ffffff;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
/* 滚动区域:允许纵向滚动,限制最大高度 */
.zy-popup-select-options {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
max-height: var(--max-visible-height); /* 使用 CSS 变量 */
}
.zy-popup-select-options-scrollable {
overflow-y: auto;
}
/* 单个选项保持居中等样式不变 */
.zy-popup-select-option-item {
position: relative;
display: flex;
align-items: center;
justify-content: center; /* 主轴居中:X 轴居中 */
padding: 0 60rpx;
box-sizing: border-box;
}
.zy-popup-select-option-item::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px; /* 先设成 1px */
background-color: #eeeeee;
transform: scaleY(0.5); /* 缩小一半 → 视觉上就是 0.5px */
transform-origin: bottom;
pointer-events: none;
}
.zy-popup-select-option-item:last-child::after {
display: none;
}
/* 文本:完全居中,不受对号影响 */
.zy-popup-select-option-text {
font-size: $uni-font-size-18;
color: $uni-text-color;
font-weight: 400;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 100rpx); /* 防止太长碰到右边 */
text-align: center;
z-index: 1; /* 确保在上层 */
}
/* 选中状态文字颜色 */
.zy-popup-select-option-selected .zy-popup-select-option-text {
color: $uni-primary;
}
.zy-popup-select-option-text-selected {
transform: translate(8px, 0px);
}
/* ✅ 对号:绝对定位,脱离文档流 */
.zy-popup-select-checkmark {
transform: translate(22px, 0px);
color: $uni-primary;
font-weight: bold;
font-size: $uni-font-size-18;
width: auto;
height: auto;
z-index: 2; /* 确保显示在上面 */
}
/* 分割线:固定在取消按钮上方 */
.zy-popup-select-divider {
height: 8px;
background-color: $uni-bg-color;
/* 不再随内容滚动 */
}
/* 取消按钮 */
.zy-popup-select-cancel {
display: flex;
align-items: center;
justify-content: center;
}
.zy-popup-select-cancel-text {
font-size: $uni-font-size-18;
color: $uni-text-color;
text-align: center;
font-weight: 400;
}
/* 插槽容器通用样式 */
.zy-popup-select-slot-header,
.zy-popup-select-slot-footer,
.zy-popup-select-slot-empty {
width: 100%;
padding: 12px 0;
display: flex;
justify-content: center;
align-items: center;
position: relative;
/* 添加顶部分隔线 */
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background-color: #eee;
transform: scaleY(0.5);
transform-origin: top;
}
}
/* 空状态文本 */
.zy-popup-select-empty-text {
font-size: $uni-font-size-14;
color: #999;
}
/* 假滚动条 */
.zy-popup-select-fake-scrollbar {
position: absolute;
top: 4px;
right: 3px; /* 距离右边一点 */
bottom: 250px; /* 避开取消按钮区域 */
width: 3px;
border-radius: 1px;
background-color: #999;
pointer-events: none; /* 不影响点击穿透 */
z-index: 9;
// 添加淡入效果(可选)
opacity: 0.8;
transition: opacity 0.3s ease-out;
}
/* 弹窗容器需要相对定位才能让绝对定位生效 */
.zy-popup-select-popup-wrapper {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 999;
background-color: #fff;
padding-bottom: env(safe-area-inset-bottom);
border-radius: 8px 8px 0 0;
overflow: hidden;
/* 关键:为假滚动条提供定位上下文 */
.zy-popup-select-popup {
position: relative; /* 必须加!否则 absolute 失效 */
}
}
</style>
去除当前组件内所有的插槽
最新发布