vue3 本文实现了一个Vue3折叠面板组件

摘要:

本文实现了一个Vue3折叠面板组件,包含两个核心文件:ibmCollpase.vue(父组件)和ibmCollpaseItem.vue(子组件)。父组件通过provide提供共享状态和方法,子组件通过inject获取。主要功能包括:

支持手风琴(accordion)模式和普通模式

提供beforeCollapse钩子函数验证

实现展开/折叠动画效果

支持自定义展开图标位置

包含disabled禁用状态处理

组件通过v-model双向绑定activeNames,并暴露setActiveNames方法供外部调用。

//ibmCollpase.vue

html 复制代码
<script setup>
import { provide, computed, watch,defineEmits } from 'vue';

const props = defineProps({
  accordion: Boolean,
  expandIconPosition: {
    type: String,
    default: 'right'
  },
  beforeCollapse:Function,
  
})

const modelValue = defineModel({
  type: [Array, String, Number],
  default: () => []
})


const emit = defineEmits(['change'])

// 修复 ensureArray 函数
const ensureArray = (value) => { 
  
  if (value === undefined || value === null || value === '') return []
  
  let result = []
  
  if (typeof value === 'string') {
    // 将字符串拆分为单个字符的数组
    result = value.split('')    
  } else if (typeof value === 'number') {
    // 数字转为字符串再拆分
    result = String(value).split('')
  } else if (Array.isArray(value)) {
    result = [...value]
  }
  
  // 根据 accordion 模式过滤结果
  if (props.accordion && result.length > 0) {
    return [result[0]] // 手风琴模式只取第一个元素
  }
  
  return result
}

// 使用计算属性 - 修复这里
const activeNames = computed({
  get: () => {
    const result = ensureArray(modelValue.value)    
    return result
  },
  set: (value) => {    
    // 直接设置到 modelValue,让父组件同步更新
    modelValue.value = value
  }
})

// 点击面板项的处理

const handleClick = (name) => {

  const {beforeCollapse}= props
  // 如果没有传入beforeCollapse方法,则直接执行_handleClick方法  
  if(!beforeCollapse){
    _handleClick(name)     
    return
  }
  
  const sholdChange=beforeCollapse(name)
  const bool= [isBoolean(sholdChange),isPromise(sholdChange)].includes(true)
  
  if(!bool){
  throw new Error('beforeCollapse 必须返回布尔值或 Promise 对象')
  }
  if(isPromise(sholdChange)){
    sholdChange.then((res) => {
      //调的是resolve方法
      if(res!=false){
        handleClick(name)
      }
  }).catch((error) => {
    //调的是reject方法
    //不会调用_handleClick方法
  })
  }
  else if(sholdChange){      
      _handleClick(name)
    }  
}

//判断是不是布尔类型
const isBoolean = (value) => {
  return typeof value === 'boolean'
}

//判断是不是promise类型
const isPromise = (value) => {
  return value instanceof Promise
}

const _handleClick = (name) => {
  
  
  const currentValue = [...activeNames.value]
  
  if (props.accordion) {
    const index = currentValue.indexOf(name)
    activeNames.value = index > -1 ? [] : [name]
    emit('change', activeNames)
  } else {
    const index = currentValue.indexOf(name)
    if (index > -1) {
      currentValue.splice(index, 1)
    } else {
      currentValue.push(name)
    activeNames.value = currentValue
    emit('change', activeNames)
    }
  }     
  activeNames.value = currentValue    
  emit('change', activeNames)
}


// provide 依赖注入
provide("collapseContentKey", {
  activeNames,
  handleClick,  
})

// 修复 setActiveNames 方法
const setActiveNames = (_activeNames) => {         
  activeNames.value = ensureArray(_activeNames)
}


defineExpose({
  activeNames,
  setActiveNames,
})



</script>


<template>
    <!-- collpase 折叠组件 -->
     <!-- 这里是折叠组件的容器  开始 
      icon-position-left/right 这个class是控制折叠面板的标题位置
      expand-icon-position: left/right 这个class是控制展开按钮的位置
      默认在右边
     -->      
    <div class="ibc-collpase" :class="[`icon-position-${props.expandIconPosition}`]">
      <!-- 面板项 开始 
       面板展开效果,通过is-active来控制   
       is-disabled 禁用状态
      -->
      <slot></slot>      
    
    </div>
    <!-- 这里是折叠组件的容器  结束 -->


