2025.12.21本周知识要点

21 阅读3分钟

1.vue中css样式穿透:

Vue2中:使用::v-deep;

Vue3中:使用:deep();

2.ios系统,overlay遮罩层内的元素无法滑动

原因:ios系统内部问题,在fixed布局中的子元素,可能会无法滑动

解决方案:元素不放到fixed布局中

3.文字超过三行显示省略号并根据窗口大小调整,自适应显示展开/收起按钮

(2025.12.6本周知识要点8后续...)

自适应显示方案:通过ResizeObserver监控container元素大小变化,然后比较spanBox和textBox的高度

(这里用vue2写的)

父组件template:

<template>
  <div id="chapterSummary" class="chapter-summary">
    <div v-for="(chapter, index) in chapterSummary" :key="index" :id="`chapter-step-${index}`" class="chapter-step"
      @click="changeChapter(index)" :style="{ display: index === 0 ? 'none' : 'block' }"
    >
      <div class="step-line" v-if="index !==chapterSummary.length - 1"></div>
      <div class="chapter-head" :class="{ 'chapter-head-active': chapterSelected === index}">
        <span class="chapter-bg">{{ getTime(chapter.bg) }}</span>
        <span class="chapter-title">{{ chapter.title }}</span>
      </div>
      <EllipsisBox style="margin-top: 6px;" :text="chapter.abstract" :index="index" :isOver="chapterIsOverList?.[index] ?? false"
        :textLineHeight="textLineHeight"
        @changeTextIsOver="changeTextIsOver"
      />
    </div>
  </div>
</template>

父组件js监控container:

// container挂载时添加ResizeObserver监控
mounted() {
    const chapterSummary = document.getElementById('chapterSummary');
    if (chapterSummary) {
      this.observer(chapterSummary); // 监听chapterSummary元素,其尺寸变化时,自适应章节纪要介绍的展开按钮是否展示
    }
  },
// container卸载前清除ResizeObserver监控
beforeDestroy() {
    if (this.observeCfg) {
      this.observeCfg.disconnect();
    }
  }
// 监控到container宽度变化后的回调,修改三行文字组件是否需要更改展示按钮显示状态
onContainerResize: throttle(
  function() {
    if (this.chapterSummary?.length > 0) {
      this.chapterIsOverList = this.chapterSummary.reduce((acc, cur, index) => {
        const spanBox = document.getElementById(`chapter-abstract-${index}`);
        if (spanBox) {
          acc[index] = spanBox.offsetHeight > this.textLineHeight * 3; // 最多三行3*textLineHeight,超过则说明文本超过三行
        }
        return acc;
      }, [])
    }
  },
  50
),
// ResizeObserver监控
observer(el) {
  const cd = () => {
    this.onContainerResize();
  }
  const observer = new ResizeObserver(cd);
  observer.observe(el);
  this.observeCfg = observer;
}

子组件template:

<template>
  <div class="box-wrapper">
    <div ref="textBox" class="box" :class="{ 'ellipsis-box': folded }"
      @mousemove="onMouseMove"
      @mouseleave="onMouseLeave"
    >
      <span class="folded-btn" @click.stop="folded = !folded" v-if="isOver">
        <span v-if="mouseOver && folded">展开<i class="el-icon-arrow-down"></i></span>
        <span v-if="!folded">收起<i class="el-icon-arrow-up"></i></span>
      </span>
      <span :id="`chapter-abstract-${index}`" ref="spanBox" class="text" :style="{ lineHeight: `${textLineHeight}px`}">{{ text }}</span>
    </div>
  </div>
</template>

4.tab栏根据宽度自适应显示tab个数,多出的tab下拉选择

方案:通过ResizeObserver监控tab栏外部盒子宽度(盒子为固定宽度),除以每个tab栏宽度可得显示的tab数

5.点击右键,鼠标位置显示菜单选项

右键事件:

// 右键事件
addEventListener("contextmenu", (event) => {});
oncontextmenu = (event) => {};
// 鼠标位置:[X, Y]
mousePosition = [event.clientX, event.clientY]

右键弹窗代码(Vue3):

<template>
  <div class="context-menu-container">
    <!-- 自定义Popover组件 -->
    <div 
      v-if="visible" 
      ref="menuRef" 
      class="context-menu-popover"
      :style="menuStyle"
      @click.stop
    >
      <div class="context-menu-content">
        <div
          v-for="item in menuOptions"
          :key="item.value"
          :class="['context-menu-item', { 'context-menu-item-disabled': item.disabled }]"
          :disabled="item.disabled"
          @click="!item.disabled && handleItemClick(item)"
        >
          {{ item.title }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, withDefaults, watch, nextTick } from 'vue';

