2025-09-07
Angular
00
请注意,本文编写于 85 天前,最后修改于 85 天前,其中某些信息可能已经过时。

目录

效果如下
tooltip组件源码
使用demo

有时候我们在使用echarts时会遇到一种情况就是,UI给的效果图使用echarts配置起来特别费劲,这时候我们可能需要使用原生html简单实现,但是原生的title显示的内容太单调了,这时候就需要自定义一个tooltip组件,实现和echarts tooltip一样的效果,也能跟随鼠标移动显示

本实例使用的是angular 7.x

效果如下

image.png

tooltip组件源码

tooltip.directive.ts 用来绑定tooltip的触发元素

ts
// tooltip.directive.ts (Angular 7 兼容版) import { Directive, ElementRef, Input, HostListener } from '@angular/core'; import { TooltipComponent } from './tooltip.component'; @Directive({ selector: '[appTooltip]' }) export class TooltipDirective { @Input('appTooltip') tooltip: TooltipComponent; // Angular 7 需要正确声明选择器 @Input() tooltipPosition: string = ''; @Input() tooltipContext: any; // 新增:用于传递上下文数据 constructor(private elementRef: ElementRef) {} @HostListener('mouseenter', ['$event']) onMouseEnter(event: MouseEvent) { debugger; console.log('mouseenter'); if (!this.tooltip) return; // 将上下文传递给tooltip组件 this.tooltip.context = this.tooltipContext; this.tooltip.positionStyle = this.tooltipPosition; this.tooltip.show(); // 获取触发元素的边界矩形 const triggerRect = this.elementRef.nativeElement.getBoundingClientRect(); console.log(this.elementRef.nativeElement, triggerRect); this.tooltip.updatePosition(triggerRect); } @HostListener('mouseleave') onMouseLeave() { console.log('mouseleave'); console.log('xxx', this.tooltip); if (!this.tooltip) return; this.tooltip.hide(); } @HostListener('mousemove', ['$event']) onMouseMove(event: MouseEvent) { debugger; console.log('mousemove'); if (!this.tooltip || !this.tooltip.isOpen || this.tooltipPosition) return; // 传递鼠标位置给 tooltip 组件 this.tooltip.mousePosition = { x: event.clientX, y: event.clientY }; this.tooltip.updatePosition(); } }

tooltip.component.ts

