完成导航栏功能

This commit is contained in:
2026-03-18 10:37:52 +08:00
parent c2cd6dcd95
commit b7c8881e55
2 changed files with 783 additions and 86 deletions

View File

@@ -1,6 +1,22 @@
<template>
<view class="tags">
<scroll-view class="tags-scroll" scroll-x="true" show-scrollbar="false">
<!-- 左滑动按钮 -->
<view
class="scroll-btn scroll-btn-left"
:class="{ disabled: !canScrollLeft }"
@click="scrollToLeft"
>
<text class="scroll-btn-icon"></text>
</view>
<!-- 标签滚动容器 -->
<scroll-view
ref="scrollViewRef"
class="tags-scroll"
scroll-x="true"
show-scrollbar="false"
@scroll="onScroll"
>
<transition-group name="tag-list" tag="view" class="tags-row">
<view
v-for="t in tabs"
@@ -18,40 +34,87 @@
</transition-group>
</scroll-view>
<!-- 右键菜单 (带动画) -->
<transition name="menu-fade">
<!-- 右滑动按钮 -->
<view
class="scroll-btn scroll-btn-right"
:class="{ disabled: !canScrollRight }"
@click="scrollToRight"
>
<text class="scroll-btn-icon"></text>
</view>
<!-- 蓝色方格功能按钮 (扩大悬停区域) -->
<view
class="function-btn-wrapper"
@mouseenter="handleFunctionMenuOver"
@mouseleave="handleFunctionMenuLeave"
>
<view
v-if="menuVisible"
class="context-menu"
:style="{ top: menuY + 'px', left: menuX + 'px' }"
@click.stop=""
class="function-btn"
@click="toggleFunctionMenu"
>
<view class="menu-item" @click="handleAction('refresh')">
<text class="menu-icon"></text>
<text class="menu-text">刷新</text>
<view class="function-btn-icon">
<text class="function-btn-text"></text>
</view>
</view>
<view class="menu-item" v-if="!selectedTab?.isAffix" @click="handleAction('close')">
<text class="menu-icon">×</text>
<text class="menu-text">关闭</text>
</view>
<view class="menu-item" @click="handleAction('close-other')">
<text class="menu-icon">↸</text>
<text class="menu-text">关闭其他</text>
</view>
<view class="menu-item" @click="handleAction('close-all')">
<text class="menu-icon">⊘</text>
<text class="menu-text">全部关闭</text>
<!-- 功能按钮悬停菜单 -->
<view
v-if="functionMenuVisible"
class="function-menu"
@click.stop=""
@mouseenter="handleMenuOver"
@mouseleave="handleMenuLeave"
>
<view
v-for="menuItem in functionMenuItems"
:key="menuItem.command"
class="menu-item"
:class="{ disabled: menuItem.disabled }"
@click="handleTagAction(menuItem.command, activeTabId)"
>
<text class="menu-icon">{{ menuItem.icon }}</text>
<text class="menu-text">{{ menuItem.label }}</text>
</view>
</view>
</view>
</transition>
<!-- 右键菜单 (移除 transition直接使用 v-if) -->
<view
v-if="contextMenuVisible"
class="context-menu"
:style="{ top: menuY + 'px', left: menuX + 'px' }"
@click.stop=""
>
<view
v-for="menuItem in contextMenuItems"
:key="menuItem.command"
class="menu-item"
:class="{ disabled: menuItem.disabled }"
@click="handleTagAction(menuItem.command, selectedTab)"
>
<text class="menu-icon">{{ menuItem.icon }}</text>
<text class="menu-text">{{ menuItem.label }}</text>
</view>
</view>
<!-- 临时调试面板 -->
<!-- <view style="position: fixed; top: 100px; right: 20px; background: red; color: white; padding: 10px; z-index: 99999; font-size: 12px; max-width: 200px;">
<text>调试信息:</text><br/>
<text>functionMenuVisible: {{ functionMenuVisible }}</text><br/>
<text>菜单项数量: {{ functionMenuItems.length }}</text><br/>
<text>activeTabId: {{ activeTabId }}</text><br/>
<text>tabs数量: {{ tabs.length }}</text><br/>
<text>定时器ID: {{ hideFunctionMenuTimer }}</text><br/>
</view> -->
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import type { TabItem } from '../store/adminNavStore.uts'
defineProps<{
const props = defineProps<{
tabs: TabItem[]
activeTabId: string
}>()
@@ -64,127 +127,638 @@ const emit = defineEmits<{
(e:'refresh'): void
}>()
// ============================================
// 菜单项配置接口
// ============================================
interface TagActionMenu {
command: string
label: string
icon: string
disabled?: boolean
}
// ============================================
// 滚动相关状态
// ============================================
const scrollViewRef = ref<any>(null)
const scrollLeft = ref(0)
const scrollWidth = ref(0)
const clientWidth = ref(0)
const canScrollLeft = ref(false)
const canScrollRight = ref(true) // 初始设为true假设有可滚动内容
// ============================================
// 菜单相关状态
// ============================================
// 右键菜单状态
const menuVisible = ref(false)
const contextMenuVisible = ref(false)
const menuX = ref(0)
const menuY = ref(0)
const selectedTab = ref<TabItem | null>(null)
// 功能按钮菜单状态
const functionMenuVisible = ref(false)
let hideFunctionMenuTimer: number | null = null
// ============================================
// 统一菜单配置 (复用逻辑核心)
// ============================================
/**
* 获取标签操作菜单项
* @param targetTab 目标标签null 表示基于当前激活标签操作
*/
function getTagActionMenus(targetTab: TabItem | null): TagActionMenu[] {
const isFixedTab = targetTab?.isAffix || false
const hasMultipleTabs = props.tabs.length > 1
const hasNonFixedTabs = props.tabs.some(t => !t.isAffix)
return [
{
command: 'refresh',
label: '刷新',
icon: '↻',
disabled: false
},
{
command: 'close',
label: '关闭',
icon: '×',
disabled: isFixedTab || !targetTab // 固定标签或没有目标标签时禁用
},
{
command: 'close-other',
label: '关闭其他',
icon: '↸',
disabled: !hasMultipleTabs || props.tabs.length <= 1
},
{
command: 'close-all',
label: '全部关闭',
icon: '⊘',
disabled: !hasNonFixedTabs // 没有非固定标签时禁用
}
]
}
/**
* 统一标签操作命令处理 (复用逻辑核心)
* @param command 操作命令
* @param targetTab 目标标签
*/
function handleTagAction(command: string, targetTab?: TabItem | null): void {
if (command === 'refresh') {
emit('refresh')
} else if (command === 'close' && targetTab && !targetTab.isAffix) {
emit('tab-close', targetTab.id)
} else if (command === 'close-other') {
const keepTabId = targetTab?.id || props.activeTabId
emit('close-other', keepTabId)
} else if (command === 'close-all') {
emit('close-all')
}
// 关闭所有菜单
closeAllMenus()
}
// ============================================
// 计算属性 - 菜单项 (基于复用逻辑)
// ============================================
// 右键菜单项
const contextMenuItems = computed<TagActionMenu[]>(() => {
return getTagActionMenus(selectedTab.value)
})
// 功能按钮菜单项 (基于当前激活标签)
const functionMenuItems = computed<TagActionMenu[]>(() => {
const activeTab = getCurrentActiveTab()
return getTagActionMenus(activeTab)
})
// ============================================
// 工具方法
// ============================================
/**
* 获取当前激活的标签对象
*/
function getCurrentActiveTab(): TabItem | null {
return props.tabs.find(t => t.id === props.activeTabId) || null
}
/**
* 关闭所有菜单
*/
function closeAllMenus(): void {
contextMenuVisible.value = false
functionMenuVisible.value = false
// 清理功能菜单定时器
if (hideFunctionMenuTimer !== null) {
clearTimeout(hideFunctionMenuTimer as number)
hideFunctionMenuTimer = null
}
}
// ============================================
// 标签点击处理
// ============================================
function onTabClick(tab: TabItem) {
closeMenu()
closeAllMenus()
emit('tab-click', tab)
}
// ============================================
// 右键菜单处理
// ============================================
function openContextMenu(e: MouseEvent, tab: TabItem) {
selectedTab.value = tab
menuX.value = e.clientX
menuY.value = e.clientY
// 边缘检测
if (menuX.value + 100 > window.innerWidth) {
menuX.value = window.innerWidth - 110
if (menuX.value + 120 > window.innerWidth) {
menuX.value = window.innerWidth - 130
}
if (menuY.value + 140 > window.innerHeight) {
menuY.value = window.innerHeight - 150
}
menuVisible.value = true
// 关闭功能菜单,显示右键菜单
functionMenuVisible.value = false
contextMenuVisible.value = true
}
function closeMenu() {
menuVisible.value = false
}
// ============================================
// 功能按钮菜单处理 (仿照 AdminHeader 用户菜单实现)
// ============================================
function handleAction(type: string) {
if (!selectedTab.value) return
function handleFunctionMenuOver(e: any): void {
// #ifdef H5
console.log('=== 功能菜单悬停触发 (H5) ===')
console.log('functionMenuVisible before:', functionMenuVisible.value)
const id = selectedTab.value!.id
if (type === 'refresh') {
emit('refresh')
} else if (type === 'close') {
emit('tab-close', id)
} else if (type === 'close-other') {
emit('close-other', id)
} else if (type === 'close-all') {
emit('close-all')
if (hideFunctionMenuTimer !== null) {
clearTimeout(hideFunctionMenuTimer as number)
hideFunctionMenuTimer = null
console.log('清理定时器')
}
closeMenu()
contextMenuVisible.value = false // 关闭右键菜单
functionMenuVisible.value = true
console.log('functionMenuVisible after:', functionMenuVisible.value)
// #endif
}
function handleFunctionMenuLeave(e: any): void {
// #ifdef H5
console.log('=== 功能菜单离开触发 (H5) ===')
if (hideFunctionMenuTimer !== null) {
clearTimeout(hideFunctionMenuTimer as number)
}
hideFunctionMenuTimer = setTimeout(() => {
functionMenuVisible.value = false
console.log('定时器触发,隐藏菜单')
hideFunctionMenuTimer = null
}, 150) as number // 改回150ms与AdminHeader一致
// #endif
}
// 鼠标进入菜单区域 - 保持显示
function handleMenuOver(e: any): void {
// #ifdef H5
console.log('=== 鼠标进入菜单区域 ===')
if (hideFunctionMenuTimer !== null) {
clearTimeout(hideFunctionMenuTimer as number)
hideFunctionMenuTimer = null
console.log('进入菜单,取消隐藏定时器')
}
// #endif
}
// 鼠标离开菜单区域 - 开始隐藏倒计时
function handleMenuLeave(e: any): void {
// #ifdef H5
console.log('=== 鼠标离开菜单区域 ===')
hideFunctionMenuTimer = setTimeout(() => {
functionMenuVisible.value = false
console.log('离开菜单,定时器触发隐藏')
hideFunctionMenuTimer = null
}, 150) as number // 与AdminHeader保持一致
// #endif
}
function toggleFunctionMenu(e?: any): void {
if (e && typeof e.stopPropagation === 'function') {
e.stopPropagation()
}
contextMenuVisible.value = false // 关闭右键菜单
functionMenuVisible.value = !functionMenuVisible.value
}
// ============================================
// 滚动控制 (修复 uni-app x 兼容性)
// ============================================
/**
* 滚动事件处理
*/
function onScroll(e: any): void {
if (e && e.detail) {
scrollLeft.value = e.detail.scrollLeft || 0
scrollWidth.value = e.detail.scrollWidth || 0
clientWidth.value = e.detail.scrollWidth ? (e.detail.scrollWidth - e.detail.scrollLeft) : 0
}
checkScrollBounds()
}
/**
* 检查滚动边界状态 (简化逻辑)
*/
function checkScrollBounds(): void {
// 基础边界检测
canScrollLeft.value = scrollLeft.value > 5 // 允许5px误差
// 如果有多个标签,根据标签数量和滚动位置判断
if (props.tabs.length > 3) {
// 标签数量超过3个时通常会有滚动需要
const estimatedWidth = props.tabs.length * 120
const estimatedContainerWidth = 600
canScrollRight.value = scrollLeft.value < (estimatedWidth - estimatedContainerWidth - 20)
} else {
// 标签较少时,可能不需要滚动
canScrollRight.value = scrollWidth.value > 0 && scrollLeft.value < (scrollWidth.value - clientWidth.value - 10)
}
// 调试日志 (可根据需要开启/关闭)
// console.log('ScrollBounds:', {
// canScrollLeft: canScrollLeft.value,
// canScrollRight: canScrollRight.value,
// scrollLeft: scrollLeft.value,
// scrollWidth: scrollWidth.value,
// clientWidth: clientWidth.value,
// tabsCount: props.tabs.length
// })
}
/**
* 向左滚动 (修复 API 调用)
*/
function scrollToLeft(): void {
if (!canScrollLeft.value || !scrollViewRef.value) return
const stepWidth = 200 // 滚动步长
const targetScrollLeft = Math.max(0, scrollLeft.value - stepWidth)
// 使用 uni-app x 兼容的滚动方式
if (scrollViewRef.value.scrollTo) {
scrollViewRef.value.scrollTo({
left: targetScrollLeft,
animated: true
})
} else if (scrollViewRef.value.scrollLeft !== undefined) {
scrollViewRef.value.scrollLeft = targetScrollLeft
}
}
/**
* 向右滚动 (修复 API 调用)
*/
function scrollToRight(): void {
if (!canScrollRight.value || !scrollViewRef.value) return
const stepWidth = 200 // 滚动步长
const targetScrollLeft = scrollLeft.value + stepWidth
// 使用 uni-app x 兼容的滚动方式
if (scrollViewRef.value.scrollTo) {
scrollViewRef.value.scrollTo({
left: targetScrollLeft,
animated: true
})
} else if (scrollViewRef.value.scrollLeft !== undefined) {
scrollViewRef.value.scrollLeft = targetScrollLeft
}
}
/**
* 滚动到激活标签 (修复实现)
*/
function scrollToActiveTab(): void {
nextTick(() => {
if (!scrollViewRef.value || props.tabs.length === 0) return
const activeIndex = props.tabs.findIndex(t => t.id === props.activeTabId)
if (activeIndex === -1) return
// 简化实现:滚动到大概位置
const avgTabWidth = 120 // 估算每个标签宽度
const targetOffset = activeIndex * avgTabWidth
// 简化的滚动控制
if (scrollViewRef.value.scrollTo) {
scrollViewRef.value.scrollTo({
left: Math.max(0, targetOffset - 100),
animated: true
})
}
})
}
/**
* 更新滚动容器尺寸信息 (简化实现)
*/
function updateScrollInfo(): void {
nextTick(() => {
// 强制触发边界检测,使按钮可用
setTimeout(() => {
// 基于标签数量的简单估算
const estimatedTagsWidth = props.tabs.length * 120
const estimatedContainerWidth = 600 // 估算容器宽度
scrollWidth.value = estimatedTagsWidth
clientWidth.value = estimatedContainerWidth
checkScrollBounds()
}, 100)
})
}
// ============================================
// 监听器
// ============================================
// 监听激活标签变化,自动滚动到可视区域
watch(() => props.activeTabId, () => {
scrollToActiveTab()
})
// 监听标签数量变化,更新滚动信息
watch(() => props.tabs.length, () => {
updateScrollInfo()
})
// ============================================
// 生命周期
// ============================================
onMounted(() => {
window.addEventListener('click', closeMenu)
// 监听全局点击关闭菜单
window.addEventListener('click', closeAllMenus)
// 监听窗口大小变化
window.removeEventListener('resize', updateScrollInfo) // 防重复
window.addEventListener('resize', updateScrollInfo)
// 初始化滚动信息 - 延迟确保DOM渲染完成
setTimeout(() => {
updateScrollInfo()
scrollToActiveTab()
}, 300)
// 强制激活滚动按钮(针对多标签情况)
if (props.tabs.length > 3) {
canScrollRight.value = true
}
})
onUnmounted(() => {
window.removeEventListener('click', closeMenu)
window.removeEventListener('click', closeAllMenus)
window.removeEventListener('resize', updateScrollInfo)
// 清理功能菜单定时器
if (hideFunctionMenuTimer !== null) {
clearTimeout(hideFunctionMenuTimer as number)
hideFunctionMenuTimer = null
}
})
</script>
<style>
.tags{
/* ============================================
顶部标签栏整体布局 (修正 overflow 和 z-index)
============================================ */
.tags {
height: 44px;
background:#fff;
background: #fff;
border-bottom: 1px solid #eef2f7;
display:flex;
flex-direction:row;
align-items:center;
display: flex;
flex-direction: row;
align-items: center;
position: relative;
overflow: visible; /* 确保下拉菜单不被裁切 */
z-index: 1000; /* 提升容器层级 */
}
.tags-scroll{ width: 100%; height: 44px; }
.tags-row{
display:flex;
flex-direction:row;
align-items:center;
/* ============================================
滚动控制按钮
============================================ */
.scroll-btn {
width: 30px;
height: 30px;
background: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
margin: 0 8px;
}
.scroll-btn:hover:not(.disabled) {
background: #1677ff;
border-color: #1677ff;
}
.scroll-btn:hover:not(.disabled) .scroll-btn-icon {
color: #fff;
}
.scroll-btn.disabled {
background: #f5f7fa;
border-color: #e4e7ed;
cursor: not-allowed;
opacity: 0.4;
}
.scroll-btn-left {
margin-left: 12px;
}
.scroll-btn-right {
margin-right: 8px;
}
.scroll-btn-icon {
font-size: 16px;
color: #606266;
font-weight: bold;
line-height: 1;
}
/* ============================================
标签滚动容器
============================================ */
.tags-scroll {
flex: 1;
height: 44px;
overflow: hidden;
}
.tags-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 0 12px;
height: 44px;
min-width: 100%;
}
.tag{
/* ============================================
标签样式 (保持原有设计)
============================================ */
.tag {
height: 30px;
padding: 0 10px;
border: 1px solid #e5e7eb;
background:#fff;
background: #fff;
border-radius: 6px;
display:flex;
display: flex;
flex-direction: row;
align-items:center;
align-items: center;
gap: 8px;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
white-space: nowrap;
}
.tag:hover {
background: #f9fafb;
}
.tag.active{
border-color:#1677ff;
background:#eaf2ff;
.tag.active {
border-color: #1677ff;
background: #eaf2ff;
}
.tag-text{ font-size:12px; color:#374151; }
.tag-close{
.tag-text {
font-size: 12px;
color: #374151;
}
.tag-close {
width: 16px;
height: 16px;
border-radius: 8px;
display:flex;
flex-direction:row;
align-items:center;
justify-content:center;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.tag-close:hover {
background: rgba(0,0,0,0.1);
}
.tag-close-text{ font-size:14px; color:#6b7280; line-height:14px; }
/* 右键菜单样式 */
.tag-close:hover {
background: rgba(0, 0, 0, 0.1);
}
.tag-close-text {
font-size: 14px;
color: #6b7280;
line-height: 14px;
}
/* ============================================
蓝色方格功能按钮 (CRMEB 风格,修正层级)
============================================ */
/* 功能按钮包装器 - 扩大悬停检测区域 */
.function-btn-wrapper {
position: relative;
display: flex;
padding: 6px; /* 扩大 6px 悬停区域 */
margin-right: 6px; /* 调整外边距以保持视觉间距 */
height: 100%; /* 确保悬停不中断仿照AdminHeader */
overflow: visible;
}
.function-btn {
width: 30px;
height: 30px;
background: #1677ff;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
overflow: visible; /* 确保下拉菜单可见 */
z-index: 1001; /* 提升按钮层级 */
}
.function-btn:hover {
background: #4096ff;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.3);
}
.function-btn-icon {
display: flex;
align-items: center;
justify-content: center;
}
.function-btn-text {
font-size: 14px;
color: #fff;
font-weight: bold;
line-height: 1;
}
/* 功能按钮菜单 - 使用固定定位像素值 */
.function-menu {
position: absolute;
top: 44px; /* 使用固定像素值44px是tags容器高度 */
right: 0;
background: white;
border: 1px solid #d0d7de;
border-radius: 6px;
box-shadow: 0 8px 24px rgba(140, 149, 159, 0.2);
min-width: 160px;
z-index: 99999;
margin-top: 4px;
overflow: visible; /* 改为visible仿照AdminHeader */
padding: 5px 0;
}
/* ============================================
右键菜单样式 (保持原有设计)
============================================ */
.context-menu {
position: fixed;
z-index: 9999;
background: #fff;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 5px 0;
border-radius: 4px;
min-width: 100px;
min-width: 120px;
border: 1px solid #ebeef5;
}
/* ============================================
通用菜单项样式 (右键菜单和功能菜单共用)
============================================ */
.menu-item {
display: flex;
flex-direction: row;
@@ -192,26 +766,43 @@ onUnmounted(() => {
padding: 8px 16px;
cursor: pointer;
transition: background 0.2s;
height: 40px;
}
.menu-item:hover {
/* #ifdef H5 */
.menu-item:hover:not(.disabled) {
background: #f5f7fa;
}
.menu-item:hover:not(.disabled) .menu-text {
color: #1890ff;
}
/* #endif */
.menu-item.disabled {
cursor: not-allowed;
opacity: 0.4;
}
.menu-icon {
font-size: 14px;
margin-right: 8px;
color: #606266;
width: 14px;
text-align: center;
flex-shrink: 0;
}
.menu-text {
font-size: 13px;
color: #606266;
flex: 1;
line-height: 40px;
}
/* 标签列表动画 - 平滑平移和缩放 */
/* ============================================
标签列表动画 (保持原有设计)
============================================ */
.tag-list-enter-active,
.tag-list-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -232,7 +823,9 @@ onUnmounted(() => {
transition: transform 0.3s ease;
}
/* 右键菜单动画 - 类似 CRMEB 的缩放渐入 */
/* ============================================
菜单动画 (右键菜单和功能菜单)
============================================ */
.menu-fade-enter-active,
.menu-fade-leave-active {
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.5, 1);
@@ -244,4 +837,50 @@ onUnmounted(() => {
opacity: 0;
transform: scale(0.8) translateY(-10px);
}
/* 功能菜单动画 (从右上角展开) */
.function-menu .menu-fade-enter-active,
.function-menu .menu-fade-leave-active {
transform-origin: top right;
}
.function-menu.menu-fade-enter-from,
.function-menu.menu-fade-leave-to {
opacity: 0;
transform: scale(0.8) translateY(-10px);
}
/* ============================================
响应式适配
============================================ */
@media (max-width: 768px) {
.scroll-btn {
width: 28px;
height: 28px;
margin: 0 6px;
}
.scroll-btn-left {
margin-left: 8px;
}
.function-btn {
width: 28px;
height: 28px;
margin-right: 8px;
}
.tags-row {
padding: 0 8px;
}
.tag {
height: 28px;
padding: 0 8px;
}
.tag-text {
font-size: 11px;
}
}
</style>

View File

@@ -206,6 +206,65 @@
2. **移除组件级 Margin**: Grid 布局应使用 `gap` 属性控制间距,组件自身 (`.user-map-card`) 不应自带外部 Margin否则会破坏等高计算。
3. **强制 Canvas 填充**: 图表组件容器需设置 `width: 100% !important; height: 100% !important;` 且 `position: absolute` (配合父级 relative) 或 `flex: 1`,确保填满 Flex 空间。
#### **原因二十五uni-app x 悬停菜单功能失效 (鼠标事件兼容性)**
- **现象**: `@mouseenter` 和 `@mouseleave` 事件完全无响应,悬停菜单不显示;控制台显示状态变化但菜单不可见或定位错误;鼠标悬停在蓝色方格功能按钮上无任何反应。
- **根本原因**:
1. **平台条件编译缺失**: uni-app x 对鼠标事件有特殊处理机制,所有 `@mouseenter/@mouseleave` 的处理函数必须包装在 `#ifdef H5` 条件编译中才能生效,直接编写的代码会被平台忽略。
2. **CSS 定位兼容性**: 使用 `top: 100%` 百分比定位在 uni-app x 中计算不准确,导致菜单定位错误或不可见。
3. **容器设置不当**: 缺少 `height: 100%` 和 `overflow: visible` 设置,导致悬停检测区域不完整或菜单被裁切。
- **对比分析**: 参考 `AdminHeader.uvue` 中工作正常的个人信息悬停菜单实现,发现以下关键差异:
```javascript
// ❌ 错误写法 - 不工作
function handleFunctionMenuOver(e: any): void {
functionMenuVisible.value = true // 直接执行,被 uni-app x 忽略
}
// ✅ 正确写法 - 工作
function handleFunctionMenuOver(e: any): void {
// #ifdef H5
functionMenuVisible.value = true // 包装在条件编译中才生效
// #endif
}
```
- **完整解决方案**:
1. **添加条件编译** (最关键):
```javascript
function handleFunctionMenuOver(e: any): void {
// #ifdef H5
if (hideFunctionMenuTimer !== null) {
clearTimeout(hideFunctionMenuTimer as number)
hideFunctionMenuTimer = null
}
functionMenuVisible.value = true
// #endif
}
```
2. **修复 CSS 定位**:
```css
.function-menu {
position: absolute;
top: 44px; /* 使用固定像素值44px是tags容器高度 */
right: 0;
overflow: visible; /* 确保菜单不被裁切 */
}
```
3. **优化容器设置**:
```css
.function-btn-wrapper {
height: 100%; /* 确保悬停不中断仿照AdminHeader */
overflow: visible;
}
```
4. **统一定时器延迟**: 所有定时器延迟设置为 150ms与 AdminHeader 保持一致。
- **推广规则**:
1. uni-app x 中的所有鼠标事件处理必须包装在 `#ifdef H5` 中。
2. 悬停菜单定位优先使用固定像素值而非百分比。
3. 容器必须设置正确的 `height` 和 `overflow` 属性。
4. 成功的悬停菜单实现应该作为模板复用到其他组件。
## 🛠️ 完整修复流程
```
@@ -1947,7 +2006,6 @@ const iconMap: Record<string, string> = {
- **CRMEB 路由映射**: 1:1 复刻 CRMEB 的路由和菜单结构
- **双侧边栏布局**: 主侧边栏(一级) + 二级侧边栏(分组)
## 🎯 阶段十八: Vue/Vite 编译失败导致的连锁依赖雪崩 (500 错误与动态导入阻断)
### **原因三十二SCSS 括号闭合错误引发的 ?import 连锁报错**
@@ -1962,8 +2020,9 @@ const iconMap: Record<string, string> = {
- **解决方案**:
1. **禁止盲目改路由****绝对不要**因为看到 homePage 报错就去重写 homePage 或者怀疑路由表配错了。
2. **顺藤摸瓜找源头**:沿着浏览器 Network 或者 Console 错误的最顶部往上翻,找到第一个且唯一一个抛出 500 的资源(在本例中是 lang.scss
3. **修复语法树**:回到那个触发 500 的文件,检查并修复 emplate、script、style 标签的闭锁以及其内部(特别是 SCSS 嵌套)的语法错误(如括号配对)。语法自洽后,整个异步组件树便会瞬间全量恢复正常。
3. **修复语法树**:回到那个触发 500 的文件,检查并修复 emplate、script、style 标签的闭锁以及其内部(特别是 SCSS 嵌套)的语法错误(如括号配对)。语法自洽后,整个异步组件树便会瞬间全量恢复正常。
- **防止复发规范**: 当执行文件全局批量替换或目录大迁移后,切勿遗留未闭合的代码块。修复问题必须采用“由底向外”的收敛原则。
---
这个指南现在涵盖了 uni-app-x 项目开发中最常见的 17 类问题(新增动态导入与语法遮蔽解析),为后续开发提供了完整的故障排除和最佳实践指导。 🚀
@@ -2206,4 +2265,3 @@ curl -i -X OPTIONS "http://192.168.1.61:9122/rest/v1/ml_coupon_templates?select=
---
这个指南现在涵盖了 uni-app-x 项目开发中最常见的 33 类问题(包含全局组件化最佳实践),为后续开发提供了完整的故障排除和标准化指导。 🚀