摘要:
本文實現了一個Vue3摺疊面板組件,包含兩個核心文件:ibmCollpase.vue(父組件)和ibmCollpaseItem.vue(子組件)。父組件通過provide提供共享狀態和方法,子組件通過inject獲取。主要功能包括:
支持手風琴(accordion)模式和普通模式
提供beforeCollapse鈎子函數驗證
實現展開/摺疊動畫效果
支持自定義展開圖標位置
包含disabled禁用狀態處理
組件通過v-model雙向綁定activeNames,並暴露setActiveNames方法供外部調用。
//ibmCollpase.vue
<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
<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
<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>