当前位置: 首页 > news >正文

提示条贴合右侧边栏

好的,很高兴能为你提供这个Vue3 + Element Plus项目中自定义加载组件的设计思路和具体代码实现。这个需求非常典型,因为它不仅包含组件的视觉设计,还涉及到了组件间通信、状态管理和 DOM 交互,非常适合用来学习和巩固 Vue 的核心概念。

我将按照一个互联网大厂前端开发的标准和流程,为你深入浅出地讲解。


1. 设计思路与架构(Design Philosophy)

在动手写代码之前,我们先进行高层设计。这是一个好习惯,能让我们的代码结构更清晰、更易于维护。

1.1. 组件拆分(Component Decomposition)

首先,我们会将这个功能拆分为两个核心部分:

  1. 父组件 (Parent Component): 比如叫做 DataDisplayPage.vue。这个组件是“指挥官”,它负责:

    • 包含可拖动的右侧边栏和主内容区域。
    • 管理核心的业务逻辑状态,例如:'idle' (空闲), 'loading' (加载中), 'error' (错误), 'success' (成功)。
    • 发起获取数据的API请求。
    • 根据API请求的结果,更新状态。
    • 控制加载提示组件的显示和隐藏,并传递当前的状态给它。
    • 实现侧边栏的拖动逻辑,并把侧边栏的宽度实时同步。
  2. 子组件 (Child Component): 我们就叫它 LoadingNotification.vue。这个组件是“展示者”,它非常“纯粹”,只负责:

    • 接收父组件传递过来的状态 (props)。
    • 根据不同的状态,显示不同的UI(“正在加载中…”+旋转图标,或“数据异常”+重试按钮)。
    • 当用户点击“重新获取数据”按钮时,通知父组件 (emits)。
    • 它不关心数据是怎么来的,也不关心侧边栏是怎么拖动的,只关心自己的外观和位置。

为什么这么拆分?
这就是关注点分离 (Separation of Concerns) 的设计原则。父组件管逻辑和状态,子组件管UI展示。这样做的好处是:

  • 高内聚,低耦合: LoadingNotification.vue 组件可以被复用到项目的任何地方,因为它不依赖任何特定的页面逻辑。
  • 易于维护: 修改提示条的样式只需要改子组件,修改数据获取逻辑只需要改父组件,互不影响。
  • 清晰的单向数据流: 数据从父组件通过 props 流向子组件,子组件通过 emits 将事件发送给父组件。这是 Vue 推荐的最佳实践。
1.2. 定位与拖动策略 (Positioning Strategy)

这是本需求的难点和亮点。提示框要跟随可拖动侧边栏移动。

  • 我们不能将提示框放在侧边栏内部,因为提示框在侧边栏的左侧
  • 我们也不能简单地使用 fixed 定位,因为它需要相对侧边栏移动。

最佳方案是:

  1. 父组件的主内容区域(Main Content Area)使用 position: relative;
  2. LoadingNotification.vue 组件将被放置在主内容区域中,并使用 position: absolute;
  3. 父组件会有一个 ref 变量,比如 sidebarWidth,用来存储侧边栏的实时宽度。
  4. 我们会通过一个 computed 属性来动态计算 LoadingNotificationstyle。具体来说,它的 right 值将是 sidebarWidth.value + 'px' 再加上一些间距。
  5. 当用户拖动侧边栏时,sidebarWidth 的值会改变,computed 属性会重新计算,从而 Vue 会自动更新 LoadingNotificationstyle,实现跟随效果。

2. 代码实现 (Implementation)

现在,我们开始一步步编写代码。

步骤 1: 创建 LoadingNotification.vue 子组件

这是我们的UI展示组件。

