22/10/13 vue中box-shadow不生效

本文介绍了一种在CSS中使用box-shadow属性渲染阴影效果的方法。通过调整属性值,可以解决某些情况下阴影无法正常显示的问题。

背景
代码

box-shadow: rgb(0 0 0 / 20%) 0px 2px 1px -1px,
        rgb(0 0 0 / 14%) 0px 1px 1px 0px, rgb(0 0 0 / 12%) 0px 1px 3px 0px;

在这里插入图片描述
页面上的效果,这样写无效
在这里插入图片描述

而在html中就能够渲染
在这里插入图片描述


解决方法 写成这种

    box-shadow: 0 0px 2px rgba(0, 0, 0, .2);
<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="{ top: `${top}px`, right: `${right}px`, transform: positionStyle?positionStyle:position, maxHeight: `${props.maxHeight}px` }" @touchmove.stop > <view class="dropdown-content"> <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> </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: { // 选中时候的icon type: String, default: 'checkmarkempty' }, scrollClose: { // 页面滚动是否关闭已打开组件 type: Boolean, default: true }, maxHeight: { // 展开组件的最大高度(仅传入数字即可,需要进行计算,单位是px) type: Number, default: 486 }, positionStyle: { // 展开组件的position样式 type: String, default: '' } }) // ---------------------- // 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('translateY(8px)') const dropdownRef = ref(null) let observer = null // ---------------------- // 获取触发器位置(核心方法) function getTriggerRect() { return new Promise(resolve => { // console.log('🚀 ~ getTriggerRect ~ props.trigger:', props.trigger) uni .createSelectorQuery() .select('#' + props.trigger) .boundingClientRect(rect => { if (rect) { // console.log('✅ 定位成功:', rect) resolve(rect) } else { // console.error('❌ 查询失败,请确认 ID 是否正确或已渲染') resolve(null) } }) .exec() }) } // ---------------------- // 打开下拉框 // ---------------------- const open = async () => { if (visible.value) { close() return } const rect = await getTriggerRect() if (!rect) return doOpen(rect) // 抽离打开逻辑 } const doOpen = rect => { // 获取设备系统信息(同步) const res = uni.getSystemInfoSync() const windowHeight = Number(res.windowHeight) // 可视窗口高度(单位 px) const windowWidth = Number(res.windowWidth) // 可视窗口宽度(单位 px) const maxHeight = props.maxHeight // 弹窗最大高度(CSS 中设定的最大高度) const menuWidth = 215 // 弹窗固定宽度,必须与 CSS 一致(避免计算偏差) const rightGap = 12 // 弹窗右侧留白距离(安全边距) console.log('【doOpen】触发打开菜单', { res, rect, windowHeight, windowWidth }) // 计算触发元素上下方可用空间 const spaceBelow = windowHeight - rect.bottom // 下方剩余空间 const spaceAbove = rect.top // 上方剩余空间 // 判断上下空间是否都不足以放下弹窗(maxHeight) const isBothDirectionsInsufficient = spaceAbove < maxHeight && spaceBelow < maxHeight console.log('【空间判断】', { spaceAbove, // 上方还有多少空间 spaceBelow, // 下方还有多少空间 maxHeight, // 需要的高度 isBothDirectionsInsufficient: isBothDirectionsInsufficient // 是否上下都不够 }) let finalTop, finalLeft, finalTransform if (isBothDirectionsInsufficient) { // ======================== // 🎯 情况1:上下空间都不够 → 居中显示 // ======================== // 🔹 Y 轴定位:将弹窗垂直居中于屏幕中央 finalTop = windowHeight / 2 // 🔹 使用 transform 实现真正的居中(X 和 Y 方向都偏移自身尺寸的 50%) finalTransform = 'translate(0, -50%)' // 🔹 X 轴定位:右对齐,距离右侧边界 12px finalLeft = windowWidth - menuWidth - rightGap // ⚠️ 安全检查:防止弹窗左侧溢出屏幕(比如小屏设备) if (finalLeft < 0) { console.warn('【警告】弹窗左溢,修正为 0') finalLeft = 0 } console.log('【居中模式】启用屏幕中心定位', { finalTop, finalLeft, finalTransform }) } else { // ======================== // 📐 情况2:至少有一侧空间足够 → 正常弹出(向上或向下) // ======================== // 根据下方空间是否足够决定方向 const needUpward = spaceBelow < maxHeight // 下方不够就往上弹 if (needUpward) { // 向上展开:弹窗底部对齐触发元素顶部,再留 8px 间隙 finalTop = rect.top - maxHeight - 8 finalTransform = 'translateY(8px)' // 微调:让动画有“出现”感(可选) } else { // 向下展开:弹窗顶部对齐触发元素底部,再留 8px 间隙 finalTop = rect.bottom + 8 finalTransform = 'translateY(-8px)' // 微调偏移 } // 🔹 水平方向统一右对齐(距离右边 12px) finalLeft = '12px' console.log('【常规模式】根据方向定位', { direction: needUpward ? '向上弹出' : '向下弹出', finalTop, finalLeft, finalTransform }) } // ✅ 更新响应式数据,驱动 UI 更新 top.value = finalTop left.value = finalLeft // right.value = finalLeft position.value = finalTransform console.log('✅ 【最终定位结果】已设置:', { top: top.value, left: left.value, // right: right.value, transform: position.value }) // 显示弹窗 visible.value = true // 触发 open 事件,携带当前 modelValue emit('open', props.modelValue, props.trigger) // 在下一次 DOM 更新后绑定外部点击和滚动监听 nextTick(() => { console.log('🔧 执行 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] // 动态构造返回对象,key 来自 props const selected = { [props.valueKey]: value, [props.labelKey]: label } // 检查 modelValue 是否变化 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?.dataset?.dropdownTrigger) if (!isInside) close() } uni.$on('onPageTap', handler) observer = { ...observer, cleanupTap: () => uni.$off('onPageTap', handler) } } // ---------------------- // 页面滚动关闭 // ---------------------- const bindScrollListener = () => { const scrollHandler = res => { if (!visible.value) return // 滚动时重新定位 // repositionDebounced() console.log('🚀 ~ bindScrollListener ~ props.scrollClose:', props.scrollClose) if (props.scrollClose) return close() reposition() } uni.$on('onPageScroll', scrollHandler) observer = { ...observer, cleanupScroll: () => uni.$off('onPageScroll', scrollHandler) } } // 新增:重新计算并设置位置 // const reposition = async () => { // const rect = await getTriggerRect() // if (!rect) return // const res = uni.getSystemInfoSync() // const windowHeight = res.windowHeight // const maxHeight = props.maxHeight // const needUpward = windowHeight - rect.bottom < maxHeight // top.value = needUpward ? rect.top - maxHeight - 8 : rect.bottom + 8 // position.value = needUpward ? 'translateY(-8px)' : 'translateY(8px)' // } const reposition = async () => { // 获取触发元素的边界信息 const rect = await getTriggerRect() if (!rect) { console.warn('【reposition】无法获取触发元素位置,跳过重定位') return } console.log('【reposition】开始重新定位', { rect }) const res = uni.getSystemInfoSync() const windowHeight = Number(res.windowHeight) const windowWidth = Number(res.windowWidth) const maxHeight = props.maxHeight // 弹窗最大高度 const menuWidth = 215 // 必须与 CSS 一致 const rightGap = 12 // 右侧边距 // 计算上下可用空间 const spaceBelow = windowHeight - rect.bottom const spaceAbove = rect.top const isBothDirectionsInsufficient = spaceAbove < maxHeight && spaceBelow < maxHeight console.log('【reposition - 空间判断】', { spaceAbove, spaceBelow, maxHeight, isBothDirectionsInsufficient }) let finalTop, finalLeft, finalTransform if (isBothDirectionsInsufficient) { // ======================== // 🎯 情况1:上下都不够 → 屏幕居中 + 右对齐 // ======================== finalTop = windowHeight / 2 finalTransform = 'translate(0, -50%)' // Y轴居中,X不偏移(left 已经精确设置) finalLeft = windowWidth - menuWidth - rightGap if (finalLeft < 0) { console.warn('【reposition】左溢修正', finalLeft) finalLeft = 0 } console.log('【reposition - 居中模式】', { finalTop, finalLeft, finalTransform }) } else { // ======================== // 📐 情况2:正常方向展开 // ======================== const needUpward = spaceBelow < maxHeight finalTop = needUpward ? rect.top - maxHeight - 8 : rect.bottom + 8 finalTransform = needUpward ? 'translateY(-8px)' : 'translateY(8px)' finalLeft = windowWidth - menuWidth - rightGap if (finalLeft < 0) { console.warn('【reposition】右对齐导致左溢,修正为 0') finalLeft = 0 } console.log('【reposition - 常规模式】', { direction: needUpward ? '向上' : '向下', finalTop, finalLeft, finalTransform }) } // ✅ 更新响应式变量 top.value = finalTop left.value = finalLeft position.value = finalTransform console.log('✅ 【reposition 成功】更新定位:', { top: top.value, left: left.value, transform: position.value }) } const debounce = (fn, time = 10) => { let timer = null return function (...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); } /* 内容区可滚动 */ .dropdown-content { flex: 1; height: 0; /* 防止撑开 */ max-height: 486px; overflow-y: auto; -webkit-overflow-scrolling: touch; /* iOS 平滑滚动 */ } /* 每一项 */ .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; /* 可替换为 iconfont 字体 */ font-size: 16px; margin-left: 8px; color: #0f56d5; } </style> 修改当前组件,让滚动区域使用,确保复用当前组件样式,返回我完整的代码
最新发布
11-01
### Vue Element UI `el-dropdown` 自定义 Class 样式Vue 的开发过程中,如果需要为 Element UI 的 `el-dropdown` 组件中的下拉菜单部分设置自定义样式,可以通过多种方式实现。以下是详细的解决方案: #### 方法一:通过 Slot 和外层包裹标签 可以使用 `<template>` 或者额外的 HTML 标签来包裹 `el-dropdown-menu` 并为其添加自定义类名。 ```html <el-dropdown> <span class="el-dropdown-link"> 下拉菜单<i class="el-icon-arrow-down el-icon--right"></i> </span> <el-dropdown-menu slot="dropdown" class="custom-dropdown-menu"> <el-dropdown-item>选项1</el-dropdown-item> <el-dropdown-item>选项2</el-dropdown-item> </el-dropdown-menu> </el-dropdown> ``` 在此示例中,`class="custom-dropdown-menu"` 被直接应用到 `el-dropdown-menu` 上[^1]。这样就可以针对 `.custom-dropdown-menu` 定义特定的 CSS 样式--- #### 方法二:利用 Scoped Styles 和全局覆盖 当需要更复杂的样式调整时,可以在当前组件的 `<style>` 部分编写非 scoped 的样式规则以覆盖默认样式。 ```css <style lang="scss"> .custom-dropdown-menu { background-color: #f9fafc; border-radius: 8px; .el-dropdown-menu__item { color: #333; &:hover { background-color: #e5e9ef; } } } </style> ``` 需要注意的是,由于 Element UI 使用了较深层次的选择器,可能需要增加样式的优先级或者使用 `!important` 来强制生效[^4]。 --- #### 方法三:动态绑定 Class 名称 对于更加灵活的需求,还可以通过 JavaScript 动态绑定类名的方式实现。 ```html <el-dropdown-menu slot="dropdown" :class="{ 'active-class': isActive }"> <el-dropdown-item>选项1</el-dropdown-item> <el-dropdown-item>选项2</el-dropdown-item> </el-dropdown-menu> ``` 其中 `isActive` 是一个布尔类型的变量,在数据模型中控制该类是否被激活[^2]。 --- #### 注意事项 - 如果发现某些样式无法正常生效,请确认是否存在其他更高优先级的样式冲突。 - 对于一些特殊的交互行为(如点击事件未响应),需注意监听的目标节点以及事件冒泡机制的影响。 --- ### 示例代码 以下是一个完整的例子展示如何结合上述方法完成自定义样式配置: ```vue <template> <div> <el-dropdown> <span class="el-dropdown-link">下拉菜单<i class="el-icon-arrow-down el-icon--right"></i></span> <el-dropdown-menu slot="dropdown" class="custom-dropdown-menu"> <el-dropdown-item @click.native="handleClick('option1')">选项1</el-dropdown-item> <el-dropdown-item @click.native="handleClick('option2')">选项2</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </template> <script> export default { methods: { handleClick(option) { console.log(`选择了 ${option}`); }, }, }; </script> <style lang="scss"> .custom-dropdown-menu { background-color: #fffbea !important; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); .el-dropdown-menu__item { padding: 10px 20px; font-size: 14px; &:hover { background-color: #ffe7ba !important; } } } </style> ``` ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值