固定定位 z-index iconfont的使用 043

本文深入探讨CSS中固定定位的特性及其对元素层级的影响,包括z-index的使用原则,以及如何利用固定定位创建如导航栏等常用网页组件。同时,介绍了iconfont的使用方法和注意事项。

固定定位 z-index iconfont的使用 043

固定定位  现象 脱标 提升层级  将盒子固定住 

  参考点 浏览器的左上角 :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="./css/reset.css">
    <style>
        body{
            padding-top: 80px;
        }

        .header{
            width: 100%;
            height: 80px;
            background-color: red;
            /*固定定位*/
            position: fixed;
            top: 0;
            left: 0;
        }


    </style>
</head>
<body style="height: 2000px">
    <div class="header"></div>
    <p>I am alex</p>
    <p>alex</p>
    <p>alex</p>
    <p>alex</p>
    <p>alex</p>
    <p>alex</p>
    <p>alex</p>
    <p>alex</p>
    <p>alex</p>
    <p>alex</p>
    <p>alex</p>
    <p>alex</p>
</body>
</html>

reset.css(重置样式)  直接引用就行

/* http://meyerweb.com/eric/tools/css/reset/
   v2.0 | 20110126
   License: none (public domain)
*/

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
    display: block;
}
body {
    line-height: 1;
}
ol, ul {
    list-style: none;
}
blockquote, q {
    quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
    content: '';
    content: none;
}
table {
    border-collapse: collapse;
    border-spacing: 0;
}
View Code

回到顶部案例 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="./css/reset.css">
    <style>
        .backTop{
            width: 100px;
            height: 100px;
            line-height: 100px;
            text-align: center;
            background-color: black;
            position: fixed;
            right: 20px;
            bottom: 20px;
        }
        a{
            text-decoration: none;
        }
        .backTop a{

            color: #fff;
        }

    </style>
</head>
<body style="height: 2000px">
<div id="top"></div>
<div class="backTop">
    <!--阻止a标签的默认事件-->
    <a href="javascript:void(0);">回到顶部</a>
</div>
<!--from xxx import ooo-->
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    // def $(str):
    //     .....
    // return 对象
    //
    // $()

    $('a').click(function(){
    //   业务逻辑

        $('body,html').animate({
            scrollTop:0
        },1000);

    })

</script>


</body>
</html>
View Code

z-index :有四大特征:

  与固定定位有关系 默认为0 越大表层级越高

  1 z-index值表示谁压着谁,数值大的压盖数值小的

  2 只有定位了的元素才能有z-index ,也即 不管线对定位 绝对定位 固定定位都可以使用z-index 浮动元素不能设置z-index

  3 z-index值没有单位 就是一个正整数 默认的是0如果都没有z-index值 那么谁写在HTML后面 谁在上面压着别人 定位了的元素 永远压着没有定位的元素

  4 从父现象 父亲怂了 儿子再牛逼也没有用

父子的z-index

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .father1,.father2{
            width: 400px;
            height: 400px;

        }
        /*父亲怂了  儿子再牛逼也没用*/
        .father1{
            background-color: red;
            position: absolute;
            z-index: 10;
        }
        .father2{
            background-color: green;
            position: absolute;
            z-index: 3;
        }
        .child1,.child2{
            width: 200px;
            height: 200px;

        }
        .child1{
            background-color: darkmagenta;
            position: absolute;
            top: 350px;
            left: 450px;
            z-index: 5;
        }
        .child2{
            background-color: #ff6700;
            position: absolute;
            top: 350px;
            left: 420px;
            z-index: 10000;
        }

    </style>
</head>
<body>
    <div class="father1">
        <div class="child1"></div>
    </div>
    <div class="father2">
        <div class="child2"></div>
    </div>

</body>
</html>
View Code

 

 

 

iconfont 的使用 

iconfont

http://www.iconfont.cn/

IconFont 图标

unicode是字体在网页端最原始的应用方式,特点是:

兼容性最好,支持ie6+,及所有现代浏览器。

支持按字体的方式去动态调整图标大小,颜色等等。

但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去

色。

注意:新版iconfont支持多色图标,这些多色图标在unicode模式下将不能使用,如果有需求建议使用

symbol的引用方式

unicode使用步骤如下:

第一步:拷贝项目下面生成的font-face

@font-face {
  font-family: 'iconfont';
  src: url('iconfont.eot');
  src: url('iconfont.eot?#iefix') format('embedded-opentype'),
  url('iconfont.woff') format('woff'),
  url('iconfont.ttf') format('truetype'),
  url('iconfont.svg#iconfont') format('svg');
}

 

第二步:定义使用iconfont的样式

