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

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);
}
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>


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