调整 el-select 高度之后 箭头错位问题解决

博客主要围绕调整 el-select 或 input 高度后,el-select 箭头错位的问题展开。介绍了解决方法,即恢复 el-input__icon 的 height 原属性,并借助相对定位完成位置移动。

调整 el-select 高度之后 箭头错位问题解决

在调整 el-select 或者 input 高度,比如

.el-input__inner {
	width: 272px;
	height: 32px;
	border: 1px solid #e0e0e0 !important;
}

成功之后,el-select箭头错位,情况类似如下图

在这里插入图片描述

解决方法

恢复el-input__icon的height原属性,同时借助相对定位完成位置移动。

.el-input__icon {
	height: 40px; // 设置高度,icon元素恢复原来的高度,这时arror才能垂直居中
	position: relative;
	top: -7px; // 元素整体上移,需要自己调整,让箭头也居中
}
// directives.js import Vue from "vue"; Vue.directive('dialog-drag-resize', { bind(el, binding) { // 拖动功能 const dialogHeader = el.querySelector('.el-dialog__header'); const dialog = el.querySelector('.el-dialog'); let isDragging = false; let startX, startY, startLeft, startTop; dialogHeader.style.cursor = 'move'; dialogHeader.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; startLeft = dialog.offsetLeft; startTop = dialog.offsetTop; dialog.style.position = 'fixed'; dialog.style.margin = '0'; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; dialog.style.left = `${startLeft + dx}px`; dialog.style.top = `${startTop + dy}px`; }); document.addEventListener('mouseup', () => { isDragging = false; document.body.style.userSelect = ''; }); // 缩放功能 const resizeHandle = document.createElement('div'); // 创建SVG字符串(这里是一个简单的缩放箭头图标) const svgStr = `<svg t="1764837362721" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1786" width="200" height="200"><path d="M852.68743 966.232318 171.311547 966.232318c-62.714867 0-113.562988-50.846038-113.562988-113.558335L57.748558 171.324994c0-62.712297 50.848122-113.558335 113.562988-113.558335l681.376907 0c62.714867 0 113.562988 50.846038 113.562988 113.558335l0 681.348989C966.250418 915.38628 915.40332 966.232318 852.68743 966.232318zM909.469948 171.324994c0-31.356149-25.424061-56.779168-56.781494-56.779168L171.311547 114.545826c-31.357433 0-56.781494 25.423019-56.781494 56.779168l0 681.348989c0 31.357172 25.424061 56.779168 56.781494 56.779168l681.376907 0c31.358457 0 56.781494-25.423019 56.781494-56.779168L909.469948 171.324994zM824.297706 483.610416c-15.665413 0-28.390747-12.697183-28.390747-28.389584l0.887243-186.638771L604.102866 461.264479l-40.145947-40.144302 193.023924-193.016015L568.782006 228.104161c-15.693044 0-28.390747-12.697183-28.390747-28.389584s12.697704-28.389584 28.390747-28.389584l254.711349 0c7.929925 0 15.082105 3.27151 20.238756 8.53949 5.490263 4.657067 8.955319 11.449773 8.955319 19.850094l0 255.506255C852.68743 470.913233 839.989727 483.610416 824.297706 483.610416zM455.219017 852.673983 200.506645 852.673983c-7.929925 0-15.082105-3.270487-20.239779-8.538467-5.489239-4.65809-8.955319-11.423167-8.955319-19.850094L171.311547 568.779168c0-15.692401 12.726357-28.389584 28.390747-28.389584 15.69202 0 28.390747 12.697183 28.390747 28.389584l-0.887243 186.6664 192.690312-192.710047 40.173577 40.143279-193.050531 193.016015 188.198837 0c15.69202 0 28.390747 12.697183 28.390747 28.389584C483.608741 839.9768 470.911038 852.673983 455.219017 852.673983z" fill="#272636" p-id="1787"></path></svg>`; // 将SVG字符串转换为Data URL const svgDataUrl = `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgStr)}")`; resizeHandle.className = 'resize-handle'; Object.assign(resizeHandle.style, { position: 'absolute', top: '0.8rem', right: '3rem', width: '1rem', height: '1rem', backgroundImage: svgDataUrl, backgroundSize: 'contain', backgroundRepeat: 'no-repeat', backgroundPosition: 'center', cursor: 'ne-resize', zIndex: '100' }); dialog.appendChild(resizeHandle); let isResizing = false; let startWidth, startHeight, startCX, startCY; const content = el.querySelector('.el-dialog__body'); resizeHandle.addEventListener('mousedown', (e) => { isResizing = true; startWidth = dialog.offsetWidth; startHeight = dialog.offsetHeight; startCX = e.clientX; startCY = e.clientY; dialog.style.transition = 'none'; // 禁用过渡避免闪烁 }); document.addEventListener('mousemove', (e) => { if (!isResizing) return; const dx = e.clientX - startCX; const dy = e.clientY - startCY; const newWidth = Math.max(200, startWidth + dx); // 最小宽度限制 const newHeight = Math.max(150, startHeight - dy); // 最小高度限制 dialog.style.width = `${newWidth}px`; dialog.style.height = `${newHeight}px`; // 内容等比例缩放 const scaleX = newWidth / startWidth; const scaleY = newHeight / startHeight; const scale = Math.min(scaleX, scaleY); // 取较小保持比例 // const content2 = el.querySelector('.el-dialog__body'); // content2.style.transform = `scale(${scale})`; // content2.style.transformOrigin = 'top left'; }); document.addEventListener('mouseup', () => { if (isResizing) { isResizing = false; dialog.style.transition = ''; // 恢复过渡效果 } }); } }); 这段代码自定义的放大缩小dialog框的功能第二次点击页面出现拖影
最新发布
12-05
<template> <view class="dropdown-wrapper"> <!-- 插槽:触发器 --> <slot name="reference" :open="open" :close="close" :toggle="toggle"> <!-- 默认触发器 --> <view class="dropdown-trigger-default" data-dropdown-trigger @tap="open"> 点击选择 ▼ </view> </slot> <!-- 浮动下拉菜单 --> <view v-if="visible" ref="dropdownRef" class="zy-popup-dropdown-menu" :style="$rpxStyle({ top: `${top}px`, right: `${right}px`, transform: positionStyle || position })" @touchmove.stop > <!-- 使用 scroll-view 实现可靠滚动 --> <scroll-view class="dropdown-content" :style="$rpxStyle({ maxHeight: props.maxHeight + 'px' })" scroll-y @touchstart="onContentTouchStart" @touchmove="onContentTouchMove" > <view v-for="item in props.data" :key="item[props.valueKey]" class="dropdown-item" :class="{ 'is-selected': props.modelValue === item[props.valueKey] }" @tap="handleSelect(item)" > <text class="item-label">{{ item[props.labelKey] }}</text> <uni-icons v-show="props.modelValue === item[props.valueKey]" class="icon-check" :type="checkIcon" color="#0f56d5" /> </view> </scroll-view> </view> </view> </template> <script setup> import { ref, nextTick, onUnmounted } from 'vue' // ---------------------- // Props 定义 // ---------------------- const props = defineProps({ // 数据源 [{ label: 'xxx', value: '1' }] data: { type: Array, required: true }, // 当前选中(v-model) modelValue: { type: [String, Number, Boolean, null], default: null }, // 显示字段名 labelKey: { type: String, default: 'name' }, // 字段名 valueKey: { type: String, default: 'value' }, // 触发器元素的 ID trigger: { type: String, default: '', required: true }, // 选中图标 checkIcon: { type: String, default: 'checkmarkempty' }, // 页面滚动是否关闭下拉框 scrollClose: { type: Boolean, default: true }, // 最大高度(px) maxHeight: { type: Number, default: 486 }, // 自定义定位 transform positionStyle: { type: String, default: '' }, parentInstance: { type: Object } }) // ---------------------- // Emits // ---------------------- const emit = defineEmits(['update:modelValue', 'change', 'open', 'close']) // ---------------------- // 内部状态 // ---------------------- const visible = ref(false) const top = ref(0) const left = ref(0) const right = ref(12) const position = ref('') const dropdownRef = ref(null) let observer = null let startY, startScrollTop // ---------------------- // 触摸事件处理(用于防止滚动穿透) // ---------------------- const onContentTouchStart = e => { const el = e.currentTarget startY = e.touches?.[0]?.clientY || 0 startScrollTop = el?.scrollTop || 0 } const onContentTouchMove = e => { const el = e.currentTarget if (!el) return const endY = e.touches?.[0]?.clientY if (!endY) return const direction = startY - endY > 0 ? 'down' : 'up' // 到达顶部且向上滑动 → 允许页面滚动(不做阻止) if (startScrollTop <= 0 && direction === 'up') { // 不阻止,交给外层处理 return } // 到达底部且向下滑动 if (el.scrollHeight - el.scrollTop <= el.clientHeight + 1 && direction === 'down') { // 不阻止,允许外层滚动 return } // 否则:正在内容内部滚动,阻止默认行为避免页面抖动 e.preventDefault() } // ---------------------- // 获取触发器位置 // ---------------------- async function getTriggerRect() { // 确保 DOM 更新完成 await nextTick() return new Promise(resolve => { const query = uni.createSelectorQuery() // 注意:H5 小程序平台写法略有不同,建议加上 .in(this) // 如果是在组件内,需要传入当前实例上下文 query .select('#' + props.trigger) .boundingClientRect(data => { if (data) { console.log('获取到元素位置:', data) resolve(data) } else { console.warn('未找到元素 #' + props.trigger) resolve(null) } }) .exec() }) } async function getTriggerRectT() { await nextTick() return new Promise(resolve => { const query = uni.createSelectorQuery() query .in(props.parentInstance) // ← 绑定到父组件作用域 .select('#' + props.trigger) .boundingClientRect(data => { if (data) { console.log('成功获取父组件元素位置:', data) resolve(data) } else { console.warn('未找到 #myTrigger') resolve(null) } }) .exec() }) } // ---------------------- // 打开下拉框 // ---------------------- // 定义一个异步函数 open,用于处理打开操作,接收事件对象 e 作为参数 const open = async e => { console.log('🚀 ~ open ~ e:', e) if (visible.value) { close() return } let rect = null if (props.parentInstance) { rect = await getTriggerRectT() } else { rect = await getTriggerRect() } console.log('🚀 ~ open ~ rect:', rect) if (!rect) return doOpen(rect) } const doOpen = rect => { console.log('🚀 ~ rect:', rect) const res = uni.getSystemInfoSync() const windowHeight = Number(res.windowHeight) console.log('🚀 ~ windowHeight 视口高度:', windowHeight) const windowWidth = Number(res.windowWidth) console.log('🚀 ~ windowWidth 视口宽度:', windowWidth) let maxHeight = null if (props.maxHeight < (props.data.length + 1) * 46) { maxHeight = props.maxHeight } else { maxHeight = (props.data.length + 1) * 46 } console.log('🚀 ~ maxHeight 弹窗最大高度:', maxHeight) const menuWidth = 215 const rightGap = 12 const spaceBelow = windowHeight - rect.bottom console.log('🚀 ~ spaceBelow: 安全距离', spaceBelow) const spaceAbove = rect.top console.log('🚀 ~ spaceAbove: 安全距离', spaceAbove) const isBothDirectionsInsufficient = spaceAbove < maxHeight && spaceBelow < maxHeight console.log('🚀 ~ isBothDirectionsInsufficient:', isBothDirectionsInsufficient) let finalTop, finalLeft, finalTransform if (isBothDirectionsInsufficient) { // 上下空间都不够 → 居中显示 finalTop = windowHeight / 2 finalTransform = 'translate(0, -50%)' finalLeft = windowWidth - menuWidth - rightGap if (finalLeft < 0) finalLeft = 0 } else { // 正常方向展开 const needUpward = spaceBelow < maxHeight console.log('🚀 ~ needUpward:', needUpward) if (needUpward) { // 弹窗在上方时,确保不覆盖触发元素 finalTop = rect.top - maxHeight // 可以添加一个小的间距,避免紧贴太近 finalTop = Math.max(0, finalTop - 5) } else { // 弹窗在下方时,紧贴触发元素底部 finalTop = rect.bottom } finalTransform = needUpward ? '' : '' finalLeft = windowWidth - menuWidth - rightGap if (finalLeft < 0) finalLeft = 0 } // 移除不必要的偏移 top.value = finalTop console.log('🚀 ~ top.value:', top.value) left.value = finalLeft console.log('🚀 ~ left.value:', left.value) position.value = finalTransform console.log('🚀 ~ position.value:', position.value) visible.value = true emit('open', props.modelValue, props.trigger) nextTick(() => { bindOutsideClickListener() bindScrollListener() }) } // ---------------------- // 关闭 & 切换 & 选择 // ---------------------- const close = () => { if (!visible.value) return visible.value = false emit('close', props.trigger) removeListeners() } const toggle = () => { visible.value ? close() : open() } const handleSelect = item => { const value = item[props.valueKey] const label = item[props.labelKey] const selected = { [props.valueKey]: value, [props.labelKey]: label } if (props.modelValue !== value) { emit('update:modelValue', value, props.trigger) emit('change', selected, props.trigger) } close() } // ---------------------- // 外部点击关闭 // ---------------------- const bindOutsideClickListener = () => { const handler = e => { const path = e.path || [] const isInside = path.some( node => node === dropdownRef.value?.$el || node?.classList?.contains('zy-popup-dropdown-menu') || node?.dataset?.dropdownTrigger ) if (!isInside) close() } uni.$on('onPageTap', handler) observer = { ...observer, cleanupTap: () => uni.$off('onPageTap', handler) } } // ---------------------- // 页面滚动关闭 // ---------------------- const bindScrollListener = () => { const scrollHandler = () => { if (!visible.value) return if (props.scrollClose) { close() } else { reposition() } } uni.$on('onPageScroll', scrollHandler) observer = { ...observer, cleanupScroll: () => uni.$off('onPageScroll', scrollHandler) } } // ---------------------- // 重新定位(滚动时调整位置) // ---------------------- const reposition = async () => { let rect = null if (props.parentInstance) { rect = await getTriggerRectT() } else { rect = await getTriggerRect() } if (!rect) return const res = uni.getSystemInfoSync() const windowHeight = Number(res.windowHeight) const windowWidth = Number(res.windowWidth) let maxHeight = null if (props.maxHeight < (props.data.length + 1) * 46) { maxHeight = props.maxHeight } else { maxHeight = (props.data.length + 1) * 46 } const menuWidth = 215 const rightGap = 12 const spaceBelow = windowHeight - rect.bottom const spaceAbove = rect.top const isBothDirectionsInsufficient = spaceAbove < maxHeight && spaceBelow < maxHeight let finalTop, finalLeft, finalTransform if (isBothDirectionsInsufficient) { finalTop = windowHeight / 2 finalTransform = 'translate(0, -50%)' finalLeft = windowWidth - menuWidth - rightGap if (finalLeft < 0) finalLeft = 0 } else { const needUpward = spaceBelow < maxHeight finalTop = needUpward ? rect.top - maxHeight : rect.bottom finalTransform = needUpward ? '' : '' finalLeft = windowWidth - menuWidth - rightGap if (finalLeft < 0) finalLeft = 0 } top.value = finalTop - 8 console.log('🚀 ~ reposition ~ top.value:', top.value) left.value = finalLeft console.log('🚀 ~ reposition ~ left.value:', left.value) position.value = finalTransform console.log('🚀 ~ reposition ~ position.value:', position.value) } // 防抖函数 const debounce = (fn, time = 50) => { let timer = null return (...args) => { if (timer) clearTimeout(timer) timer = setTimeout(() => fn.apply(this, args), time) } } const repositionDebounced = debounce(reposition, 50) // ---------------------- // 移除监听 // ---------------------- const removeListeners = () => { observer?.cleanupTap?.() observer?.cleanupScroll?.() observer = null } // ---------------------- // 暴露方法 // ---------------------- defineExpose({ open, close, toggle }) // ---------------------- // 卸载清理 // ---------------------- onUnmounted(() => { removeListeners() }) </script> <style scoped> /* 整体容器 */ .dropdown-wrapper { display: inline-block; } /* 默认触发器样式 */ .dropdown-trigger-default { display: inline-flex; align-items: center; justify-content: center; padding: 8px 16px; background-color: #fff; border: 1px solid #ddd; border-radius: 6px; font-size: 15px; color: #333; } /* 下拉菜单主体 */ .zy-popup-dropdown-menu { position: fixed; width: 215px; background-color: #ffffff; border-radius: 8px; z-index: 9999; display: flex; flex-direction: column; box-shadow: 0 4px 5px -3px rgba(0, 0, 0, 0.08), 0 8px 12px 1px rgba(0, 0, 0, 0.04), 0 3px 15px 3px rgba(0, 0, 0, 0.05); } /* 内容区可滚动(现在由 scroll-view 渲染) */ .dropdown-content { flex: 1; max-height: 486px; /* scroll-view 会接管滚动行为,无需 overflow */ } /* 每一项 */ .dropdown-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; font-size: 15px; color: #333333; border-bottom: 1px solid #f5f5f5; } .dropdown-item:last-child { border-bottom: none; } /* 选中项样式 */ .dropdown-item.is-selected { color: #0f56d5; font-weight: 500; } /* 文本自动换行 */ .item-label { flex: 1; word-break: break-word; line-height: 1.4; text-align: left; } /* 对号图标 */ .icon-check { font-family: 'erda' !important; font-size: 16px; margin-left: 8px; color: #0f56d5; } </style> 当前组件代码打开的弹窗位置没有紧贴住触发元素的上边框或者下边框,元素距离视口上边越近打开的弹窗就会距离触发元素越近,反之则越远
11-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值