</template>











<style scoped>
/* CSS代码全部写这里 */

.ibc-collpase{
  border-top:1px solid #ccc;  
}

</style>

//ibmCollpaseItem

html 复制代码
<script setup>
import { ref, inject, defineProps,defineExpose, computed, useId, watch } from 'vue'
// const isActive = ref(false)

//defineProps的作用是:定义props,并将props注入到当前组件的实例上
//props是父组件传递给子组件的自定义属性,子组件可以通过props来接收父组件传递过来的数据
const props = defineProps({
    name: String,
    disabled: Boolean,
    title: String,
})

//在这里,我们要判断是否有传name,如果没有传,则要给name一个唯一的默认值,防止报错
//如果传了name,则不处理
const name = props.name == undefined ? useId() : props.name


// 注入父组件的状态
//inject 从父组件接收数据
//父组件里有 collapseContenKey 带有值,值的内容如下:
//父组件里有 activeNames 数组
//父组件里有 handleClick 方法
const collapseContent = inject("collapseContentKey")


//定义一个变量,用来保存当前组件最开始渲染时的状态
let isStartDisabled = collapseContent.activeNames.value.includes(name)

watch(
    () => props.disabled,
    () => {
        isStartDisabled = collapseContent.activeNames.value.includes(name)
    }
);


//操作到这里,我们拿到了子组件的自定义属性name,以及父组件传递过来的activeNames数组
//我们可以根据activeNames数组完成isActive的状态的切换
const isActive = computed(() => {
    if (props.disabled)
        return isStartDisabled
    return collapseContent.activeNames.value.includes(name)
})

function onBeforeEnter(el) {
    el.style.height = '0px'
}

function onEnter(el, done) {
    el.style.height = el.scrollHeight + 'px'
    // done()
}

function onBeforeLeave(el) {
    el.style.height = el.scrollHeight + 'px'
}

function onLeave(el, done) {
    el.style.height = '0px'
    // done()
}

//点击标题问题
const handleClick = () => {
    // 判断当前是否禁用
    if (props.disabled) {
        return
    }
    // 切换isActive状态    
    collapseContent.handleClick(name)
}

defineExpose({
    isActive,    
})

</script>


<template>
    <!-- 面板项 开始 
       面板展开效果,通过is-active来控制   
       is-disabled 禁用状态
    -->

    <div class="ibc-collpase-item" :class="{
        'is-active': isActive,
        'is-disabled': disabled,
    }">
        <!-- 面板项头部 开始 -->
        <div class="ibc-collpase-item__header" @click='handleClick'>
            <div class="ibc-collpase-item__title" :class="{ 'is-active': isActive }">
                <slot name="title" :isActive="isActive" >
                    {{ props.title }}
                </slot>
            </div>
            <div class="ibc-collpase-item__icon" :class="{ 'is-active': isActive }">
                <slot name="icon" :isActive="isActive">
                    <div class="ibc-icon-arrow"></div>
                </slot>
            </div>
        </div>
        <!-- 面板项头部 结束 -->

        <!-- 面板项内容 开始 
            style='height: 200px;' 控制面板项内容的高度
        -->
        <Transition @before-enter="onBeforeEnter" @enter="onEnter" @before-leave="onBeforeLeave" @leave="onLeave">
            <div class="ibc-collpase-item__wrap" v-show="isActive">
                <div class="ibc-collpase-item__content">
                    <slot></slot>
                </div>
            </div>
        </Transition>
        <!-- 面板项内容 结束 -->
    </div>
    <!-- 面板项 结束 -->
</template>


<style scoped>
/* CSS代码全部写这里 */
.ibc-collpase-item {
    border-bottom: 1px solid #ccc;
}

.ibc-collpase-item__header {
    height: 80px;
    /* background-color: skyblue; */
    display: flex;
    align-items: center;
    cursor: pointer;
}