// src/components/LoadingNotification.vue<script setup lang="ts">
import { computed } from 'vue';// 1. 定义组件接收的props
// 使用TS来定义类型,更规范
// 'loading': 加载中状态
// 'error': 错误状态
const props = defineProps<{state: 'loading' | 'error';
}>();// 2. 定义组件可以触发的事件
const emit = defineEmits<{(e: 'retry'): void; // 定义一个名为 'retry' 的事件
}>();// 3. 使用计算属性来根据 state 返回不同的文本
const message = computed(() => {return props.state === 'loading' ? '正在加载中' : '数据异常';
});// 4. 定义重试按钮的点击事件处理函数
function handleRetry() {// 当按钮被点击时,触发 retry 事件通知父组件emit('retry');
}
</script><template><div class="loading-notification"><div class="message-area">{{ message }}</div><div class="spinner-area"><div v-if="state === 'loading'" class="loading-spinner"></div><button v-if="state === 'error'" @click="handleRetry" class="retry-button">重新获取</button></div></div>
</template><style scoped>
/* 使用 scoped 来确保样式只作用于当前组件,避免全局污染 */
.loading-notification {/* 绝对定位,相对于父容器 */position: absolute;top: 20px; /* 距离顶部20px *//* right 属性将由父组件动态提供 */z-index: 2000; /* 确保在顶层 */display: flex;align-items: center;padding: 10px 16px;background-color: #ffffff;border-radius: 8px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);transition: opacity 0.3s ease, transform 0.3s ease;color: #303133;
}.message-area {font-size: 14px;margin-right: 12px;
}.spinner-area {display: flex;align-items: center;justify-content: center;
}/* 这是我们手写的旋转空心圆 */
.loading-spinner {width: 16px;height: 16px;border-radius: 50%;border: 2px solid #E4E7ED; /* 灰色部分 */border-top-color: #409EFF; /* 蓝色部分 */animation: spin 1s linear infinite;
}/* 定义旋转动画 */
@keyframes spin {from {transform: rotate(0deg);}to {transform: rotate(360deg);}
}.retry-button {/* 简单的按钮样式,你可以使用 Element Plus 的 ElButton 替代 */font-size: 12px;padding: 4px 8px;border: 1px solid #409EFF;color: #409EFF;background-color: transparent;border-radius: 4px;cursor: pointer;outline: none;
}.retry-button:hover {background-color: #ecf5ff;
}
</style>
步骤 2: 创建父组件 DataDisplayPage.vue

这是我们的页面和逻辑控制中心。