.iconfont{
  font-family:"iconfont" !important;
  font-size:16px;font-style:normal;
  -webkit-font-smoothing: antialiased;
  -webkit-text-stroke-width: 0.2px;
  -moz-osx-font-smoothing: grayscale;
}

 

第三步:挑选相应图标并获取字体编码,应用于页面

    <i class="iconfont">&#x33;</i>

 

"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。

  

 

固定定位
固定定位的现象
  • 脱标

  • 提升层级

  • 页面内容一多,盒子固定住

参考点

​ 以浏览器的左上角

固定导航栏
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
   <link rel="stylesheet" href="./css/reset.css">
   <style>
       body{
           padding-top: 80px;
      }

       .header{
           width: 100%;
           height: 80px;
           background-color: red;
           /*固定定位*/
           position: fixed;
           top: 0;
           left: 0;
           /*提升层级*/
           z-index: 99999;
      }
       .active{
           position: relative;
      }


   </style>
</head>
<body style="height: 2000px">
   <div class="header"></div>
   <p>我是alex</p>
   <p>alex</p>
   <p>alex</p>
   <p>alex</p>
   <p>alex</p>
   <p>alex</p>
   <p>alex</p>
   <p>alex</p>
   <p>alex</p>
   <p>alex</p>
   <p>alex</p>
   <p class="active">alex</p>

</body>
</html>
回到顶部

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
   <link rel="stylesheet" href="./css/reset.css">
   <style>
       .backTop{
           width: 100px;
           height: 100px;
           line-height: 100px;
           text-align: center;
           background-color: black;
           position: fixed;
           right: 20px;
           bottom: 20px;
      }
       a{
           text-decoration: none;
      }
       .backTop a{

           color: #fff;
      }

   </style>
</head>
<body style="height: 2000px">
<div id="top"></div>
<div class="backTop">
   <!--阻止a标签的默认事件-->
   <a href="javascript:void(0);">回到顶部</a>
</div>
<!--from xxx import ooo-->
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
   // def $(str):
   //     .....
   // return 对象
   //
   // $()

   $('a').click(function(){
   //   业务逻辑

       $('body,html').animate({
           scrollTop:0
      },1000);
     
  })

</script>


</body>
</html>

 