.ibc-collpase-item__title {
    flex: 1;
    /* 占据剩余空间 */
    font-size: 18px;
}

.ibc-icon-arrow {
    width: 24px;
    height: 24px;
    /* border:1px solid red; */
    display: flex;
    align-items: center;
    justify-content: center;

    transition: transform 0.3s ease-in-out;
}

.ibc-icon-arrow::before {
    content: "";
    width: 12px;
    height: 12px;
    display: block;
    border-top: 1px solid #333;
    border-right: 1px solid #333;
    transform: translateX(-25%) rotate(45deg);
}

.ibc-collpase-item.is-active .ibc-collpase-item__title {
    color: tomato;
}

.ibc-collpase-item__icon.is-active .ibc-icon-arrow {
    transform: rotate(90deg);
}

.ibc-collpase-item__icon.is-active .ibc-icon-arrow::before {
    border-color: tomato;
}


.ibc-collpase.icon-position-right .ibc-collpase-item__title {
    order: 0;
    /* 标题在右边 */
}

.ibc-collpase.icon-position-left .ibc-collpase-item__title {
    order: 1;
    /* 标题在左边 */
}

.ibc-collpase-item__content {
    line-height: 25px;
    font-size: 14px;
    padding-bottom: 20px;
    color: brown;
}

.ibc-collpase-item__wrap {
    /* height: 0; */
    overflow: hidden;
    /* transition: height 2s ease-in-out; */
}

.ibc-collpase-item:hover .ibc-collpase-item__wrap {
    /* height: 200px; */
}

.ibc-collpase-item.is-disabled .ibc-collpase-item__title {
    color: #ddd;
}

.ibc-collpase-item.is-disabled .ibc-icon-arrow::before {
    border-color: #ddd;
}

.ibc-collpase-item.is-disabled .ibc-collpase-item__header {
    cursor: not-allowed;
}

.v-enter-from,
.v-leave-to {
    opacity: 0;
}

.v-enter-active,
.v-leave-active {
    transition: height 0.5s ease-in-out, opacity 0.5s;
}

.v-enter-to,
.v-leave-from {
    opacity: 1;
}
</style>

//app.vue

html 复制代码
<script setup>
// JavaScript代码全部写这里
import IbcCollapse from './components/collpase/ibmCollpase.vue'
import IbmCollpaseItem from './components/collpase/ibmCollpaseItem.vue';
import { ref, watch,useTemplateRef, onMounted, nextTick } from 'vue';
// 用我们这个折叠组件,用户一个需求,他希望能人为的指定某个折叠项的初始状态是打开还是关闭,而不是默认的全部都是打开的。
// 所以我们需要在组件中增加一个属性,用来控制某个折叠项的初始状态。
//定义一个数组,把需要展开的项放在一个数组中。
//ref 创建一个响应式数据,可以绑定到组件的data中。通过v-model绑定到组件的modelValue中。
// v-model=activeNames,把activeNames绑定到组件的modelValue中。
//const activeNames=ref(["a","b","c"]); // 这里指定了初始状态为打开的项是a和c。
const activeNames = ref(["a", 'b',"c","d"]); // 这里指定了初始状态为打开的项是a和c。


const collapse=useTemplateRef('collapse');

onMounted(async () => {
  // 监听activeNames变化,把变化同步到collapse的modelValue中。  
  //collapse.value.setActiveNames("abcdef");
    
// 等待下一个 tick 确保响应式更新完成
  await nextTick()  
  
  console.log("onMounted--collapse.value.activeNames-", collapse.value.activeNames,); 
   
});

const onChange = (activeNames) => {  
  console.log("onChange---", activeNames.value);
}
function  onBeforeCollapse () {    
  return new Promise((resolve, reject) => {
    resolve(false);
  });
}

const collpaseItem=useTemplateRef('collpaseItem');

onMounted(async () => {    
// 等待下一个 tick 确保响应式更新完成
  await nextTick()  
  console.log("onMounted---", collpaseItem.value.isActive); 
   
});



