448 lines
10 KiB
Vue
448 lines
10 KiB
Vue
<template>
|
||
<view class="uni-collapse-item">
|
||
<!-- onClick(!isOpen) -->
|
||
<view
|
||
@click="onClick(!isOpen)"
|
||
class="uni-collapse-item__title"
|
||
:class="{
|
||
'is-open': isOpen && titleBorder === 'auto',
|
||
'uni-collapse-item-border': titleBorder !== 'none',
|
||
}"
|
||
>
|
||
<view class="uni-collapse-item__title-wrap">
|
||
<slot name="title" :expanded="isOpen">
|
||
<view class="uni-collapse-item__title-box" :class="{ 'is-disabled': disabled }">
|
||
<image v-if="thumb" :src="thumb" class="uni-collapse-item__title-img" />
|
||
<text class="uni-collapse-item__title-text">{{ title }}</text>
|
||
</view>
|
||
</slot>
|
||
</view>
|
||
<view
|
||
v-if="showArrow && !$slots.title"
|
||
:class="{
|
||
'uni-collapse-item__title-arrow-active': isOpen,
|
||
'uni-collapse-item--animation': showAnimation === true,
|
||
}"
|
||
class="uni-collapse-item__title-arrow"
|
||
>
|
||
<view :color="disabled ? '#ddd' : '#bbb'" class="i-carbon-chevron-down" />
|
||
</view>
|
||
</view>
|
||
<view
|
||
class="uni-collapse-item__wrap"
|
||
:class="{ 'is--transition': showAnimation }"
|
||
:style="{ height: (isOpen ? height : 0) + 'px' }"
|
||
>
|
||
<view
|
||
:id="elId"
|
||
ref="collapseHook"
|
||
class="uni-collapse-item__wrap-content"
|
||
:class="{ open: isheight, 'uni-collapse-item--border': border && isOpen }"
|
||
>
|
||
<slot></slot>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {
|
||
ref,
|
||
inject,
|
||
watch,
|
||
onMounted,
|
||
onUnmounted,
|
||
onUpdated,
|
||
getCurrentInstance,
|
||
nextTick,
|
||
} from 'vue'
|
||
|
||
// #ifdef APP-NVUE
|
||
const dom = weex.requireModule('dom')
|
||
// #endif
|
||
|
||
/**
|
||
* CollapseItem 折叠面板子组件
|
||
* @description 折叠面板子组件
|
||
* @property {String} title 标题文字
|
||
* @property {String} thumb 标题左侧缩略图
|
||
* @property {String} name 唯一标志符
|
||
* @property {Boolean} open = [true|false] 是否展开组件
|
||
* @property {Boolean} titleBorder = [true|false] 是否显示标题分隔线
|
||
* @property {String} border = ['auto'|'show'|'none'] 是否显示分隔线
|
||
* @property {Boolean} disabled = [true|false] 是否展开面板
|
||
* @property {Boolean} showAnimation = [true|false] 开启动画
|
||
* @property {Boolean} showArrow = [true|false] 是否显示右侧箭头
|
||
*/
|
||
|
||
const props = defineProps({
|
||
// 列表标题
|
||
title: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
name: {
|
||
type: [Number, String],
|
||
default: '',
|
||
},
|
||
// 是否禁用
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
// #ifdef APP-PLUS
|
||
// 是否显示动画,app 端默认不开启动画,卡顿严重
|
||
showAnimation: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
// #endif
|
||
// #ifndef APP-PLUS
|
||
// 是否显示动画
|
||
showAnimation: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
// #endif
|
||
// 是否展开
|
||
open: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
// 缩略图
|
||
thumb: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
// 标题分隔线显示类型
|
||
titleBorder: {
|
||
type: String,
|
||
default: 'auto',
|
||
},
|
||
border: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
showArrow: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
})
|
||
|
||
// 生成随机元素ID,用于解决百度小程序获取同一个元素位置信息的bug
|
||
const elId = ref(`Uni_${Math.ceil(Math.random() * 10e5).toString(36)}`)
|
||
const isOpen = ref(false)
|
||
const isheight = ref(null)
|
||
const height = ref(0)
|
||
const nameSync = ref('0')
|
||
const collapseHook = ref(null)
|
||
const proxy = getCurrentInstance()
|
||
|
||
// 获取折叠面板父组件上下文
|
||
const collapseContext = inject('uniCollapseContext', null)
|
||
|
||
// 监听open属性变化
|
||
watch(
|
||
() => props.open,
|
||
(val) => {
|
||
isOpen.value = val
|
||
onClick(val, 'init')
|
||
},
|
||
)
|
||
|
||
// 初始化方法
|
||
const init = (type) => {
|
||
// #ifndef APP-NVUE
|
||
getCollapseHeight(type)
|
||
// #endif
|
||
// #ifdef APP-NVUE
|
||
getNvueHwight(type)
|
||
// #endif
|
||
}
|
||
|
||
// 卸载组件时从父组件中移除
|
||
const uninstall = () => {
|
||
if (collapseContext) {
|
||
collapseContext.unregister({
|
||
nameSync: nameSync.value,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 点击事件处理 - 修复版
|
||
const onClick = (isOpenVal, type) => {
|
||
if (props.disabled) return
|
||
|
||
// 保存旧状态用于比较
|
||
const oldIsOpen = isOpen.value
|
||
|
||
// 设置新状态
|
||
isOpen.value = isOpenVal
|
||
|
||
// 如果是打开操作且在手风琴模式下,关闭其他面板
|
||
if (isOpen.value && !oldIsOpen && collapseContext) {
|
||
// 传递一个包含必要信息的对象,确保能被识别
|
||
collapseContext.setAccordion({
|
||
nameSync: nameSync.value,
|
||
uid: proxy.uid,
|
||
instance: proxy,
|
||
})
|
||
}
|
||
|
||
// 非初始化操作时,触发onChange回调
|
||
if (type !== 'init' && collapseContext) {
|
||
collapseContext.onChange(isOpen.value, {
|
||
nameSync: nameSync.value,
|
||
isOpen: isOpen.value,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 获取折叠面板高度(非APP-NVUE)
|
||
const getCollapseHeight = (type, index = 0) => {
|
||
const views = uni.createSelectorQuery().in(proxy)
|
||
views
|
||
.select(`#${elId.value}`)
|
||
.fields(
|
||
{
|
||
size: true,
|
||
},
|
||
(data) => {
|
||
// TODO 百度中可能获取不到节点信息,需要循环获取
|
||
if (index >= 10) return
|
||
if (!data) {
|
||
index++
|
||
getCollapseHeight(false, index)
|
||
return
|
||
}
|
||
|
||
// #ifdef APP-NVUE
|
||
height.value = data.height + 1
|
||
// #endif
|
||
// #ifndef APP-NVUE
|
||
height.value = data.height
|
||
// #endif
|
||
|
||
isheight.value = true
|
||
if (type) return
|
||
onClick(isOpen.value, 'init')
|
||
},
|
||
)
|
||
.exec()
|
||
}
|
||
|
||
// 获取NVUE环境下的高度
|
||
const getNvueHwight = (type) => {
|
||
// #ifdef APP-NVUE
|
||
dom.getComponentRect(collapseHook.value, (option) => {
|
||
if (option && option.result && option.size) {
|
||
height.value = option.size.height + 1
|
||
isheight.value = true
|
||
if (type) return
|
||
onClick(props.open, 'init')
|
||
}
|
||
})
|
||
// #endif
|
||
}
|
||
|
||
// 组件挂载后初始化 - 注册部分的修改
|
||
onMounted(() => {
|
||
if (!collapseContext) {
|
||
// console.error('找不到折叠面板父组件上下文!')
|
||
return
|
||
}
|
||
|
||
// 设置nameSync
|
||
nameSync.value = props.name !== '' ? props.name.toString() : `${collapseContext.childrens.length}`
|
||
|
||
// console.log('折叠项挂载:', nameSync.value)
|
||
|
||
// 注册到父组件 - 确保传递所有必要的参数
|
||
collapseContext.register({
|
||
nameSync: nameSync.value,
|
||
isOpen: isOpen.value,
|
||
instance: proxy,
|
||
uid: proxy.uid,
|
||
// 添加方法引用,使父组件可以直接操作
|
||
setOpen: (val) => {
|
||
isOpen.value = val
|
||
nextTick(() => init())
|
||
},
|
||
})
|
||
|
||
// 初始化动画高度
|
||
init()
|
||
|
||
// 检查是否应该默认展开
|
||
nextTick(() => {
|
||
if (collapseContext.getActiveNames) {
|
||
const activeNames = collapseContext.getActiveNames()
|
||
// console.log('当前活动项:', activeNames, '本项:', nameSync.value)
|
||
if (activeNames.includes(nameSync.value.toString())) {
|
||
// console.log('默认展开:', nameSync.value)
|
||
isOpen.value = true
|
||
// 更新高度
|
||
init()
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
// 组件更新后重新初始化
|
||
onUpdated(() => {
|
||
nextTick(() => {
|
||
init(true)
|
||
})
|
||
})
|
||
|
||
// 组件卸载时从父组件移除
|
||
onUnmounted(() => {
|
||
uninstall()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.uni-collapse-item {
|
||
/* #ifndef APP-NVUE */
|
||
box-sizing: border-box;
|
||
|
||
/* #endif */
|
||
&__title {
|
||
/* #ifndef APP-NVUE */
|
||
display: flex;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
/* #endif */
|
||
flex-direction: row;
|
||
align-items: center;
|
||
transition: border-bottom-color 0.3s;
|
||
|
||
// transition-property: border-bottom-color;
|
||
// transition-duration: 5s;
|
||
&-wrap {
|
||
width: 100%;
|
||
flex: 1;
|
||
}
|
||
|
||
&-box {
|
||
padding: 0 15px;
|
||
/* #ifndef APP-NVUE */
|
||
display: flex;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
/* #endif */
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
height: 48px;
|
||
line-height: 48px;
|
||
background-color: #fff;
|
||
color: #303133;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
/* #ifdef H5 */
|
||
cursor: pointer;
|
||
outline: none;
|
||
|
||
/* #endif */
|
||
&.is-disabled {
|
||
.uni-collapse-item__title-text {
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
&.uni-collapse-item-border {
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
&.is-open {
|
||
border-bottom-color: transparent;
|
||
}
|
||
|
||
&-img {
|
||
height: 22px;
|
||
width: 22px;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
&-text {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
/* #ifndef APP-NVUE */
|
||
white-space: nowrap;
|
||
color: inherit;
|
||
/* #endif */
|
||
/* #ifdef APP-NVUE */
|
||
lines: 1;
|
||
/* #endif */
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
&-arrow {
|
||
/* #ifndef APP-NVUE */
|
||
display: flex;
|
||
box-sizing: border-box;
|
||
/* #endif */
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 20px;
|
||
height: 20px;
|
||
margin-right: 10px;
|
||
transform: rotate(0deg);
|
||
|
||
&-active {
|
||
transform: rotate(-180deg);
|
||
}
|
||
}
|
||
}
|
||
|
||
&__wrap {
|
||
/* #ifndef APP-NVUE */
|
||
will-change: height;
|
||
box-sizing: border-box;
|
||
/* #endif */
|
||
background-color: #fff;
|
||
overflow: hidden;
|
||
position: relative;
|
||
height: 0;
|
||
|
||
&.is--transition {
|
||
// transition: all 0.3s;
|
||
transition-property: height, border-bottom-width;
|
||
transition-duration: 0.3s;
|
||
/* #ifndef APP-NVUE */
|
||
will-change: height;
|
||
/* #endif */
|
||
}
|
||
|
||
&-content {
|
||
position: absolute;
|
||
font-size: 13px;
|
||
color: #303133;
|
||
// transition: height 0.3s;
|
||
border-bottom-color: transparent;
|
||
border-bottom-style: solid;
|
||
border-bottom-width: 0;
|
||
|
||
&.uni-collapse-item--border {
|
||
border-bottom-width: 1px;
|
||
border-bottom-color: red;
|
||
border-bottom-color: #ebeef5;
|
||
}
|
||
|
||
&.open {
|
||
position: relative;
|
||
}
|
||
}
|
||
}
|
||
|
||
&--animation {
|
||
transition-property: transform;
|
||
transition-duration: 0.3s;
|
||
transition-timing-function: ease;
|
||
}
|
||
}
|
||
</style>
|