feat: 实现供水运营专题大屏BI可视化
- 新增OperationDashboard.vue全屏大屏组件 - 添加6个核心KPI指标卡片: 进水总量/出水总量/产销差率/营收额/平均水质/报警次数 - 实现6个ECharts图表: 供水趋势/水质分布/报警统计/管网空间/设备状态/营收分析 - 创建静态HTML版本operation-dashboard.html,使用CDN加载Vue3/ECharts/Element Plus - 更新路由配置,添加/operation路径支持 - 修复nextTick导入问题,优化build脚本 🚧 开发者: bot_dev1 📝 任务: #38 [BI] 运营仪表盘 + 供水专题大屏
This commit is contained in:
+511
@@ -0,0 +1,511 @@
|
||||
import * as util from '../core/util';
|
||||
import {devicePixelRatio} from '../config';
|
||||
import { ImagePatternObject } from '../graphic/Pattern';
|
||||
import CanvasPainter from './Painter';
|
||||
import { GradientObject, InnerGradientObject } from '../graphic/Gradient';
|
||||
import { ZRCanvasRenderingContext } from '../core/types';
|
||||
import Eventful from '../core/Eventful';
|
||||
import { ElementEventCallback } from '../Element';
|
||||
import { getCanvasGradient } from './helper';
|
||||
import { createCanvasPattern } from './graphic';
|
||||
import Displayable from '../graphic/Displayable';
|
||||
import BoundingRect from '../core/BoundingRect';
|
||||
import { REDRAW_BIT } from '../graphic/constants';
|
||||
import { platformApi } from '../core/platform';
|
||||
|
||||
function createDom(id: string, painter: CanvasPainter, dpr: number) {
|
||||
const newDom = platformApi.createCanvas();
|
||||
const width = painter.getWidth();
|
||||
const height = painter.getHeight();
|
||||
|
||||
const newDomStyle = newDom.style;
|
||||
if (newDomStyle) { // In node or some other non-browser environment
|
||||
newDomStyle.position = 'absolute';
|
||||
newDomStyle.left = '0';
|
||||
newDomStyle.top = '0';
|
||||
newDomStyle.width = width + 'px';
|
||||
newDomStyle.height = height + 'px';
|
||||
|
||||
newDom.setAttribute('data-zr-dom-id', id);
|
||||
}
|
||||
|
||||
newDom.width = width * dpr;
|
||||
newDom.height = height * dpr;
|
||||
|
||||
return newDom;
|
||||
}
|
||||
|
||||
export interface LayerConfig {
|
||||
// 每次清空画布的颜色
|
||||
clearColor?: string | GradientObject | ImagePatternObject
|
||||
// 是否开启动态模糊
|
||||
motionBlur?: boolean
|
||||
// 在开启动态模糊的时候使用,与上一帧混合的alpha值,值越大尾迹越明显
|
||||
lastFrameAlpha?: number
|
||||
};
|
||||
|
||||
export default class Layer extends Eventful {
|
||||
|
||||
id: string
|
||||
|
||||
dom: HTMLCanvasElement
|
||||
domBack: HTMLCanvasElement
|
||||
|
||||
ctx: CanvasRenderingContext2D
|
||||
ctxBack: CanvasRenderingContext2D
|
||||
|
||||
painter: CanvasPainter
|
||||
|
||||
// Configs
|
||||
/**
|
||||
* 每次清空画布的颜色
|
||||
*/
|
||||
clearColor: string | GradientObject | ImagePatternObject
|
||||
/**
|
||||
* 是否开启动态模糊
|
||||
*/
|
||||
motionBlur = false
|
||||
/**
|
||||
* 在开启动态模糊的时候使用,与上一帧混合的alpha值,值越大尾迹越明显
|
||||
*/
|
||||
lastFrameAlpha = 0.7
|
||||
/**
|
||||
* Layer dpr
|
||||
*/
|
||||
dpr = 1
|
||||
|
||||
/**
|
||||
* Virtual layer will not be inserted into dom.
|
||||
*/
|
||||
virtual = false
|
||||
|
||||
config = {}
|
||||
|
||||
incremental = false
|
||||
|
||||
zlevel = 0
|
||||
|
||||
maxRepaintRectCount = 5
|
||||
|
||||
private _paintRects: BoundingRect[]
|
||||
|
||||
__dirty = true
|
||||
__firstTimePaint = true
|
||||
|
||||
__used = false
|
||||
|
||||
__drawIndex = 0
|
||||
__startIndex = 0
|
||||
__endIndex = 0
|
||||
|
||||
// indices in the previous frame
|
||||
__prevStartIndex: number = null
|
||||
__prevEndIndex: number = null
|
||||
|
||||
__builtin__: boolean
|
||||
|
||||
constructor(id: string | HTMLCanvasElement, painter: CanvasPainter, dpr?: number) {
|
||||
super();
|
||||
|
||||
let dom;
|
||||
dpr = dpr || devicePixelRatio;
|
||||
if (typeof id === 'string') {
|
||||
dom = createDom(id, painter, dpr);
|
||||
}
|
||||
// Not using isDom because in node it will return false
|
||||
else if (util.isObject(id)) {
|
||||
dom = id;
|
||||
id = dom.id;
|
||||
}
|
||||
this.id = id as string;
|
||||
this.dom = dom;
|
||||
|
||||
const domStyle = dom.style;
|
||||
if (domStyle) { // Not in node
|
||||
util.disableUserSelect(dom);
|
||||
dom.onselectstart = () => false;
|
||||
domStyle.padding = '0';
|
||||
domStyle.margin = '0';
|
||||
domStyle.borderWidth = '0';
|
||||
}
|
||||
|
||||
this.painter = painter;
|
||||
|
||||
this.dpr = dpr;
|
||||
}
|
||||
|
||||
getElementCount() {
|
||||
return this.__endIndex - this.__startIndex;
|
||||
}
|
||||
|
||||
afterBrush() {
|
||||
this.__prevStartIndex = this.__startIndex;
|
||||
this.__prevEndIndex = this.__endIndex;
|
||||
}
|
||||
|
||||
initContext() {
|
||||
this.ctx = this.dom.getContext('2d');
|
||||
(this.ctx as ZRCanvasRenderingContext).dpr = this.dpr;
|
||||
}
|
||||
|
||||
setUnpainted() {
|
||||
this.__firstTimePaint = true;
|
||||
}
|
||||
|
||||
createBackBuffer() {
|
||||
const dpr = this.dpr;
|
||||
|
||||
this.domBack = createDom('back-' + this.id, this.painter, dpr);
|
||||
this.ctxBack = this.domBack.getContext('2d');
|
||||
|
||||
if (dpr !== 1) {
|
||||
this.ctxBack.scale(dpr, dpr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create repaint list when using dirty rect rendering.
|
||||
*
|
||||
* @param displayList current rendering list
|
||||
* @param prevList last frame rendering list
|
||||
* @return repaint rects. null for the first frame, [] for no element dirty
|
||||
*/
|
||||
createRepaintRects(
|
||||
displayList: Displayable[],
|
||||
prevList: Displayable[],
|
||||
viewWidth: number,
|
||||
viewHeight: number
|
||||
) {
|
||||
if (this.__firstTimePaint) {
|
||||
this.__firstTimePaint = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const mergedRepaintRects: BoundingRect[] = [];
|
||||
const maxRepaintRectCount = this.maxRepaintRectCount;
|
||||
let full = false;
|
||||
const pendingRect = new BoundingRect(0, 0, 0, 0);
|
||||
|
||||
function addRectToMergePool(rect: BoundingRect) {
|
||||
if (!rect.isFinite() || rect.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mergedRepaintRects.length === 0) {
|
||||
// First rect, create new merged rect
|
||||
const boundingRect = new BoundingRect(0, 0, 0, 0);
|
||||
boundingRect.copy(rect);
|
||||
mergedRepaintRects.push(boundingRect);
|
||||
}
|
||||
else {
|
||||
let isMerged = false;
|
||||
let minDeltaArea = Infinity;
|
||||
let bestRectToMergeIdx = 0;
|
||||
for (let i = 0; i < mergedRepaintRects.length; ++i) {
|
||||
const mergedRect = mergedRepaintRects[i];
|
||||
|
||||
// Merge if has intersection
|
||||
if (mergedRect.intersect(rect)) {
|
||||
const pendingRect = new BoundingRect(0, 0, 0, 0);
|
||||
pendingRect.copy(mergedRect);
|
||||
pendingRect.union(rect);
|
||||
mergedRepaintRects[i] = pendingRect;
|
||||
isMerged = true;
|
||||
break;
|
||||
}
|
||||
else if (full) {
|
||||
// Merged to exists rectangles if full
|
||||
pendingRect.copy(rect);
|
||||
pendingRect.union(mergedRect);
|
||||
const aArea = rect.width * rect.height;
|
||||
const bArea = mergedRect.width * mergedRect.height;
|
||||
const pendingArea = pendingRect.width * pendingRect.height;
|
||||
const deltaArea = pendingArea - aArea - bArea;
|
||||
if (deltaArea < minDeltaArea) {
|
||||
minDeltaArea = deltaArea;
|
||||
bestRectToMergeIdx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (full) {
|
||||
mergedRepaintRects[bestRectToMergeIdx].union(rect);
|
||||
isMerged = true;
|
||||
}
|
||||
|
||||
if (!isMerged) {
|
||||
// Create new merged rect if cannot merge with current
|
||||
const boundingRect = new BoundingRect(0, 0, 0, 0);
|
||||
boundingRect.copy(rect);
|
||||
mergedRepaintRects.push(boundingRect);
|
||||
}
|
||||
if (!full) {
|
||||
full = mergedRepaintRects.length >= maxRepaintRectCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop the paint list of this frame and get the dirty rects of elements
|
||||
* in this frame.
|
||||
*/
|
||||
for (let i = this.__startIndex; i < this.__endIndex; ++i) {
|
||||
const el = displayList[i];
|
||||
if (el) {
|
||||
/**
|
||||
* `shouldPaint` is true only when the element is not ignored or
|
||||
* invisible and all its ancestors are not ignored.
|
||||
* `shouldPaint` being true means it will be brushed this frame.
|
||||
*
|
||||
* `__isRendered` being true means the element is currently on
|
||||
* the canvas.
|
||||
*
|
||||
* `__dirty` being true means the element should be brushed this
|
||||
* frame.
|
||||
*
|
||||
* We only need to repaint the element's previous painting rect
|
||||
* if it's currently on the canvas and needs repaint this frame
|
||||
* or not painted this frame.
|
||||
*/
|
||||
const shouldPaint = el.shouldBePainted(viewWidth, viewHeight, true, true);
|
||||
const prevRect = el.__isRendered && ((el.__dirty & REDRAW_BIT) || !shouldPaint)
|
||||
? el.getPrevPaintRect()
|
||||
: null;
|
||||
if (prevRect) {
|
||||
addRectToMergePool(prevRect);
|
||||
}
|
||||
|
||||
/**
|
||||
* On the other hand, we only need to paint the current rect
|
||||
* if the element should be brushed this frame and either being
|
||||
* dirty or not rendered before.
|
||||
*/
|
||||
const curRect = shouldPaint && ((el.__dirty & REDRAW_BIT) || !el.__isRendered)
|
||||
? el.getPaintRect()
|
||||
: null;
|
||||
if (curRect) {
|
||||
addRectToMergePool(curRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The above loop calculates the dirty rects of elements that are in the
|
||||
* paint list this frame, which does not include those elements removed
|
||||
* in this frame. So we loop the `prevList` to get the removed elements.
|
||||
*/
|
||||
for (let i = this.__prevStartIndex; i < this.__prevEndIndex; ++i) {
|
||||
const el = prevList[i];
|
||||
/**
|
||||
* Consider the elements whose ancestors are invisible, they should
|
||||
* not be painted and their previous painting rects should be
|
||||
* cleared if they are rendered on the canvas (`__isRendered` being
|
||||
* true). `!shouldPaint` means the element is not brushed in this
|
||||
* frame.
|
||||
*
|
||||
* `!el.__zr` means it's removed from the storage.
|
||||
*
|
||||
* In conclusion, an element needs to repaint the previous painting
|
||||
* rect if and only if it's not painted this frame and was
|
||||
* previously painted on the canvas.
|
||||
*/
|
||||
const shouldPaint = el && el.shouldBePainted(viewWidth, viewHeight, true, true);
|
||||
if (el && (!shouldPaint || !el.__zr) && el.__isRendered) {
|
||||
// el was removed
|
||||
const prevRect = el.getPrevPaintRect();
|
||||
if (prevRect) {
|
||||
addRectToMergePool(prevRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge intersected rects in the result
|
||||
let hasIntersections;
|
||||
do {
|
||||
hasIntersections = false;
|
||||
for (let i = 0; i < mergedRepaintRects.length;) {
|
||||
if (mergedRepaintRects[i].isZero()) {
|
||||
mergedRepaintRects.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
for (let j = i + 1; j < mergedRepaintRects.length;) {
|
||||
if (mergedRepaintRects[i].intersect(mergedRepaintRects[j])) {
|
||||
hasIntersections = true;
|
||||
mergedRepaintRects[i].union(mergedRepaintRects[j]);
|
||||
mergedRepaintRects.splice(j, 1);
|
||||
}
|
||||
else {
|
||||
j++;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} while (hasIntersections);
|
||||
|
||||
this._paintRects = mergedRepaintRects;
|
||||
|
||||
return mergedRepaintRects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paint rects for debug usage.
|
||||
*/
|
||||
debugGetPaintRects() {
|
||||
return (this._paintRects || []).slice();
|
||||
}
|
||||
|
||||
resize(width: number, height: number) {
|
||||
const dpr = this.dpr;
|
||||
|
||||
const dom = this.dom;
|
||||
const domStyle = dom.style;
|
||||
const domBack = this.domBack;
|
||||
|
||||
if (domStyle) {
|
||||
domStyle.width = width + 'px';
|
||||
domStyle.height = height + 'px';
|
||||
}
|
||||
|
||||
dom.width = width * dpr;
|
||||
dom.height = height * dpr;
|
||||
|
||||
if (domBack) {
|
||||
domBack.width = width * dpr;
|
||||
domBack.height = height * dpr;
|
||||
|
||||
if (dpr !== 1) {
|
||||
this.ctxBack.scale(dpr, dpr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空该层画布
|
||||
*/
|
||||
clear(
|
||||
clearAll?: boolean,
|
||||
clearColor?: string | GradientObject | ImagePatternObject,
|
||||
repaintRects?: BoundingRect[]
|
||||
) {
|
||||
const dom = this.dom;
|
||||
const ctx = this.ctx;
|
||||
const width = dom.width;
|
||||
const height = dom.height;
|
||||
|
||||
clearColor = clearColor || this.clearColor;
|
||||
const haveMotionBLur = this.motionBlur && !clearAll;
|
||||
const lastFrameAlpha = this.lastFrameAlpha;
|
||||
|
||||
const dpr = this.dpr;
|
||||
const self = this;
|
||||
|
||||
if (haveMotionBLur) {
|
||||
if (!this.domBack) {
|
||||
this.createBackBuffer();
|
||||
}
|
||||
|
||||
this.ctxBack.globalCompositeOperation = 'copy';
|
||||
this.ctxBack.drawImage(
|
||||
dom, 0, 0,
|
||||
width / dpr,
|
||||
height / dpr
|
||||
);
|
||||
}
|
||||
|
||||
const domBack = this.domBack;
|
||||
|
||||
function doClear(x: number, y: number, width: number, height: number) {
|
||||
ctx.clearRect(x, y, width, height);
|
||||
if (clearColor && clearColor !== 'transparent') {
|
||||
let clearColorGradientOrPattern;
|
||||
// Gradient
|
||||
if (util.isGradientObject(clearColor)) {
|
||||
// shouldn't cache when clearColor is not global and size changed
|
||||
const shouldCache = clearColor.global || (
|
||||
(clearColor as InnerGradientObject).__width === width
|
||||
&& (clearColor as InnerGradientObject).__height === height
|
||||
);
|
||||
// Cache canvas gradient
|
||||
clearColorGradientOrPattern = shouldCache
|
||||
&& (clearColor as InnerGradientObject).__canvasGradient
|
||||
|| getCanvasGradient(ctx, clearColor, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: width,
|
||||
height: height
|
||||
});
|
||||
|
||||
(clearColor as InnerGradientObject).__canvasGradient = clearColorGradientOrPattern;
|
||||
(clearColor as InnerGradientObject).__width = width;
|
||||
(clearColor as InnerGradientObject).__height = height;
|
||||
}
|
||||
// Pattern
|
||||
else if (util.isImagePatternObject(clearColor)) {
|
||||
// scale pattern by dpr
|
||||
clearColor.scaleX = clearColor.scaleX || dpr;
|
||||
clearColor.scaleY = clearColor.scaleY || dpr;
|
||||
clearColorGradientOrPattern = createCanvasPattern(
|
||||
ctx, clearColor, {
|
||||
dirty() {
|
||||
self.setUnpainted();
|
||||
self.painter.refresh();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
ctx.save();
|
||||
ctx.fillStyle = clearColorGradientOrPattern || (clearColor as string);
|
||||
ctx.fillRect(x, y, width, height);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
if (haveMotionBLur) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = lastFrameAlpha;
|
||||
ctx.drawImage(domBack, x, y, width, height);
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
if (!repaintRects || haveMotionBLur) {
|
||||
// Clear the full canvas
|
||||
doClear(0, 0, width, height);
|
||||
}
|
||||
else if (repaintRects.length) {
|
||||
// Clear the repaint areas
|
||||
util.each(repaintRects, rect => {
|
||||
doClear(
|
||||
rect.x * dpr,
|
||||
rect.y * dpr,
|
||||
rect.width * dpr,
|
||||
rect.height * dpr
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Interface of refresh
|
||||
refresh: (clearColor?: string | GradientObject | ImagePatternObject) => void
|
||||
|
||||
// Interface of renderToCanvas in getRenderedCanvas
|
||||
renderToCanvas: (ctx: CanvasRenderingContext2D) => void
|
||||
|
||||
// Events
|
||||
onclick: ElementEventCallback<unknown, this>
|
||||
ondblclick: ElementEventCallback<unknown, this>
|
||||
onmouseover: ElementEventCallback<unknown, this>
|
||||
onmouseout: ElementEventCallback<unknown, this>
|
||||
onmousemove: ElementEventCallback<unknown, this>
|
||||
onmousewheel: ElementEventCallback<unknown, this>
|
||||
onmousedown: ElementEventCallback<unknown, this>
|
||||
onmouseup: ElementEventCallback<unknown, this>
|
||||
oncontextmenu: ElementEventCallback<unknown, this>
|
||||
|
||||
ondrag: ElementEventCallback<unknown, this>
|
||||
ondragstart: ElementEventCallback<unknown, this>
|
||||
ondragend: ElementEventCallback<unknown, this>
|
||||
ondragenter: ElementEventCallback<unknown, this>
|
||||
ondragleave: ElementEventCallback<unknown, this>
|
||||
ondragover: ElementEventCallback<unknown, this>
|
||||
ondrop: ElementEventCallback<unknown, this>
|
||||
}
|
||||
Reference in New Issue
Block a user