完成导航栏功能
This commit is contained in:
@@ -1,6 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="tags">
|
<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">
|
<transition-group name="tag-list" tag="view" class="tags-row">
|
||||||
<view
|
<view
|
||||||
v-for="t in tabs"
|
v-for="t in tabs"
|
||||||
@@ -18,40 +34,87 @@
|
|||||||
</transition-group>
|
</transition-group>
|
||||||
</scroll-view>
|
</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
|
<view
|
||||||
v-if="menuVisible"
|
class="function-btn"
|
||||||
class="context-menu"
|
@click="toggleFunctionMenu"
|
||||||
:style="{ top: menuY + 'px', left: menuX + 'px' }"
|
|
||||||
@click.stop=""
|
|
||||||
>
|
>
|
||||||
<view class="menu-item" @click="handleAction('refresh')">
|
<view class="function-btn-icon">
|
||||||
<text class="menu-icon">↻</text>
|
<text class="function-btn-text">⋯</text>
|
||||||
<text class="menu-text">刷新</text>
|
</view>
|
||||||
</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>
|
v-if="functionMenuVisible"
|
||||||
<view class="menu-item" @click="handleAction('close-other')">
|
class="function-menu"
|
||||||
<text class="menu-icon">↸</text>
|
@click.stop=""
|
||||||
<text class="menu-text">关闭其他</text>
|
@mouseenter="handleMenuOver"
|
||||||
</view>
|
@mouseleave="handleMenuLeave"
|
||||||
<view class="menu-item" @click="handleAction('close-all')">
|
>
|
||||||
<text class="menu-icon">⊘</text>
|
<view
|
||||||
<text class="menu-text">全部关闭</text>
|
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>
|
||||||
</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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<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'
|
import type { TabItem } from '../store/adminNavStore.uts'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
tabs: TabItem[]
|
tabs: TabItem[]
|
||||||
activeTabId: string
|
activeTabId: string
|
||||||
}>()
|
}>()
|
||||||
@@ -64,127 +127,638 @@ const emit = defineEmits<{
|
|||||||
(e:'refresh'): void
|
(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 menuX = ref(0)
|
||||||
const menuY = ref(0)
|
const menuY = ref(0)
|
||||||
const selectedTab = ref<TabItem | null>(null)
|
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) {
|
function onTabClick(tab: TabItem) {
|
||||||
closeMenu()
|
closeAllMenus()
|
||||||
emit('tab-click', tab)
|
emit('tab-click', tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 右键菜单处理
|
||||||
|
// ============================================
|
||||||
|
|
||||||
function openContextMenu(e: MouseEvent, tab: TabItem) {
|
function openContextMenu(e: MouseEvent, tab: TabItem) {
|
||||||
selectedTab.value = tab
|
selectedTab.value = tab
|
||||||
menuX.value = e.clientX
|
menuX.value = e.clientX
|
||||||
menuY.value = e.clientY
|
menuY.value = e.clientY
|
||||||
|
|
||||||
// 边缘检测
|
// 边缘检测
|
||||||
if (menuX.value + 100 > window.innerWidth) {
|
if (menuX.value + 120 > window.innerWidth) {
|
||||||
menuX.value = window.innerWidth - 110
|
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) {
|
function handleFunctionMenuOver(e: any): void {
|
||||||
if (!selectedTab.value) return
|
// #ifdef H5
|
||||||
|
console.log('=== 功能菜单悬停触发 (H5) ===')
|
||||||
|
console.log('functionMenuVisible before:', functionMenuVisible.value)
|
||||||
|
|
||||||
const id = selectedTab.value!.id
|
if (hideFunctionMenuTimer !== null) {
|
||||||
|
clearTimeout(hideFunctionMenuTimer as number)
|
||||||
if (type === 'refresh') {
|
hideFunctionMenuTimer = null
|
||||||
emit('refresh')
|
console.log('清理定时器')
|
||||||
} 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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(() => {
|
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(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('click', closeMenu)
|
window.removeEventListener('click', closeAllMenus)
|
||||||
|
window.removeEventListener('resize', updateScrollInfo)
|
||||||
|
|
||||||
|
// 清理功能菜单定时器
|
||||||
|
if (hideFunctionMenuTimer !== null) {
|
||||||
|
clearTimeout(hideFunctionMenuTimer as number)
|
||||||
|
hideFunctionMenuTimer = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.tags{
|
/* ============================================
|
||||||
|
顶部标签栏整体布局 (修正 overflow 和 z-index)
|
||||||
|
============================================ */
|
||||||
|
.tags {
|
||||||
height: 44px;
|
height: 44px;
|
||||||
background:#fff;
|
background: #fff;
|
||||||
border-bottom: 1px solid #eef2f7;
|
border-bottom: 1px solid #eef2f7;
|
||||||
display:flex;
|
display: flex;
|
||||||
flex-direction:row;
|
flex-direction: row;
|
||||||
align-items:center;
|
align-items: center;
|
||||||
position: relative;
|
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;
|
gap: 8px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
.tag{
|
|
||||||
|
/* ============================================
|
||||||
|
标签样式 (保持原有设计)
|
||||||
|
============================================ */
|
||||||
|
.tag {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
background:#fff;
|
background: #fff;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display:flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items:center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag:hover {
|
.tag:hover {
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
}
|
}
|
||||||
.tag.active{
|
|
||||||
border-color:#1677ff;
|
.tag.active {
|
||||||
background:#eaf2ff;
|
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;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display:flex;
|
display: flex;
|
||||||
flex-direction:row;
|
flex-direction: row;
|
||||||
align-items:center;
|
align-items: center;
|
||||||
justify-content: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 {
|
.context-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
background: #fff;
|
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;
|
padding: 5px 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
min-width: 100px;
|
min-width: 120px;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #ebeef5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
通用菜单项样式 (右键菜单和功能菜单共用)
|
||||||
|
============================================ */
|
||||||
.menu-item {
|
.menu-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -192,26 +766,43 @@ onUnmounted(() => {
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item:hover {
|
/* #ifdef H5 */
|
||||||
|
.menu-item:hover:not(.disabled) {
|
||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-item:hover:not(.disabled) .menu-text {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
|
||||||
|
.menu-item.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-icon {
|
.menu-icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-text {
|
.menu-text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
|
flex: 1;
|
||||||
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标签列表动画 - 平滑平移和缩放 */
|
/* ============================================
|
||||||
|
标签列表动画 (保持原有设计)
|
||||||
|
============================================ */
|
||||||
.tag-list-enter-active,
|
.tag-list-enter-active,
|
||||||
.tag-list-leave-active {
|
.tag-list-leave-active {
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
@@ -232,7 +823,9 @@ onUnmounted(() => {
|
|||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 右键菜单动画 - 类似 CRMEB 的缩放渐入 */
|
/* ============================================
|
||||||
|
菜单动画 (右键菜单和功能菜单)
|
||||||
|
============================================ */
|
||||||
.menu-fade-enter-active,
|
.menu-fade-enter-active,
|
||||||
.menu-fade-leave-active {
|
.menu-fade-leave-active {
|
||||||
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.5, 1);
|
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||||
@@ -244,4 +837,50 @@ onUnmounted(() => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.8) translateY(-10px);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -206,6 +206,65 @@
|
|||||||
2. **移除组件级 Margin**: Grid 布局应使用 `gap` 属性控制间距,组件自身 (`.user-map-card`) 不应自带外部 Margin,否则会破坏等高计算。
|
2. **移除组件级 Margin**: Grid 布局应使用 `gap` 属性控制间距,组件自身 (`.user-map-card`) 不应自带外部 Margin,否则会破坏等高计算。
|
||||||
3. **强制 Canvas 填充**: 图表组件容器需设置 `width: 100% !important; height: 100% !important;` 且 `position: absolute` (配合父级 relative) 或 `flex: 1`,确保填满 Flex 空间。
|
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 的路由和菜单结构
|
- **CRMEB 路由映射**: 1:1 复刻 CRMEB 的路由和菜单结构
|
||||||
- **双侧边栏布局**: 主侧边栏(一级) + 二级侧边栏(分组)
|
- **双侧边栏布局**: 主侧边栏(一级) + 二级侧边栏(分组)
|
||||||
|
|
||||||
|
|
||||||
## 🎯 阶段十八: Vue/Vite 编译失败导致的连锁依赖雪崩 (500 错误与动态导入阻断)
|
## 🎯 阶段十八: Vue/Vite 编译失败导致的连锁依赖雪崩 (500 错误与动态导入阻断)
|
||||||
|
|
||||||
### **原因三十二:SCSS 括号闭合错误引发的 ?import 连锁报错**
|
### **原因三十二:SCSS 括号闭合错误引发的 ?import 连锁报错**
|
||||||
@@ -1962,8 +2020,9 @@ const iconMap: Record<string, string> = {
|
|||||||
- **解决方案**:
|
- **解决方案**:
|
||||||
1. **禁止盲目改路由**:**绝对不要**因为看到 homePage 报错就去重写 homePage 或者怀疑路由表配错了。
|
1. **禁止盲目改路由**:**绝对不要**因为看到 homePage 报错就去重写 homePage 或者怀疑路由表配错了。
|
||||||
2. **顺藤摸瓜找源头**:沿着浏览器 Network 或者 Console 错误的最顶部往上翻,找到第一个且唯一一个抛出 500 的资源(在本例中是 lang.scss)。
|
2. **顺藤摸瓜找源头**:沿着浏览器 Network 或者 Console 错误的最顶部往上翻,找到第一个且唯一一个抛出 500 的资源(在本例中是 lang.scss)。
|
||||||
3. **修复语法树**:回到那个触发 500 的文件,检查并修复 emplate、script、style 标签的闭锁以及其内部(特别是 SCSS 嵌套)的语法错误(如括号配对)。语法自洽后,整个异步组件树便会瞬间全量恢复正常。
|
3. **修复语法树**:回到那个触发 500 的文件,检查并修复 emplate、script、style 标签的闭锁以及其内部(特别是 SCSS 嵌套)的语法错误(如括号配对)。语法自洽后,整个异步组件树便会瞬间全量恢复正常。
|
||||||
- **防止复发规范**: 当执行文件全局批量替换或目录大迁移后,切勿遗留未闭合的代码块。修复问题必须采用“由底向外”的收敛原则。
|
- **防止复发规范**: 当执行文件全局批量替换或目录大迁移后,切勿遗留未闭合的代码块。修复问题必须采用“由底向外”的收敛原则。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
这个指南现在涵盖了 uni-app-x 项目开发中最常见的 17 类问题(新增动态导入与语法遮蔽解析),为后续开发提供了完整的故障排除和最佳实践指导。 🚀
|
这个指南现在涵盖了 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 类问题(包含全局组件化最佳实践),为后续开发提供了完整的故障排除和标准化指导。 🚀
|
这个指南现在涵盖了 uni-app-x 项目开发中最常见的 33 类问题(包含全局组件化最佳实践),为后续开发提供了完整的故障排除和标准化指导。 🚀
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user