歡迎點(diǎn)擊領(lǐng)取 -《前端開發(fā)面試題進(jìn)階秘籍》:前端登頂之巔-最全面的前端知識(shí)點(diǎn)梳理總結(jié)
需求分析:
1、實(shí)現(xiàn)一個(gè)區(qū)域下拉選項(xiàng)與現(xiàn)有ui組件庫不同,支持多選、單選需求
2、支持選中區(qū)域后-全選中當(dāng)前區(qū)域下的所有城市信息
3、只能選中當(dāng)前一個(gè)區(qū)域的內(nèi)的城市其余城市禁用
擴(kuò)展思路:
1、封裝公共組件或者封裝在組件庫內(nèi)
2、出入?yún)⑾嚓P(guān)api透明好理解
3、支持單選或者多選,支持只選擇當(dāng)前區(qū)域下的城市或者全面區(qū)域下的城市
4、在原有的element plus下進(jìn)行擴(kuò)展延伸滿足需求
5、缺陷:未做maxLength-標(biāo)簽最大展示的api;這個(gè)按需自己修改一下就行
1、第一種模式:顯示區(qū)域信息
2、第二種模式:只展示城市內(nèi)容文章來源:http://www.zghlxwxcb.cn/news/detail-646050.html
1、相關(guān)開發(fā)代碼篇
創(chuàng)建文件:custom-select.vue文件;復(fù)制copy當(dāng)下代碼;
使用方式:
1、外部入?yún)⒗绯鞘校?code>dataSource=[{ label: 華北,value: '華東', children: [{ label: '山東',value: 'shandong'}]],樹形結(jié)構(gòu)
2、標(biāo)簽引用:<custom-select :disabled="true" :multilevel="true" height="32" v-model="checkGroup" :dataSource="cityList"></custom-select>
3、相關(guān)api說明文檔在文章底部文章來源地址http://www.zghlxwxcb.cn/news/detail-646050.html
<template>
<div
tabindex="1"
ref="customSelectRef"
@click="handleClickDiv"
@mouseenter="handelMouseEnter"
@mouseleave="handleMouseLeave"
:style="{ width: modelLabel && modelValue?.length ? '166px' : '100px', height: (height + 'px') || '25px' }"
:class="['custom-select_contaniner-i', isShowDropdown && 'custom-select_background']"
>
<div>
<span v-if="modelLabel" class="custom-tag">
<span>{{ modelLabel }}</span>
<i
class="custom_tag_delete"
@mouseenter="handelIconMouseEnter"
@mouseleave="handleIconMouseLeave"
@click.stop="handleDeleteIcon"
>
<svg
v-if="!ishShowIconDeleteText"
t="1678090923023"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="6709"
width="11"
height="11"
>
<path
d="M263.802377 224.219482a7.964444 7.964444 0 0 1 11.263425 0l236.934198 236.934198 236.934198-236.934198a7.964444 7.964444 0 0 1 11.263425 0l39.582895 39.582895a7.964444 7.964444 0 0 1 0 11.263425l-236.934198 236.934198 236.934198 236.934198a7.964444 7.964444 0 0 1 0 11.263425l-39.582895 39.582895a7.964444 7.964444 0 0 1-11.263425 0l-236.934198-236.934198-236.934198 236.934198a7.964444 7.964444 0 0 1-11.263425 0l-39.582895-39.582895a7.964444 7.964444 0 0 1 0-11.263425l236.934198-236.934198-236.934198-236.934198a7.964444 7.964444 0 0 1 0-11.263425l39.582895-39.582895z"
fill="#8a8a8a"
p-id="6710"
/>
</svg>
<svg
v-else
t="1678091410677"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="6936"
width="22"
height="22"
>
<path
d="M479.072 512l-98.72-98.72c-9.152-9.152-9.088-23.84 0-32.928 9.152-9.152 23.84-9.088 32.928 0l98.72 98.72 98.72-98.72c9.152-9.152 23.84-9.088 32.928 0 9.152 9.152 9.088 23.84 0 32.928l-98.72 98.72 98.72 98.72c9.152 9.152 9.088 23.84 0 32.928-9.152 9.152-23.84 9.088-32.928 0l-98.72-98.72-98.72 98.72c-9.152 9.152-23.84 9.088-32.928 0-9.152-9.152-9.088-23.84 0-32.928l98.72-98.72zM512 837.824c179.936 0 325.824-145.888 325.824-325.824s-145.888-325.824-325.824-325.824c-179.936 0-325.824 145.888-325.824 325.824s145.888 325.824 325.824 325.824z"
fill="#B7B8B9"
p-id="6937"
/>
</svg>
</i>
</span>
<span v-if="modelLabel && modelValue?.length > 1" class="custom-tag">+ {{ modelValue.length - 1 }}</span>
<span v-if="!modelLabel" class="cus_placeholder">{{ placeholder }}</span>
</div>
<i class="arrow-top-icon" v-if="!isShowIconRemove || !modelLabel" :class="[!isShowDropdown && 'arrow-top-icon-active']">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"
/>
</svg>
</i>
<i class="remove-icon" v-if="isShowIconRemove && modelLabel" @click.stop="handleRemove">
<svg
t="1678084213981"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4480"
width="11"
height="11"
>
<path
d="M512 32c265.097 0 480 214.903 480 480S777.097 992 512 992 32 777.097 32 512 246.903 32 512 32z m0 64C282.25 96 96 282.25 96 512s186.25 416 416 416 416-186.25 416-416S741.75 96 512 96z m169.706 246.294c12.496 12.497 12.496 32.758 0 45.255L557.256 512l124.45 124.452c12.496 12.497 12.496 32.758 0 45.255-12.497 12.496-32.758 12.496-45.255 0L512 557.254 387.549 681.706c-12.497 12.496-32.758 12.496-45.255 0-12.496-12.497-12.496-32.758 0-45.255l124.452-124.452-124.452-124.45c-12.496-12.497-12.496-32.758 0-45.255 12.497-12.496 32.758-12.496 45.255 0l124.452 124.45 124.45-124.45c12.497-12.496 32.758-12.496 45.255 0z"
fill="#8a8a8a"
p-id="4481"
/>
</svg>
</i>
</div>
<transition>
<div
v-if="isShowDropdown"
ref="cusSelectDropdown"
class="cus_select_background"
:style="{ minWidth: popperOffestWidth + 'px', zIndex: 99999 }"
>
<div v-if="multilevel" style="padding: 5px 20px;">
<div :key="key" v-for="(opt, key) in cusDataListChecked" class="multilevel_box">
<el-checkbox
style="width: 60px;"
v-model="opt.checkAll"
@change="handleCheckAllChange($event, opt)"
:indeterminate="opt.isIndeterminate"
:disabled="disabled && checkList.length ? !opt.checkList.length : false"
>
{{ opt.label }}
</el-checkbox>
<el-checkbox-group
v-model="opt.checkList"
v-if="opt.children"
@change="handleCheckedCitiesChange($event, opt)"
style="display: inline-block; padding-left: 20px"
>
<el-checkbox
:label="item.value"
style="width: 60px"
:key="index + Math.random()"
v-for="(item, index) in opt.children"
:disabled="disabled && checkList.length ? !opt.checkList.length : false"
>
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="cus_select_contaniner" v-else>
<div class="cus_select_left">中國</div>
<div class="cus_select_right">
<el-checkbox-group
v-model="checkList"
@change="handelCheckGroup"
style="display: inline-block; padding-left: 20px"
>
<el-checkbox
:key="index"
:label="item.value"
style="width: 60px"
v-for="(item, index) in dataSource"
>
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<span class="el-popper__arrow" data-popper-arrow="" style="position: absolute; left: 140px;"></span>
</div>
</transition>
</template>
<script setup lang="ts">
import { createPopper } from '@popperjs/core'
import { ref, onMounted, nextTick, watch, onUnmounted, toRaw, onBeforeMount, computed } from 'vue'
const props = withDefaults(
defineProps<{
height?: string | number
dataSource: any
modelValue?: any
placeholder?: string
multilevel?: boolean
disabled?: boolean
}>(),
{
height: 25,
disabled: false,
multilevel: false,
dataSource: [],
modelValue: [],
placeholder: '請(qǐng)選擇'
}
)
const emit = defineEmits(['update:modelValue'])
const customSelectRef = ref()
const cusSelectDropdown = ref()
const cusDataListChecked = ref<any[]>([])
const checkList = ref<string[]>([])
const popperOffestWidth = ref<number>(0)
const isShowDropdown = ref<boolean>(false)
const modelLabel = ref<string>('')
const isShowIconRemove = ref<boolean>(false)
const ishShowIconDeleteText = ref<boolean>(false)
const handleClickDiv = () => {
isShowDropdown.value = !isShowDropdown.value
}
const handelCheckGroup = (value) => {
const obj = props.dataSource.filter((item) => item.value === value[0])[0]
modelLabel.value = obj?.label
emit('update:modelValue', value)
}
const handelMouseEnter = () => {
isShowIconRemove.value = true
}
const handleMouseLeave = () => {
isShowIconRemove.value = false
}
const handleRemove = () => {
modelLabel.value = ''
checkList.value = []
if (isShowDropdown.value) {
isShowDropdown.value = false
}
if (props.multilevel) {
cusDataListChecked.value = addCheckProperties(props.dataSource)
}
emit('update:modelValue', [])
}
const handleDeleteIcon = () => {
isShowDropdown.value = false
checkList.value.splice(0, 1)
if (props.multilevel) return cusDataListChecked.value = findTreeChecked(cusDataListChecked.value)
const info = toRaw(checkList.value)[0]
const obj = props.dataSource.filter((item) => item.value === info)[0]
modelLabel.value = obj?.label || ''
}
const handelIconMouseEnter = () => {
ishShowIconDeleteText.value = true
}
const handleIconMouseLeave = () => {
ishShowIconDeleteText.value = false
}
// 點(diǎn)擊某個(gè)DOM元素之外的方法
const handlerDocClick = (event) => {
const isSelf = customSelectRef.value?.contains(event.target)
|| cusSelectDropdown.value?.contains(event.target)
if (!isSelf) {
isShowDropdown.value = false
}
}
/**
* 展示區(qū)域省份的邏輯
* */
const handleCheckAllChange = (bool: any, option) => {
const allCity = option.children ? option.children.map(item => item.value) : [option.value]
bool ? option.checkList = allCity : option.checkList = []
option.isIndeterminate = false
checkList.value = option.checkList
const newLabelArr = option.children
? option.children.filter(item => checkList.value.includes(item.value))
: checkList.value?.length ? [{ label: '默認(rèn)' }] : []
modelLabel.value = newLabelArr?.[0]?.label || ''
emit('update:modelValue', checkList.value)
}
const handleCheckedCitiesChange = (value: any[], option) => {
const checkedCount = value.length
const allCity = option.children ? option.children.map(item => item.value) : [option.value]
option.checkAll = checkedCount === allCity.length
option.isIndeterminate = checkedCount > 0 && checkedCount < allCity.length
checkList.value = option.checkList
const newLabelArr = option.children
? option.children.filter(item => checkList.value.includes(item.value))
: checkList.value?.length ? [{ label: '默認(rèn)' }] : []
modelLabel.value = newLabelArr?.[0]?.label || ''
emit('update:modelValue', checkList.value)
}
const addCheckProperties = (treeData) => {
let result = []
result = JSON.parse(JSON.stringify(treeData))
result.forEach(node => {
const child = node.children;
node.checkAll = false;
node.isIndeterminate = false;
node.checkList = [];
if (child && child.length > 0) {
addCheckProperties(child);
}
});
return result
}
const findTreeChecked = (treeData) => {
let newLabel
const val = toRaw(checkList.value)
const defaultBool = val.some(item => item.includes('default'))
treeData.forEach(node => {
if (node.children?.length) {
const child = node.children;
const bool = child.some(opt => val.includes(opt.value))
!newLabel ? newLabel = child.filter(item => val.includes(item.value))[0] : void null
if (bool) {
node.checkAll = val.length === child?.length;
node.isIndeterminate = val.length > 0 && val.length < child?.length;
node.checkList = val;
} else {
node.isIndeterminate = false
}
}
})
treeData[0].isIndeterminate = false;
treeData[0].checkAll = defaultBool ? true : false;
treeData[0].checkList = defaultBool ? ['default'] : [];
modelLabel.value = defaultBool ? '默認(rèn)' : newLabel?.label || ''
return treeData
}
watch(
[customSelectRef, cusSelectDropdown],
() => {
if (customSelectRef.value && cusSelectDropdown.value) {
createPopper(customSelectRef.value, cusSelectDropdown.value, {
placement: 'bottom',
modifiers: [
{
name: 'offset',
options: {
offset: [80, 8]
}
}
]
})
}
},
{
deep: true,
immediate: true
}
)
watch(
props.modelValue,
(newval) => {
if (!newval || !newval.length) return
checkList.value = props.modelValue
if (props.multilevel) return
const obj = props.dataSource.filter((item) => item.value === newval[0])[0]
modelLabel.value = obj?.label
},
{
deep: true,
immediate: true
}
)
onBeforeMount(() => {
if (props.multilevel) {
cusDataListChecked.value = addCheckProperties(props.dataSource)
}
})
onMounted(async () => {
await nextTick()
popperOffestWidth.value = customSelectRef.value.offsetWidth
document.addEventListener('click', handlerDocClick, true)
if (props.multilevel && props.modelValue.length) {
cusDataListChecked.value = findTreeChecked(cusDataListChecked.value)
}
})
onUnmounted(() => {
document.removeEventListener('click', handlerDocClick, true)
})
</script>
<script lang="ts">
export default { name: 'CustomSelect' }
</script>
<style lang="scss" scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.custom-select_contaniner-i {
width: 100%;
height: 25px;
padding: 7px 9px;
padding-left: 5px;
border-radius: 4px;
line-height: 1;
cursor: pointer;
position: relative;
user-select: none;
word-wrap: break-word;
word-break: break-all;
font-size: 13px;
flex-grow: 1;
display: inline-flex;
align-items: center;
box-sizing: border-box;
justify-content: space-between;
color: var(--el-input-text-color, var(--el-text-color-regular));
background-color: var(--el-input-bg-color, var(--el-fill-color-blank));
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
}
.custom-tag {
color: var(--el-color-info);
display: inline-flex;
justify-content: center;
align-items: center;
height: 18px;
padding: 0 9px;
line-height: 1;
border-radius: 4px;
white-space: nowrap;
font-size: 12px;
background-color: var(--el-fill-color);
}
.custom_tag_delete {
width: 18px;
margin-left: 5px;
font-size: 0px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: var(--el-color-info);
}
.custom-tag:first-child {
margin-right: 6px;
padding-right: 4px;
}
.arrow-top-icon {
width: 14px;
transform: rotateX(-180deg);
color: var(--el-text-color-placeholder);
}
.remove-icon {
margin-top: 2px;
color: var(--el-text-color-placeholder);
}
.arrow-top-icon-active {
transform: rotateX(0deg);
}
.custom-select:hover {
box-shadow: 0 0 0 1px var(--el-border-color-hover) inset;
}
.custom-select:focus {
outline: none;
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
.custom-select_background {
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
::-webkit-scrollbar {
width: 4px;
height: 4px;
background-color: transparent;
}
/*滾動(dòng)條的軌道*/
::-webkit-scrollbar-track {
background-color: transparent;
}
/*滾動(dòng)條的滑塊按鈕*/
::-webkit-scrollbar-thumb {
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.1);
box-shadow: inset 0 0 2px rgba(#000000, 0.04);
}
/*滾動(dòng)條的上下兩端的按鈕*/
::-webkit-scrollbar-button {
height: 0;
background-color: transparent;
}
.cus_select_contaniner {
padding: 5px 10px;
display: flex;
}
.cus_select_left {
width: 60px;
margin-top: 5px;
}
.cus_select_right {
flex: 1;
width: 480px;
}
.cus_select_background {
min-height: 200px;
box-sizing: border-box;
border-radius: 4px;
font-size: var(--el-font-size-base);
color: var(--el-text-color-regular);
background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color-light);
.multilevel_box {
display: flex;
padding: 5px;
border-bottom: 1px solid #e4e7ed;
}
.multilevel_box:last-child {
border-bottom: none;
}
}
.cus_placeholder {
color: var(--el-text-color-placeholder);
}
.el-popper__arrow {
top: -5px;
}
.el-popper__arrow {
position: absolute;
width: 10px;
height: 10px;
z-index: -1;
}
.el-popper__arrow::before {
border: 1px solid var(--el-border-color-light);
background: var(--el-bg-color-overlay);
right: 0;
border-bottom-color: transparent!important;
border-right-color: transparent!important;
}
</style>
2、組件-相關(guān)api說明
參數(shù) | 說明 | 類型 | 默認(rèn)值 | 必填項(xiàng) |
---|---|---|---|---|
height | 輸入框的高度 | String/Number | 25 | 否 |
dataSource | [{}]-label,value;樹形結(jié)構(gòu) | Array[] | [] | 是 |
modelValue | 當(dāng)前選中項(xiàng)內(nèi)容 | Array | [] | 否 |
placeholder | 輸入框內(nèi)容 | String | 請(qǐng)輸入 | 否 |
multilevel | 是否開啟跨層級(jí)模式 | Boolean | false | 否 |
disabled | 是否開啟跨層級(jí)禁用 | Boolean | false | 否 |
到了這里,關(guān)于vue3實(shí)現(xiàn)自定義select下拉框內(nèi)容之城市區(qū)域篇的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!