//如何禁用某个面板项?
// 我们可以给某个面板项增加一个属性,用来控制是否禁用。
// 我们可以给这个属性绑定一个布尔值,当为true时,表示禁用,false时,表示启用。disabled属性的默认值是false。
// 我们可以在组件中增加一个disabled属性,用来控制某个面板项是否禁用。
// 我们可以在handleClick方法中增加一个判断,如果某个面板项被禁用,则不做任何操作。


//如果用户加了accordion属性,表示手风琴效果,则只能展开一个面板项。
// 我们可以在组件中增加一个accordion属性,用来控制是否是手风琴效果。

</script>

<template> <!-- HTML代码全部写这里 -->
  <div class="wrap">
    <IbcCollapse v-model="activeNames" expand-icon-position="left"   
    ref="collapse"
    @change="onChange"    
    >
    <!-- :before-collapse="onBeforeCollapse" -->

      <IbmCollpaseItem name="a" title="面板项一的标题" disabled>
        <p> 面板项2的内容 </p><p> 面板项2的内容 </p><p> 面板项2的内容 </p><p> 面板项2的内容 </p><p> 面板项2的内容 </p>
      </IbmCollpaseItem>
      <IbmCollpaseItem name="x" ref="collpaseItem" >
        <template #title="{ isActive }">面板项2的标题                  
          <span class="ibc-collpase-item__title_left">
            {{ isActive ? '▼' : '◀' }}
          </span>
        </template>
        <template #icon="{ isActive }">
          <div class="icon">
            {{ isActive ? '▼' : '▶' }}
          </div>
        </template>
        <p> 面板项2的内容 </p><p> 面板项2的内容 </p><p> 面板项2的内容 </p><p> 面板项2的内容 </p><p> 面板项2的内容 </p>
      </IbmCollpaseItem>
      <IbmCollpaseItem name="c" title="面板项三的标题">
        <template #icon="{ isActive }">
          <div class="icon">
            {{ isActive ? '▼' : '▶' }}
          </div>
        </template>
        <p> 面板项3的内容 </p><p> 面板项3的内容 </p>        
      </IbmCollpaseItem>
      <IbmCollpaseItem name="d" title="面板项四的标题"></IbmCollpaseItem>
      <IbmCollpaseItem name="e" title="面板项五的标题"></IbmCollpaseItem>
    </IbcCollapse>
  </div>
</template>











<style scoped>
/* CSS代码全部写这里 */

.icon {
  width: 24px;
  height: 24px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 20px;
  /* border: 1px solid red; */
}

.ibc-collpase-item__icon.is-active .icon {
  color: tomato;
}

.wrap {
  width: 600px;
  min-height: 200px;
  margin: 50px auto;
  /* background-color: khaki; */
}
.ibc-collpase.icon-position-left .ibc-collpase-item__title_left {
  display: inline-block;
  width: 24px;
  height: 24px;
  margin-right: 10px;
  font-size: 20px;
  text-align: center;
  /* border: 1px solid blue; */
}

.ibc-collpase.icon-position-right .ibc-collpase-item__title_left {
  display: none;  
}
</style>
相关推荐
我叫张小白。39 分钟前
Vue3 响应式数据:让数据拥有“生命力“
前端·javascript·vue.js·vue3
zzlyx991 小时前
用C#采用Avalonia+Mapsui在离线地图上插入图片画信号扩散图
java·开发语言·c#
IT_陈寒1 小时前
React 18并发渲染实战:5个核心API让你的应用性能飙升50%
前端·人工智能·后端
Yue丶越1 小时前
【C语言】自定义类型:结构体
c语言·开发语言
合作小小程序员小小店1 小时前
桌面开发,点餐管理系统开发,基于C#,winform,sql server数据库
开发语言·数据库·sql·microsoft·c#
科普瑞传感仪器1 小时前
从轴孔装配到屏幕贴合:六维力感知的机器人柔性对位应用详解
前端·javascript·数据库·人工智能·机器人·自动化·无人机
笃行客从不躺平1 小时前
线程池监控是什么
java·开发语言
星轨初途1 小时前
C++的输入输出(上)(算法竞赛类)
开发语言·c++·经验分享·笔记·算法
n***F8751 小时前
SpringMVC 请求参数接收
前端·javascript·算法