Files
cashier_weapp/uni_modules/le-dropdown/components/le-dropdown/le-dropdown.vue
2024-04-09 09:26:47 +08:00

603 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view
:class="['le-dropdown',isCeiling&&'le-dropdown-ceiling']"
:style="`--dropdownThemeColor:${themeColor};--dropdownThemeColorRgb:${hexToRgb(themeColor)}`">
<!-- 导航 -->
<view class="le-dropdown-menu">
<view class="le-dropdown-menu-item" v-for="(item, index) in menuList" :key="index"
@tap.stop="menuClick(index)">
<view class="le-flex">
<text class="le-dropdown-menu-item-text" :style="{
color: index === current ? themeColor : inactiveColor
}">{{item.title}}</text>
<view v-if="item.type==='sort'"
:class="['le-dropdown-menu-item-arrow',item.value==='asc'&&'le-dropdown-menu-item-arrow_top',item.value==='desc'&&'le-dropdown-menu-item-arrow_bottom']">
</view>
<view v-if="item.type!=='sort'"
:class="['le-dropdown-menu-item-basicarrow',index === current&&'le-dropdown-menu-item-basicarrow_rotate']">
</view>
</view>
</view>
</view>
<!-- 展示的内容 -->
<view class="le-dropdown-content" :style="[
{
transition: `opacity ${duration / 1000}s linear`,
top:'auto',
bottom:`calc(100vh - ${contentHeight + windowTop}px)`,
height:`calc(100vh - ${contentHeight + windowTop}px)`
},
contentStyle
]" @tap="close">
<view ref="leDropdownContentPopupRef" class="le-dropdown-content-popup" :style="[popupStyle]"
@tap.stop.prevent>
<block v-for="(item,index) in menuList" :key="index">
<!-- 单选列表 -->
<view class="le-dropdown-popup-content le-dropdown-cell"
v-if="item.type==='cell'&&index === current">
<view :class="['le-dropdown-cell-item',item.value===sItem.value&&'le-dropdown-cell-active']"
v-for="(sItem,sIndex) in item.options" :key="sIndex" @click="onSelectCell(sItem,index)">
<view class="le-dropdown-cell-active-text">{{sItem.label}}</view>
<view v-show="item.value===sItem.value" class="le-dropdown-cell-active-icon"></view>
</view>
</view>
<!-- 级联动选择 -->
<view class="le-dropdown-popup-content le-dropdown-picker"
v-if="item.type==='picker'&&index === current">
<le-dropdown-picker v-model="item.value" v-bind="item.componentProps">
</le-dropdown-picker>
<view class="le-dropdown-footer">
<view class="le-dropdown-confirm" @click="onFilterConfirm(item,index)">
{{item.confirmText||'确定'}}
</view>
</view>
</view>
<!-- 筛选 -->
<view class="le-dropdown-popup-content le-dropdown-filter"
v-if="item.type==='filter'&&index === current">
<view class="le-dropdown-filter-item" v-for="(sItem,sIndex) in item.children" :key="sIndex">
<view class="le-dropdown-filter-title">
{{sItem.title}}
<text class="le-dropdown-filter-subtitle"
v-if="sItem.type==='slider'">{{sItem.value}}{{sItem.suffix}}</text>
</view>
<view class="le-dropdown-filter-content">
<!-- 单选类型 -->
<block v-if="sItem.type==='radio'">
<view v-for="(ssItem,ssIndex) in sItem.options"
:class="['le-dropdown-filter-box',sItem.value===ssItem.value&&'le-dropdown-filter-box-active']"
:key="ssIndex" @click="onRadioFilter(sIndex,ssItem,index)">
{{ssItem.label}}
</view>
</block>
<!-- 多选类型 -->
<block v-else-if="sItem.type==='checkbox'">
<view v-for="(ssItem,ssIndex) in sItem.options"
:class="['le-dropdown-filter-box',sItem.value.includes(ssItem.value)&&'le-dropdown-filter-box-active']"
:key="ssIndex" @click="onCheckboxFilter(sIndex,ssItem,index)">
{{ssItem.label}}
</view>
</block>
<!-- 滑块类型 -->
<block v-else-if="sItem.type==='slider'">
<slider style="width: 100%;" :activeColor="themeColor" :value="sItem.value"
:min="sItem.componentProps.min||0" :max="sItem.componentProps.max||100"
:step="sItem.componentProps.step||1"
:show-value="sItem.componentProps['show-value']||true"
@change="onSliderChange($event,sIndex,ssItem,index)" />
</block>
</view>
</view>
<view class="le-dropdown-footer">
<view class="le-dropdown-reset" @click="onFilterReset(item,index)">重置</view>
<view class="le-dropdown-confirm" @click="onFilterConfirm(item,index)">
{{item.confirmText||'确定'}}
</view>
</view>
</view>
</block>
</view>
<view class="le-dropdown-content-mask" @click="close"></view>
</view>
</view>
</template>
<script setup>
/**
* menuList 导航数据
* @property {String} title 导航名称
* @property {String} type =[cell|picker|sort|click|filter] 导航点击展示的类型
* @value cell 下拉选择(单选)
* @value picker 级联动
* @value sort 排序
* @value click 点击
* @value filter 复杂筛选
* @property {String} value 导航当前选中的值
* @property {Array} options 导航展示可选的数据值
* */
import {
ref,
computed,
getCurrentInstance,
onMounted
} from "vue";
import cloneDeep from './utils/cloneDeep.js';
import hexToRgb from './utils/hexToRgb.js';
import LeDropdownPicker from "./components/le-picker.vue";
const props = defineProps({
// 导航数据
menuList: {
type: Array,
default: () => {
return [
]
}
},
// 主题的颜色
themeColor: {
type: String,
default: "#3185FF"
},
// 没选中的颜色
inactiveColor: {
type: String,
default: "#333333"
},
// 过渡时间
duration: {
type: [Number, String],
default: 300
},
// 是否吸顶
isCeiling: {
type: Boolean,
default: false
},
})
const emits = defineEmits(["open", "close", "update:menuList", "onConfirm", "onChange"])
// 初始化数据用于重置使用
const initMenuList = cloneDeep(props.menuList)
const instance = getCurrentInstance(); // 获取组件实例
// 下拉出来部分的样式
const popupStyle = computed(() => {
let style = {};
// 进行Y轴位移展开状态时恢复原位。收齐状态时往上位移100%,进行隐藏
style.transform = `translateY(${active.value ? 0 : '-100%'})`
style['transition-duration'] = props.duration / 1000 + 's';
return style;
})
// 当前是第几个菜单处于激活状态
const current = ref(99999);
// 外层内容的样式,初始时处于底层,且透明
const contentStyle = ref({
zIndex: -1,
opacity: 0
})
const active = ref(false) // 下拉菜单的状态
const contentHeight = ref(0)
const leDropdownContentPopupRef = ref(null)
const windowTop = ref(0)
uni.getSystemInfo({
success(e) {
windowTop.value = e.windowTop;
}
})
// 点击菜单
const menuClick = (index) => {
getContentHeight()
switch (props.menuList[index].type) {
case 'sort':
onSort(index);
break;
case 'click':
onClick(index);
break;
default:
// 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
if (index === current.value) {
close();
return;
}
open(index);
break;
}
}
// 打开当前筛选窗
const open = (index) => {
// 展开时,设置下拉内容的样式
active.value = true;
contentStyle.value.zIndex = 11;
contentStyle.value.opacity = 1;
contentStyle.value.bottom = `0px`;
contentStyle.value.top = `80rpx`;
// 标记展开状态以及当前展开项的索引
current.value = index;
emits("open", current.value)
}
// 关闭当前筛选窗
const close = () => {
active.value = false;
contentStyle.value.opacity = 0;
// 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
setTimeout(() => {
contentStyle.value.zIndex = -1;
current.value = 99999;
contentStyle.value.bottom = `calc(100vh - ${contentHeight.value + windowTop.value}px)`
contentStyle.value.top = 'auto';
}, props.duration)
emits("close", current.value)
}
// 获取下拉菜单内容的高度
const getContentHeight = () => {
uni.createSelectorQuery()
.in(instance)
.selectAll('.le-dropdown-menu')
.boundingClientRect()
.exec(data => {
contentHeight.value = data[0][0].bottom
});
}
// 点击排序
const onSort = (index) => {
const type = current.value === 99999 ? current.value : props.menuList[current.value].type
switch (type) {
case 'sort':
case 'click':
case 99999:
start()
break;
default:
close();
setTimeout(() => {
start()
}, props.duration)
break;
}
function start() {
const menuList = cloneDeep(props.menuList);
current.value = index;
menuList[index].value = !menuList[index].value ? 'asc' : menuList[index].value == 'asc' ? 'desc' :
null;
emits("update:menuList", menuList)
emits("onConfirm", menuList[index])
}
}
// 点击按钮
const onClick = (index) => {
const type = current.value === 99999 ? current.value : props.menuList[current.value].type
switch (type) {
case 'sort':
case 'click':
case 99999:
start()
break;
default:
close();
setTimeout(() => {
start()
}, props.duration)
break;
}
function start() {
const menuList = cloneDeep(props.menuList);
current.value = index;
emits("onConfirm", menuList[index])
}
}
// 单选列表选中事件
const onSelectCell = (sItem, index) => {
const menuList = cloneDeep(props.menuList);
menuList[index].title = sItem.label;
menuList[index].value = sItem.value;
emits("update:menuList", menuList)
close();
emits("onConfirm", menuList[index])
}
// 筛选单选选中事件
const onRadioFilter = (sIndex, ssItem, index) => {
const menuList = cloneDeep(props.menuList);
menuList[index].children[sIndex].value = ssItem.value;
emits("update:menuList", menuList)
emits("onChange", menuList[index], sIndex)
}
// 筛选多选选中事件
const onCheckboxFilter = (sIndex, ssItem, index) => {
const menuList = cloneDeep(props.menuList);
const indexs = menuList[index].children[sIndex].value.indexOf(ssItem.value)
if (indexs != -1) {
menuList[index].children[sIndex].value.splice(indexs, 1)
} else {
menuList[index].children[sIndex].value.push(ssItem.value)
}
emits("update:menuList", menuList)
emits("onChange", menuList[index], sIndex)
}
// 滑块值的变化事件
const onSliderChange = (event, sIndex, ssItem, index) => {
const menuList = cloneDeep(props.menuList);
menuList[index].children[sIndex].value = event.detail.value;
emits("update:menuList", menuList)
emits("onChange", menuList[index], sIndex)
}
// 筛选选中事件
const onSelectFilter = (sIndex, ssItem, index) => {
const menuList = cloneDeep(props.menuList);
menuList[index].active[sIndex] = ssItem.value;
emits("update:menuList", menuList)
emits("onChange", menuList[index], sIndex)
}
// 重置筛选
const onFilterReset = (item, index) => {
const menuList = cloneDeep(props.menuList);
menuList[index].children.forEach((items, indexs) => {
items.value = initMenuList[index].children[indexs].value
})
emits("update:menuList", menuList)
// close();
// emits("onConfirm",menuList[index])
}
// 确定筛选
const onFilterConfirm = (item, index) => {
close();
const menuList = cloneDeep(props.menuList);
emits("onConfirm", menuList[index])
}
const bindFun = (data) => {
return data
}
onMounted(() => {
getContentHeight()
})
</script>
<style lang="scss" scoped>
.le-flex {
display: flex;
align-items: center;
height: 100%;
}
.le-dropdown {
width: 100%;
position: relative;
.le-dropdown-menu {
display: flex;
position: relative;
z-index: 11;
height: 80rpx;
.le-dropdown-menu-item {
height: 100%;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
.le-dropdown-menu-item-text {
font-size: 24rpx;
}
.le-dropdown-menu-item-arrow {
margin-left: 6rpx;
transition: transform .3s;
align-items: center;
display: flex;
position: relative;
width: 10rpx;
height: 100%;
&::before {
content: '';
position: absolute;
top: calc(50% - 8rpx);
right: -2rpx;
transform: translateY(-50%);
border: 6rpx solid transparent;
border-bottom-color: #C1C1C1;
}
&::after {
content: '';
position: absolute;
top: calc(50% + 8rpx);
right: -2rpx;
transform: translateY(-50%);
border: 6rpx solid transparent;
border-top-color: #C1C1C1;
}
}
.le-dropdown-menu-item-arrow_top {
&::before {
border-bottom-color: var(--dropdownThemeColor);
}
}
.le-dropdown-menu-item-arrow_bottom {
&::after {
border-top-color: var(--dropdownThemeColor);
}
}
.le-dropdown-menu-item-basicarrow {
margin-left: 6rpx;
transition: transform .3s;
align-items: center;
display: flex;
position: relative;
border: 6rpx solid transparent;
border-bottom: 0rpx solid transparent;
border-top-color: #C1C1C1;
}
.le-dropdown-menu-item-basicarrow_rotate {
transform: rotate(180deg);
border-top-color: var(--dropdownThemeColor);
}
}
}
.le-dropdown-content {
position: absolute;
z-index: 8;
width: 100%;
left: 0px;
bottom: 0;
overflow: hidden;
.le-dropdown-content-mask {
position: absolute;
z-index: 9;
background: rgba(0, 0, 0, .3);
width: 100%;
left: 0;
top: 0;
bottom: 0;
}
.le-dropdown-content-popup {
position: relative;
max-height: 100%;
overflow: auto;
overscroll-behavior: contain;
z-index: 10;
transition: all 0.3s;
transform: translate3D(0, -100%, 0);
}
}
}
.le-dropdown-ceiling{
position: sticky;
top: 0;
z-index: 1;
}
.le-dropdown-popup-content {
font-size: 28rpx;
border-radius: 0 0 20rpx 20rpx;
background-color: #ffffff;
}
// 单选列表
.le-dropdown-cell {
padding: 0 30rpx;
.le-dropdown-cell-item {
padding: 20rpx 0;
color: #333333;
font-size: 28rpx;
border-bottom: 1rpx solid #D5D5D5;
display: flex;
align-items: center;
justify-content: space-between;
.le-dropdown-cell-active-text{
flex: 1;
padding-right: 20rpx;
}
}
.le-dropdown-cell-item:last-child {
border-bottom: 0rpx solid #D5D5D5;
}
.le-dropdown-cell-active {
color: var(--dropdownThemeColor);
.le-dropdown-cell-active-icon {
width: 12rpx;
height: 28rpx;
margin-right: 10rpx;
border-color: var(--dropdownThemeColor);
border-style: solid;
border-width: 0 4rpx 4rpx 0;
transform: rotate(45deg);
}
}
}
// 筛选
.le-dropdown-filter {
.le-dropdown-filter-item {
padding: 0 26rpx;
}
.le-dropdown-filter-title {
padding-top: 34rpx;
margin-bottom: 18rpx;
color: #333333;
font-size: 24rpx;
.le-dropdown-filter-subtitle {
margin-left: 10rpx;
color: var(--dropdownThemeColor);
}
}
.le-dropdown-filter-content {
display: flex;
flex-wrap: wrap;
}
.le-dropdown-filter-box {
width: 200rpx;
margin-right: 30rpx;
margin-bottom: 14rpx;
padding: 18rpx 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #333333;
background-color: #F5F5F5;
border-radius: 999rpx;
}
.le-dropdown-filter-box-active {
color: var(--dropdownThemeColor);
background-color: rgba(var(--dropdownThemeColorRgb), 0.04);
}
}
.le-dropdown-footer {
display: flex;
align-items: center;
margin-top: 14rpx;
.le-dropdown-reset {
flex: 1;
margin: 26rpx;
display: flex;
align-items: center;
justify-content: center;
height: 68rpx;
font-size: 28rpx;
background-color: #FFFFFF;
color: var(--dropdownThemeColor);
border: 2rpx solid var(--dropdownThemeColor);
border-radius: 999rpx;
}
.le-dropdown-confirm {
flex: 1;
margin: 26rpx;
display: flex;
align-items: center;
justify-content: center;
height: 68rpx;
font-size: 28rpx;
background-color: var(--dropdownThemeColor);
border: 2rpx solid var(--dropdownThemeColor);
color: #ffffff;
border-radius: 999rpx;
}
}
</style>