// src/views/DataDisplayPage.vue<script setup lang="ts">
import { ref, computed } from 'vue';
import LoadingNotification from '@/components/LoadingNotification.vue';// --- State Management ---
// 侧边栏的初始宽度
const sidebarWidth = ref<number>(250);
// 加载状态,'idle' 表示初始/成功状态,此时不显示提示
type LoadingState = 'idle' | 'loading' | 'error';
const loadingState = ref<LoadingState>('idle');// 控制提示框是否应该显示
const showNotification = computed(() => {return loadingState.value === 'loading' || loadingState.value === 'error';
});// --- API Simulation ---
// 模拟API调用
function mockApiCall(shouldFail: boolean = false): Promise<string> {console.log('开始获取数据...');return new Promise((resolve, reject) => {setTimeout(() => {if (shouldFail) {console.error('API请求失败');reject(new Error('Backend error'));} else {console.log('API请求成功');resolve('这是从后端获取的数据');}}, 2000); // 模拟2秒的网络延迟});
}// --- Event Handlers ---
// "获取数据" 按钮的点击事件
async function handleGetData() {loadingState.value = 'loading'; // 进入加载中状态try {const data = await mockApiCall(false); // 模拟成功// const data = await mockApiCall(true); // 取消注释此行以模拟失败console.log('数据处理成功:', data);loadingState.value = 'idle'; // 数据成功返回,恢复idle状态,提示框消失} catch (error) {loadingState.value = 'error'; // 捕获到异常,进入错误状态}
}// "获取异常数据" 按钮的点击事件,用于演示
async function handleGetErrorData() {loadingState.value = 'loading';try {await mockApiCall(true); // 强制失败} catch (error) {loadingState.value = 'error';}
}// 处理子组件发出的 'retry' 事件
async function handleRetry() {console.log('接收到重试请求,重新获取数据...');// 重新尝试获取数据,可以复用 handleGetData 逻辑// 这里我们直接调用会失败的接口,方便演示await handleGetErrorData();
}// --- Draggable Sidebar Logic ---
const isDragging = ref(false);function startDrag(event: MouseEvent) {isDragging.value = true;// 监听整个窗口的 mousemove 和 mouseup 事件window.addEventListener('mousemove', onDrag);window.addEventListener('mouseup', stopDrag);
}function onDrag(event: MouseEvent) {if (!isDragging.value) return;// 实时计算侧边栏宽度// window.innerWidth 是浏览器窗口的总宽度// event.clientX 是鼠标当前的X坐标// 新的宽度 = 窗口宽度 - 鼠标X坐标const newWidth = window.innerWidth - event.clientX;// 设置一个最小和最大宽度防止拖动出界if (newWidth > 200 && newWidth < 600) {sidebarWidth.value = newWidth;}
}function stopDrag() {isDragging.value = false;// 拖动结束,移除事件监听器,避免不必要的性能开销window.removeEventListener('mousemove', onDrag);window.removeEventListener('mouseup', stopDrag);
}// --- Computed Styles ---
// 动态计算 LoadingNotification 的样式
const notificationStyle = computed(() => {return {// 关键:right 的值依赖于 sidebarWidth// 20px 是提示框和侧边栏之间的间距right: `${sidebarWidth.value + 20}px`,};
});
</script><template><div class="page-layout"><main class="main-content"><div class="controls"><el-button type="primary" @click="handleGetData">点击获取数据 (模拟成功)</el-button><el-button type="danger" @click="handleGetErrorData">点击获取数据 (模拟失败)</el-button></div><h1>主内容区域</h1><p>这里的宽度会随着右侧边栏的拖动而变化。</p><LoadingNotificationv-if="showNotification":style="notificationStyle":state="loadingState"@retry="handleRetry"/></main><aside class="sidebar" :style="{ width: `${sidebarWidth}px` }"><div class="drag-handle" @mousedown="startDrag"></div><div class="sidebar-content"><h2>可拖动侧边栏</h2><p>从左侧边缘拖动来改变宽度。</p></div></aside></div>
</template><style scoped>
.page-layout {display: flex;height: 100vh; /* 占满整个视口高度 */width: 100%;overflow: hidden; /* 防止出现滚动条 */
}/* 主内容区域需要相对定位,作为 LoadingNotification 的定位父级 */
.main-content {flex-grow: 1; /* 占据剩余所有空间 */position: relative; /* 关键! */padding: 20px;background-color: #f0f2f5;
}.controls {margin-bottom: 20px;
}.sidebar {flex-shrink: 0; /* 防止侧边栏被压缩 */position: relative; /* 为拖动条定位 */background-color: #ffffff;border-left: 1px solid #dcdfe6;
}.sidebar-content {padding: 20px;
}.drag-handle {position: absolute;left: -5px; /* 稍微突出一点,方便点击 */top: 0;height: 100%;width: 10px;cursor: col-resize; /* 鼠标样式变为列拖动 */z-index: 10;
}
</style>

3. 如何使用与讲解 (How to Use & Explanation)

  1. 文件结构:

    • LoadingNotification.vue 文件放在 src/components/ 目录下。
    • DataDisplayPage.vue 文件放在 src/views/ 目录下。
    • 确保你的项目已经安装了 vue, element-plus
  2. 核心概念讲解 (适合初学者):

    • <script setup>: 这是 Vue 3 中推荐的组合式 API (Composition API) 语法糖。它让代码更简洁,所有在 <script setup> 中定义的变量和函数都可以直接在 <template> 中使用。
    • ref: 用来创建一个响应式变量。当 ref 变量的值改变时,Vue 会自动更新界面上用到这个变量的地方。我们用它来存储 sidebarWidthloadingState
    • computed: 计算属性。它像一个智能的变量,会根据其他响应式变量(如 ref)的变化而自动重新计算自己的值。我们用它来动态生成 notificationStyle,这是实现跟随效果的灵魂。
    • propsemits: 这是父子组件通信的“标准协议”。props 让父组件可以“发号施令”给子组件(比如告诉它当前是什么状态),emits 让子组件可以“汇报情况”给父组件(比如告诉它“用户点击了重试按钮”)。
    • CSS position: 我们利用 position: relativeposition: absolute 建立了一个定位上下文。子组件的 absolute 定位是相对于第一个 position 不是 static 的祖先元素。在这里,就是我们的 .main-content
    • 事件监听 (@mousedown, @mousemove, @mouseup): 这是实现拖动功能的标准 DOM 操作。我们在父组件中处理这些事件,因为是父组件控制着侧边栏的宽度,体现了“谁的数据谁负责”的原则。

4. 大厂规范与其它方案

  • 代码规范: 以上代码遵循了 TypeScript 类型定义、ESLint 推荐规则(如 const 优先)、单向数据流等,符合大厂的开发规范。注释清晰,变量命名有意义。
  • CSS 变量: 在大型项目中,颜色、间距等值可以使用 CSS 变量 (var(--el-color-primary)) 来定义,方便进行全局主题切换。
  • 替代方案: Element Plus 自带 Loading: 对于简单场景,可以直接使用 Element Plus 提供的 ElLoading 服务或指令。
    • 服务调用: const loading = ElLoading.service({ text: 'Loading...' }),然后在数据返回后调用 loading.close()
    • 指令: <div v-loading="isLoading"></div>
    • 为什么我们不直接用? 因为你的需求非常具体(特定的样式、错误状态和重试按钮、跟随侧边栏移动的定位),手写一个自定义组件能提供最大的灵活性和控制力,这也是一个很好的学习过程。

希望这份详尽的指南能帮助你成功实现这个功能,并对 Vue 的核心思想有更深入的理解!

http://www.lryc.cn/news/615274.html

相关文章:

  • Java 大视界 -- Java 大数据在智能家居场景联动与用户行为模式挖掘中的应用(389)
  • 虚拟机Ubuntu重启发现找不到共享文件夹
  • 2025AI颠覆认知!解锁智能新纪元
  • ubuntu修改密码
  • Java基础-TCP通信(多发多收和一发一收)
  • webrtc弱网-BandwidthQualityScaler 源码分析与算法原理
  • 基于 RAUC 的 Jetson OTA 升级全攻略
  • RAGFoundry:面向检索增强生成的模块化增强框架
  • 功能测试中常见的面试题-一
  • DataDex 多样化 JSON 服务——使用教程
  • linux php版本降级,dnf版本控制
  • 在CoT中为什么仅用方程式提示不够
  • drippingblues靶机教程
  • Spring Boot自定义Starter:从原理到实战全解析
  • AutoML 的下半场——从“模型选择”到“端到端业务闭环”
  • [Oracle] SUBSTR()函数
  • 【代码篇】关于PartiallyPassword插件_实现文章加密
  • 【工作流引擎】Flowable 和 Activiti
  • Web前端之 ECMAScript6
  • [激光原理与应用-204]:光学器件 - LD激光二极管工作原理以及使用方法
  • 人类语义认知统一模型:融合脑科学与AI的突破
  • VisionPro常用标定方式
  • 数据结构—二叉树及gdb的应用
  • Linux网络编程:TCP的远程多线程命令执行
  • 202506 电子学会青少年等级考试机器人四级器人理论真题
  • Baumer高防护相机如何通过YoloV8深度学习模型实现火星陨石坑的检测识别(C#代码UI界面版)
  • 开发手札:UnrealEngine和Unity3d坐标系问题
  • CSS 选择器进阶:用更聪明的方式定位元素
  • kubectl get node k8s-node01 -o yaml | grep taint -B 5 -A 5
  • 开源智能手机安全相机推荐:Snap Safe