当前位置: 首页 > 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.dtcms.com/a/323561.html

相关文章:

  • java web项目入门了解
  • 天地图,cesium,leaflet
  • java练习题:数字位数
  • Windows下使用PyInstaller打包PyQt项目
  • 第15届蓝桥杯Scratch图形化省赛中级组2024年8月24日真题
  • 4深度学习Pytorch-神经网络--损失函数(sigmoid、Tanh、ReLU、LReLu、softmax)
  • Linux-JSON Schema
  • Java类和对象课上练习题目设计
  • LLM 的向量的方向表示语义,向量长度表示什么
  • Docker容器lnmp平台部署discuz论坛
  • 工具类-高效集合差异计算工具DiffWrapper
  • visual studio 无明显错误,但是无法编译成功解决—仙盟创梦IDE
  • C++入门自学Day7-- String类的自实现
  • Adapting Vision-Language Models Without Labels A Comprehensive Survey
  • RWKV与VRWKV
  • Filter,Interceptor拦截器-登录校验
  • visual studio 字体设置
  • 【小米比笔记本Pro15.6】>>Stasrt PXE over IPv6,Press [Esc] to EXIT...
  • 第二十天:余数相同问题
  • 信息安全简要
  • 分布式锁详解及 Spring Boot 实战示例
  • Redis 持久化策略深度剖析:从原理到实战,守护数据不丢失
  • 基于 InfluxDB 的服务器性能监控系统实战(二)
  • [论文阅读] 人工智能 + 软件工程 | Posterior-GRPO:优化代码生成推理过程的新框架
  • Solana上Launchpad混战:新颖性应被重视
  • 云服务器--阿里云OSS(1)【阿里云OSS简单介绍以及环境准备】
  • 论文学习21:Pyramid Scene Parsing Network
  • AG32cpld实现一个UartTx“外设”
  • 莫比乌斯反演学习笔记
  • Qt 元对象系统中的 QMetaObject 类和他的invokeMethod() 函数及其他常见函数应用详解​