ts
// tooltip.component.ts (Angular 7 兼容版) import { Component, Input, ElementRef, ViewChild, Renderer2, AfterViewInit, OnDestroy, OnInit, SimpleChanges, TemplateRef } from '@angular/core'; @Component({ selector: 'app-tooltip', templateUrl: './tooltip.component.html', styleUrls: ['./tooltip.component.css'] }) export class TooltipComponent implements AfterViewInit, OnDestroy { @ViewChild('tooltip') tooltip: ElementRef; // Angular 7 不需要 static 选项 @Input() position: string = 'top'; @Input() appendToBody = true; // 新增一个属性来存储上下文 @Input() context: any; @Input() contentTemplate: TemplateRef<any>; isOpen = false; positionStyle = ''; mousePosition = { x: 0, y: 0 }; private readonly offset = 12; constructor(private renderer: Renderer2, private el: ElementRef) {} ngAfterViewInit() { // 在 Angular 7 中使用 setTimeout 确保 DOM 完全渲染 setTimeout(() => { if (this.appendToBody && typeof document !== 'undefined') { document.body.appendChild(this.el.nativeElement); } }); } show() { this.isOpen = true; // 使用 setTimeout 确保显示后再计算位置 //setTimeout(() => this.updatePosition()); } hide() { this.isOpen = false; //setTimeout(() => this.updatePosition()); } updatePosition(triggerRect?: ClientRect) { // 在 Angular 7 中使用 setTimeout 确保 DOM 已更新 setTimeout(() => { if (!this.isOpen || !this.tooltip) return; this.calculatePosition(triggerRect); }); } private calculatePosition(triggerRect?: DOMRect) { const tooltipEl = this.tooltip.nativeElement; const tooltipRect = tooltipEl.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; let x = 0, y = 0; if (this.positionStyle && triggerRect) { // 修复位置计算逻辑 - 基于触发元素位置 const triggerRight = triggerRect.right; const triggerLeft = triggerRect.left; const triggerTop = triggerRect.top; const triggerBottom = triggerRect.bottom; const triggerWidth = triggerRect.width; const triggerHeight = triggerRect.height; switch (this.positionStyle) { case 'top': x = triggerLeft + triggerWidth / 2 - tooltipRect.width / 2; y = triggerTop - tooltipRect.height - this.offset; break; case 'top-left': x = triggerLeft; y = triggerTop - tooltipRect.height - this.offset; break; case 'top-right': x = triggerRight - tooltipRect.width; y = triggerTop - tooltipRect.height - this.offset; break; case 'right': x = triggerRight + this.offset; y = triggerTop + triggerHeight / 2 - tooltipRect.height / 2; break; case 'right-top': x = triggerRight + this.offset; y = triggerTop; break; case 'right-bottom': x = triggerRight + this.offset; y = triggerBottom - tooltipRect.height; break; case 'bottom': x = triggerLeft + triggerWidth / 2 - tooltipRect.width / 2; y = triggerBottom + this.offset; break; case 'bottom-left': x = triggerLeft; y = triggerBottom + this.offset; break; case 'bottom-right': x = triggerRight - tooltipRect.width; y = triggerBottom + this.offset; break; case 'left': x = triggerLeft - tooltipRect.width - this.offset; y = triggerTop + triggerHeight / 2 - tooltipRect.height / 2; break; case 'left-top': x = triggerLeft - tooltipRect.width - this.offset; y = triggerTop; break; case 'left-bottom': x = triggerLeft - tooltipRect.width - this.offset; y = triggerBottom - tooltipRect.height; break; case 'center': x = triggerLeft + triggerWidth / 2 - tooltipRect.width / 2; y = triggerTop + triggerHeight / 2 - tooltipRect.height / 2; break; default: x = triggerLeft; y = triggerTop + triggerHeight + this.offset; } } else if (this.mousePosition.x > 0 && this.mousePosition.y > 0) { // 鼠标跟随模式 x = this.mousePosition.x + this.offset; y = this.mousePosition.y + this.offset; } else { // 默认位置 x = 100; y = 100; } // 边界检查 - 确保不会超出视口 if (x < this.offset) { x = this.offset; } else if (x + tooltipRect.width > windowWidth) { x = windowWidth - tooltipRect.width - this.offset; } if (y < this.offset) { y = this.offset; } else if (y + tooltipRect.height > windowHeight) { y = windowHeight - tooltipRect.height - this.offset; } // 应用位置 this.renderer.setStyle(tooltipEl, 'left', `${x}px`); this.renderer.setStyle(tooltipEl, 'top', `${y}px`); this.renderer.setStyle(tooltipEl, 'opacity', '1'); } ngOnDestroy() { if ( this.appendToBody && typeof document !== 'undefined' && document.body.contains(this.el.nativeElement) ) { document.body.removeChild(this.el.nativeElement); } } }
html
<!-- tooltip.component.html --> <div class="tooltip-container" #tooltip [class.visible]="isOpen"> <ng-container *ngIf="contentTemplate; else defaultTemplate"> <ng-container *ngTemplateOutlet="contentTemplate; context:{$implicit: context,data:context}"></ng-container> </ng-container> <ng-template #defaultTemplate> <ng-container *ngIf="context; else defaultContent"> <!-- 默认的上下文显示 --> <div class="custom-tooltip-content"> <div class="tooltip-header"> <b>{{context.title}}</b> </div> <div class="tooltip-body"> {{context.value}} </div> </div> </ng-container> <ng-template #defaultContent> <ng-content></ng-content> </ng-template> </ng-template> </div>
less
/* tooltip.component.css - 增加箭头样式 */ .tooltip-container { position: absolute; z-index: 1000; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border-radius: 4px; background: white; max-width: 300px; transition: opacity 0.3s ease, visibility 0.3s ease; opacity: 0; visibility: hidden; pointer-events: none; transform-origin: center; } .tooltip-container.visible { opacity: 1; visibility: visible; pointer-events: auto; } .tooltip-arrow { position: absolute; width: 10px; height: 10px; background: white; transform: rotate(45deg); z-index: -1; } /* 智能位置调整箭头 */ [data-position="top-left"] .tooltip-arrow, [data-position="top"] .tooltip-arrow, [data-position="top-right"] .tooltip-arrow { bottom: -5px; } [data-position="top-left"] .tooltip-arrow { left: 10px; } [data-position="top"] .tooltip-arrow { left: 50%; transform: translateX(-50%) rotate(45deg); } [data-position="top-right"] .tooltip-arrow { right: 10px; } [data-position="bottom-left"] .tooltip-arrow, [data-position="bottom"] .tooltip-arrow, [data-position="bottom-right"] .tooltip-arrow { top: -5px; } [data-position="bottom-left"] .tooltip-arrow { left: 10px; } [data-position="bottom"] .tooltip-arrow { left: 50%; transform: translateX(-50%) rotate(45deg); } [data-position="bottom-right"] .tooltip-arrow { right: 10px; } [data-position="left"] .tooltip-arrow { right: -5px; top: 50%; transform: translateY(-50%) rotate(45deg); } [data-position="right"] .tooltip-arrow { left: -5px; top: 50%; transform: translateY(-50%) rotate(45deg); }

使用demo

html
<div class="excharts-container" id="charts3"> <div *ngFor="let item of charts3.data; let index = index" class="vertical-bar-item"> <div class="index">{{index+1}}</div> <div class="name">{{item.name}}</div> <div class="barbox" [appTooltip]="tooltipRef"> <div class="bar1" [ngStyle]="{'width': (item.value/ charts2.data[0].value)*100 +'%' }"></div> <div class="bar2" [ngStyle]="{'width': 100 - (item.value/ charts2.data[0].value)*100 +'%' }"></div> </div> <app-tooltip #tooltipRef> <div class="my-custom-template"> <h3>{{item.name}} - {{item.value}}</h3> <p>还可以是任意内容</p> </div> </app-tooltip> <div class="value num"> {{formatNum(item.value)}} </div> </div> </div>

高级用法

html
<div class="card_body"> <div class="excharts-container" id="charts3"> <div *ngFor="let item of charts3.data; let index = index" class="vertical-bar-item"> <div class="index">{{index+1}}</div> <div class="name">{{item.name}}</div> <div class="barbox" [appTooltip]="tooltipRef" [tooltipContext]="item"> <div class="bar1" [ngStyle]="{'width': (item.value/ charts2.data[0].value)*100 +'%' }"></div> <div class="bar2" [ngStyle]="{'width': 100 - (item.value/ charts2.data[0].value)*100 +'%' }"></div> </div> <div class="value num"> {{formatNum(item.value)}} </div> </div> </div> <app-tooltip #tooltipRef [contentTemplate]="customTooltipTemplate"> </app-tooltip> <ng-template #customTooltipTemplate let-data="data"> <div class="my-custom-template"> <h3 *ngIf="data"> {{data.name}} </h3> <p>还可以是任意内容</p> </div> </ng-template> </div>
如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:繁星

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!