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