// 菜单项类型定义
export interface MenuItem {
  title: string;
  value: string;
  disabled?: boolean;
}

// Props类型定义
interface ContextMenuProps {
  visible?: boolean;
  position?: [number, number];
  options?: MenuItem[];
}

// 事件类型定义
interface ContextMenuEmits {
  (e: 'item-click', item: MenuItem): void;
  (e: 'close'): void;
  (e: 'update:visible', value: boolean): void;
}

// Props定义
const props = withDefaults(defineProps<ContextMenuProps>(), {
  visible: false,
  position: () => [0, 0],
  options: () => []
});

// 事件定义
const emit = defineEmits<ContextMenuEmits>();

// 菜单引用
const menuRef = ref<HTMLElement | null>(null);

// 计算菜单位置样式
const menuStyle = ref<{ left: string; top: string }>({
  left: `${props.position[0]}px`,
  top: `${props.position[1]}px`
});

// 监听visible变化,计算菜单位置
watch(() => props.visible, (newVal) => {
  if (newVal) {
    nextTick(() => {
      calculateMenuPosition();
    });
  }
});

// 监听position变化,重新计算菜单位置
watch(() => props.position, () => {
  if (props.visible) {
    nextTick(() => {
      calculateMenuPosition();
    });
  }
}, { deep: true });

// 计算菜单位置,确保不超出屏幕
const calculateMenuPosition = () => {
  if (!menuRef.value) return;
  
  const menu = menuRef.value;
  const rect = menu.getBoundingClientRect();
  const { clientWidth, clientHeight } = document.documentElement;
  const [mouseX, mouseY] = props.position;
  let x = mouseX;
  let y = mouseY;
  
  // 检查菜单右侧是否超出屏幕, wujie基座宽度在左侧导航展开时占181px
  if (x + rect.width > clientWidth + 181) {
    // 鼠标位置作为菜单的右边界
    x = mouseX - rect.width;
  }
  
  // 检查菜单底部是否超出屏幕, wujie基座高度占117px
  if (y + rect.height > clientHeight + 117) {
    // 鼠标位置作为菜单的左下角
    y = mouseY - rect.height;
  }
  
  // 确保菜单不超出屏幕左边界
  if (x < 0) {
    x = 0;
  }
  
  // 确保菜单不超出屏幕上边界
  if (y < 0) {
    y = 0;
  }
  
  // 更新菜单位置
  menuStyle.value = {
    left: `${x}px`,
    top: `${y}px`
  };
};

// 使用默认选项或传入的选项
const menuOptions = computed(() => props.options);

// 处理菜单项点击
const handleItemClick = (item: MenuItem) => {
  emit('item-click', item);
  emit('update:visible', false);
  emit('close');
};

// 点击外部关闭菜单
const handleClickOutside = (event: MouseEvent) => {
  if (menuRef.value && !menuRef.value.contains(event.target as Node)) {
    emit('update:visible', false);
    emit('close');
  }
};

// 按ESC键关闭菜单
const handleEscKey = (event: KeyboardEvent) => {
  if (event.key === 'Escape') {
    emit('update:visible', false);
    emit('close');
  }
};

// 组件挂载时添加事件监听
onMounted(() => {
  document.addEventListener('click', handleClickOutside, true);
  document.addEventListener('keydown', handleEscKey);
});

// 组件卸载时移除事件监听
onUnmounted(() => {
  document.removeEventListener('click', handleClickOutside, true);
  document.removeEventListener('keydown', handleEscKey);
});
</script>

<style scoped>
.context-menu-container {
  position: relative;
}

/* 自定义Popover样式 */
.context-menu-popover {
  position: fixed;
  z-index: 2000;
  margin-left: 5px;
  margin-top: 5px;
  background-color: var(--g-list-item-background-color);
  border: 1px solid var(--input-default1-border);
  border-radius: 4px;
  box-shadow: 0px 0px 8px 0px var(--g-popover-shadow);
  padding: 5px 0;
  min-width: 80px;
  max-width: 160px;
  overflow: hidden;
}

.context-menu-content {
  width: 100%;
}

.context-menu-item {
  padding: 5px 10px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--main1-text1-color);
  cursor: pointer;
  transition: background-color 0.2s;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.context-menu-item:hover {
  background-color: var(--g-theme-highlight-hover-bg);
  color: var(--g-theme-highlight-color);
}

/* 禁用状态 */
.context-menu-item-disabled {
  cursor: not-allowed !important;
  background-color: transparent !important;
}

.context-menu-item-disabled:hover {
  background-color: transparent !important;
}
</style>

6.vue3知识点

ref, defineProps, defineEmit, watch, watchEffect, reactive, toRefs, provide, inject, readonly