posted @ 2018-11-18 13:03 你没有想象的那么重要 阅读( ...) 评论( ...) 编辑 收藏
<template> <!-- <view class="modal-overlay" @tap.stop="hideModal" v-if="visible" @touchmove.stop.prevent> --> <view class="modal-overlay" @tap.stop="hideModal" @touchmove.stop.prevent> <view class="modal-content"> <swiper class="swiper" :class="advData.value.length==1?'on':''" :autoplay="autoplay" :duration="duration" @change="stopChange" v-if="advData.type == 'pic' && advData.value.length"> <swiper-item v-for="(item,index) in advData.value" :key="index" @click="jump(item.link)"> <!-- <view class="swiper-item"> --> <view class="swiper-item-img"> <image :src="item.img" mode="aspectFit"></image> </view> <!-- </view> --> </swiper-item> </swiper> <text class="iconfont icon-ic_close1 fs-40 text--w111-fff"></text> </view> </view> </template> <style scoped> .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.6); display: flex; height: 100%; justify-content: center; align-items: center; z-index: 1000; -webkit-overflow-scrolling: touch; /* 禁用 iOS 弹性滚动 */ } .modal-content { background-size: contain; background-repeat: no-repeat; position: fixed; top: 45%; left: 50%; transform: translate(-50%, -50%); height: 50%; z-index: 1001; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; } .modal-content-mobile { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); height: 30%; background-size: cover; background-position: center; z-index: 1001; } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10rpx; } .modal-footer1 { position: relative; top: 32%; transform: translate(-50%, -50%); margin-left: 48%; align-items: center; } .modal-footer2 { position: relative; top: 28%; margin-left: 50%; transform: translate(-50%, -50%); align-items: center; } .modal-footer3 { position: relative; top: 55%; margin-left: 50%; transform: translate(-50%, -50%); align-items: center; } .modal-body { font-size: 25rpx; position: relative; margin: 0 auto; top: 19%; width: 75%; align-items: center; } .modal-body-mobile { font-size: 35rpx; position: relative; margin: 0 auto; top: 35%; width: 80%; align-items: center; } .modal-body view { margin-top: 5%; justify-content: center; align-items: center; } .modal-body-mobile view { margin-top: 5%; justify-content: center; align-items: center; } input { justify-content: center; margin-top: 3%; width: 80%; background: #FFFFFF; border-radius: 8px 8px 8px 8px; padding: 14rpx 32rpx; font-size: 30rpx; } .swiper-item { height: 100%; text-align: center; position: relative; display: flex; align-items: flex-end; flex-direction: column-reverse } .jump-over { position: absolute; height: 325rpx; text-align: center; padding: 0 15rpx; font-weight: bold; z-index: 999; bottom: 20rpx; top: 50%; left: 50%; transform: translate(-50%, -50%); } .jump_img { position: absolute; bottom: 80rpx; left: 50%; transform: translateX(-50%); z-index: 1002; } .swiper-item { height: 100%; text-align: center; position: relative; display: flex; align-items: flex-end; flex-direction: column-reverse } .swiper-item-img { width: 60%; max-width: 800px; margin: 0 auto; height: 100%; position: relative; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; } .swiper-item-img image { width: 100%; height: 100%; object-fit: contain; } .modal-content { /* position: relative; */ width: 50vw; /* height: 60vh; */ /* max-width: 800px; */ /* display: flex; */ /* flex-direction: column; */ /* align-items: center; */ /* justify-content: center; */ } .swiper { width: 100%; height: 100%; } .icon-ic_close1 { position: absolute; z-index: 1002; } </style> 修改代码:要求:这是一个uniapp的开屏弹窗 根据代码写出来css样式,关闭按钮要放在图片的下方并和图片保持间隔
06-14
<template> <div :class="['page-layout', wrapClass, isDark && 'theme-dark']"> <div :class="['page-layout--placeholer', breadMenuHidden && 'no-nav']"></div> <TopHeader class="page-layout__header" /> <SideBar :menus class="page-layout__side" /> <div :class="['page-layout__main', { 'no-nav': breadMenuHidden }]"> <div v-if="!breadMenuHidden" class="page-layout__main-nav" > <BreadMenu /> </div> <div class="page-layout__main-view" :class="{ 'is-mobile': appStore.$state.isMobile }" > <div v-if="pageTitle.visible" class="page-layout__main-view-title common-page-title" > <span> {{ pageTitle.label }}</span> <a-button v-if="route?.meta?.backBtn" type="primary" @click="handleBack" >{{ btnLabel }}</a-button > </div> <RouterView v-slot="{ Component }"> <KeepAlive :include="cacheViews"> <component :is="Component" :key="routeKey" /> </KeepAlive> </RouterView> </div> <!-- TODO: 底部有固定定位元素<MainFixedBar />时占位用 勿删! --> <div class="page-bar__fixed-placeholer"></div> </div> </div> </template> <script setup lang="ts"> import { computed, onMounted, ref, h, watchEffect, watch, nextTick } from "vue" import { useAppStore, useMsgStore, useUserStore, useSysConfigStore } from "@/stores" import TopHeader from "./components/TopHeader.vue" import SideBar from "./components/side-bar/index.vue" import BreadMenu from "./components/BreadMenu.vue" import { onBeforeRouteLeave, useRoute, useRouter } from "vue-router" import { Button, Notification, type NotificationReturn } from "@arco-design/web-vue" import type { RecordModel as MessageData } from "@/api/message/api-type" import { IconRight } from "@arco-design/web-vue/es/icon" import { ORDER_TRANSFER } from "@/router/order" import { getSystemConfig } from "@/api/system/index.serv" import { to } from "await-to-js" defineOptions({ name: "Layout" }) const route = useRoute() const router = useRouter() const appStore = useAppStore() const userStore = useUserStore() const msgStore = useMsgStore() const sysConfigStore = useSysConfigStore() const orderMsgReturn = ref<NotificationReturn>() const breadMenuHidden = ref(false) const btnLabel = computed(() => { return window.opener ? "关闭" : route?.meta.backBtn?.label || "关闭" }) const cacheViews = computed(() => appStore.routeViewCache) const msgId = computed(() => msgStore.msgNoticeId) const pageTitle = computed(() => { const { navTitle, title, titleVisible } = route.meta ?? {} return { visible: title && titleVisible !== false, label: navTitle || title } }) const routeKey = computed(() => { const { name, query = {} } = route if (Object.keys(query).length) { return `${name as string}-${JSON.stringify(route.query)}` } return name as string }) const wrapClass = computed(() => { return appStore.menuStatus ? "menu-collapse" : "menu-expend" }) const isDark = computed(() => appStore.theme === "dark") const menus = computed(() => userStore.accessMenu ?? []) const menuMap = computed(() => userStore.accessMenuMap) const doOrderLoop = computed(() => { return [...menuMap.value.values()].some((list) => { const loopData = list?.meta?.doLoop if (!loopData) return false const { code, dependMenu } = loopData if (code === "order") { if (!dependMenu) return true return menuMap.value.has(dependMenu) } else { return false } }) }) watchEffect(() => { breadMenuHidden.value = route.meta?.breadMenuVisible === false }) const goToTransfer = (data: MessageData) => { router .push({ name: ORDER_TRANSFER.name }) .then((res) => { Notification.remove(msgId.value) }) } const showOrderMsg = (data: MessageData) => { return Notification.info({ icon: () => h("span", { class: ["iconfont icon-file"] }), title: "您有一个订单需要进行调度", content: data.msg_body, id: msgId.value, closable: true, duration: 0, class: "order-msg__tip", position: "bottomRight", footer: () => h( Button, { type: "text", onClick: () => goToTransfer(data) }, ["立即前往", h(IconRight)] ) }) } watch( [() => msgStore.msgTargetId, () => sysConfigStore.orderMessageConfig.orderNoticeSwitch as number], async ([id, orderNoticeSwitch]: [number | undefined, number]) => { await nextTick() if (id && msgStore.unreadMsgLatest && orderNoticeSwitch === 1) { const msgReturn = showOrderMsg(msgStore.unreadMsgLatest) orderMsgReturn.value ||= msgReturn } else { Notification.remove(msgId.value) } }, { immediate: true } ) const handleBack = () => { if (window.opener) { window.close() } else { const routeTo = route.meta?.backBtn?.routeTo if (routeTo) { router.replace({ name: routeTo }) } } } onMounted(async () => { msgStore.clearLoopTimer() const [err, res] = await to(getSystemConfig()) if (!err && res) { sysConfigStore.setUpdateConfig({ id: res.data.id, orderNoticeSwitch: res.data.orderNoticeSwitch }) doOrderLoop.value && msgStore.messageAndCountLoop() } }) onBeforeRouteLeave(() => { Notification.remove(msgId.value) msgStore.clearLoopTimer() }) </script> <style lang="scss" scoped> @import "../assets/styles/mixin.scss"; $--layout-transition-all: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1); $--layout-transition-margin: margin 0.2s cubic-bezier(0.34, 0.69, 0.1, 1); $--layout-transition-left: left 0.2s cubic-bezier(0.34, 0.69, 0.1, 1); .page-layout { height: 100%; display: flex; flex-direction: column; overflow: hidden; &--placeholer { flex: none; height: calc(var(--page-header-height) + var(--page-breadmenu-height)); transition: none; &.no-nav { height: var(--page-header-height); } } &__header { position: fixed; top: 0; left: 0; right: 0; z-index: var(--page-max-z-index); flex: none; padding: 0 16px; height: var(--page-header-height); border-bottom: var(--color-border-2) 1px solid; background-color: #fff; transition: $--layout-transition-all; } &__side { position: fixed; z-index: var(--page-max-z-index); left: 0; top: var(--page-header-height); bottom: 0; display: flex; flex-direction: column; :deep(.arco-menu-inner) { flex: 1; margin-bottom: var(--menu-collapse-width); } :deep(.arco-menu-collapse-button) { position: absolute; } & + .page-layout__main { margin-left: var(--menu-expand-width); .page-layout__main-nav { left: var(--menu-expand-width); } } &.arco-menu-collapsed + .page-layout__main { margin-left: var(--menu-collapse-width); .page-layout__main-nav { left: var(--menu-collapse-width); } } } &__main { padding: var(--page-padding-gutter); padding-top: 0; transition: $--layout-transition-margin; height: 100%; display: flex; flex-direction: column; overflow-y: auto; @include scrollbar(8px, transparent, var(--color-fill-4)); &.no-nav { padding-top: var(--page-padding-gutter); } &-nav { flex: none; display: flex; align-items: center; background: var(--page-back-color); position: fixed; z-index: var(--page-max-z-index); top: var(--page-header-height); left: 0; right: 0; height: var(--page-breadmenu-height); padding: 0 var(--page-padding-gutter); transition: $--layout-transition-left; } &-content { position: relative; height: 100%; display: flex; flex-direction: column; padding: var(--page-padding-gutter); padding-top: --page-breadmenu-height; background: #f9f9f9; flex: 1; // margin-left: var(--menu-expand-width); // transition: margin-left 0.28s; // transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1); &.no-nav { padding-top: var(--page-padding-gutter); } } &-view { flex: 1; display: flex; flex-direction: column; background: #fff; border-radius: 4px; padding: var(--page-padding-gutter); // > :last-child:not(.common-page-title) { // flex: 1; // display: flex; // flex-direction: column; // } &-title { display: flex; justify-content: space-between; align-items: center; } } } &.theme-dark { .page-layout__header, .page-layout__side { background: var(--color-text-2); } } } </style> <style lang="scss"> .order-msg__tip { .arco-notification-title { font-weight: 600; } .arco-notification-left { padding-right: 5px; .arco-notification-icon { font-size: 16px; color: var(--color-text-2); } } .arco-notification-content { margin-top: 6px; } } </style>
06-26
<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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值