项目中有个需求,将对象一天内对应的不同的状态在时间轴上显示出来。调研的方案有5种,
1、时间轴用div画,时间轴上遮罩的状态改变则改变时间轴div本身的颜色。
2、时间轴用div画,时间轴上的遮罩用div画,状态改变则改变遮罩div的颜色,时间轴div只做展示不做样式更改。
3、时间轴用静态的图片展示,时间轴上的遮罩用div画,状态改变则改变遮罩div的颜色。
4、时间轴用canvas画,时间轴上的遮罩用canvas画,每次状态改变重绘canvas的时间轴和遮罩层。
5、时间轴用canvas画,时间轴用另外一个canvas画,每次状态改变都修改遮罩层的canvas。

调研的5种方案中,1 2 3都会面临频繁的回流和重绘,所以最后采用4 5,但是最后采用的时方案4, 5应该是最优方案,因为4是一个canvas。状态改变时重绘时间轴是浪费的,但是项目时间紧张,没有按照方案5处理。后续会继续优化,下面我将方案四的绘制思路整理出来。

1、画canvas标签,canvas标签悬浮到状态时要展示对应的时间,时间样式用div展示,内容都在下面

 <template>
<div class="time-scale-container">
    <div v-show="visible" class="time-scale-tooltip">
        <div class="scale-tooltip-label">
            <div>开始时间</div>
            <div class="scale-tooltip-hour">{{ startTime }}</div>
        </div>
        <div class="scale-tooltip-arrow"></div>
        <div class="scale-tooltip-label">
            <div>结束时间</div>
            <div class="scale-tooltip-hour">{{ endTime }}</div>
        </div>
    </div>
    <canvas id="vehicleTimeScale" class="time-scale-canvas"></canvas>
</div>
 </template>

2、封装canvas工具类

     /**
 * @file 绘制 Canvas 工具函数
 */

import {InitCanvas, DrawText2d, DrawLine2d, DrawFillRect2d, ClearRect} from './types';

const canvasScale = window.devicePixelRatio;

// 初始化canvas
export const initCanvas: InitCanvas = (canvasId, options) => {
    const canvas: HTMLCanvasElement | null = document.querySelector(`#${canvasId}`);
    if (!canvas) {
        throw new Error(`canvas容器不存在`);
    }

// 获取canvas画布的宽高,css中已经设置canvas的宽高,则以css为准,如果没设置默认值为 300 * 150
const clientWidth = canvas.clientWidth;
const clientHeight = canvas.clientHeight;

// canvas 宽高 TODO 设置canvas宽高后,canvas不能随浏览器缩放而缩放,暂时注释掉
// canvas.style.width = canvas.clientWidth + 'px';
// canvas.style.height = canvas.clientHeight + 'px';

// <canvas>可能在视网膜屏幕上显得太模糊。
// 使用window.devicePixelRatio确定应添加多少额外的像素密度以使图像更清晰
canvas.width = clientWidth * canvasScale;
canvas.height = clientHeight * canvasScale;

const ctx = canvas.getContext(options.contextType);

return {canvas, ctx};
 };

export const drawText2d: DrawText2d = (ctx, content, offsetX, offsetY, options) => {
    // 设置文本填充颜色
    if (options && options.fillStyle) {
        ctx.fillStyle = options.fillStyle;
    }

// 设置文本字号 注:时间和字体要一起设置才生效
if (options && options.fontSize) {
    ctx.font = `${options.fontSize}px PingFangSC-Regular, PingFang SC`;
}

// 设置文本字体
if (options && options.fontFamily) {
    ctx.font += options.fontFamily;
}

// 设置文本水平对齐方式
if (options && options.textAlign) {
    ctx.textAlign = options.textAlign;
}
// 设置文本基线对齐方式
if (options && options.textBaseline) {
    ctx.textBaseline = options.textBaseline;
}
// 设置文本显示方向
if (options && options.direction) {
    ctx.direction = options.direction;
}

// 文字填充
ctx.fillText(content, offsetX, offsetY);
};

export const drawLine2d: DrawLine2d = (ctx, moveToX, moveToY, lineToX, lineToY, options) => {
    // 画线
    ctx.beginPath();

// 设置线条描边颜色
if (options && options.strokeStyle) {
    ctx.strokeStyle = options.strokeStyle;
}

// 设置线条转角的样式
if (options && options.lineJoin) {
    ctx.lineJoin = options.lineJoin;
}

// 设置线条宽度
if (options && options.lineWidth) {
    ctx.lineWidth = options.lineWidth;
}

// 当lineJoin类型是miter时候,miter效果生效的限制值
if (options && options.miterLimit) {
    ctx.miterLimit = options.miterLimit;
}
ctx.moveTo(moveToX, moveToY);
ctx.lineTo(lineToX, lineToY);
ctx.stroke();
};

export const drawFillRect2d: DrawFillRect2d = (ctx, startX, startY, width, height, options) => {
    if (options && options.fillStyle) {
        ctx.fillStyle = options.fillStyle;
    }

    ctx.fillRect(startX, startY, width, height);
};

export const clearRect: ClearRect = (ctx, startX, startY, width, height) => {
    ctx.clearRect(startX, startY, width, height);
};

3、初始化canvas并定义鼠标移入移出事件

// 初始化canvas
    const option = {
        contextType: ContextType.D2
    };
    const {canvas, ctx} = initCanvas('vehicleTimeScale', option);
    this.canvas = canvas;
    this.ctx = ctx;

    // canvas时间轴加鼠标移动事件
    this.canvas!.addEventListener('mousemove', this.handler);
    // canvas鼠标移除事件
    this.canvas!.addEventListener('mouseout', () => {
        this.visible = false;
    });

4、计算24小时时间轴对应的位置及画线

 // 计算每个时间线对应X轴位置
getTimeLineXpoint(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
    // 计算每一个格子的宽度,canvas前后各空两个格,基础参数已经多加一个,所以在加三个
    const perStep = canvas.width / (TIME_INTERVA_NUMBER + 3);

    for (let i = 1; i <= TIME_INTERVA_NUMBER; i++) {
        this.scaleXpointArr.push((i + 1) * perStep);
    }
}

// 画时间轴线 和 时间标识
drawTimeLineAText(height: number) {
    for (let i = 0; i < this.scaleXpointArr.length; i++) {
        if (i % BASE_SCALE_NUM === 0) {
            //整数刻度
            drawLine2d(this.ctx!, this.scaleXpointArr[i], height * 0.1, this.scaleXpointArr[i], height * 0.35, {
                strokeStyle: LINE_OPTION.strokeStyle
            });

            // 时间标识
            let textContent = i / BASE_SCALE_NUM + '';
            // 文字测量。获得是字符占据的宽度
            const textWidth = this.ctx!.measureText(textContent).width;
            drawText2d(this.ctx!, textContent, this.scaleXpointArr[i] - textWidth, height * 0.8, {
                fontSize: TEXT_OPTION.fontSize,
                fillStyle: TEXT_OPTION.fillStyle
            });

            const textContent1 = ':00';
            drawText2d(this.ctx!, textContent1, this.scaleXpointArr[i], height * 0.8, {
                fontSize: TEXT_OPTION.fontSize,
                fillStyle: TEXT_OPTION.fillStyle
            });
        } else {
            //半数刻度
            drawLine2d(this.ctx!, this.scaleXpointArr[i], height * 0.1, this.scaleXpointArr[i], height * 0.2, {
                strokeStyle: LINE_OPTION.strokeStyle
            });
        }
    }
}

5、根据状态开始时间结束时间,画状态遮罩层

 / 计算时间轴 0:00对应的位置
getTimeAndPosition() {
    const timer = this.getCurrentZeroTime();
    this.scaleTimePosition = {time: timer, positionX: this.scaleXpointArr[0]};
}

// 计算状态时间与位置的对应关系
getVehiclePosition(vehicleOrderStatus: VehicleOrderStatus[]) {
    vehicleOrderStatus.forEach((item: VehicleOrderStatus) => {
        const {startX, endX} = this.getXposition(item);
        const startTime = formatTimeHelper(item.startTime, 'HH:mm:ss');
        const endTime = formatTimeHelper(item.endTime, 'HH:mm:ss');

        const height = this.canvas!.height;
        const newCanvas = {
            x: startX,
            y: 0,
            width: endX - startX,
            height: height,
            startTime,
            endTime,
            status: item.vehicleStatus
        };
            this.vehicleStatusCanvas.push(newCanvas);
    });
}

// 计算开始时间结束时间对应的x轴位置
getXposition(vehicleOrderStatus: VehicleOrderStatus) {
    if (!this.scaleTimePosition) return {startX: 0, endX: 0};
    const startTimeDiff = vehicleOrderStatus.startTime - this.scaleTimePosition.time;
    const endTimeDiff = vehicleOrderStatus.endTime - this.scaleTimePosition.time;

    // 开始时间和结束时间如果为0点,则开始位置为0点对应的位置
    const startX = startTimeDiff
        ? (startTimeDiff / 1000) * this.perSecondStep + this.scaleTimePosition.positionX
        : this.scaleTimePosition.positionX;
    const endX = endTimeDiff
        ? (endTimeDiff / 1000) * this.perSecondStep + this.scaleTimePosition.positionX
        : this.scaleTimePosition.positionX;

    return {startX, endX};
}

// 绘制状态图层
drawVehicleOrderStatus() {
    for (let i = 0; i < this.vehicleStatusCanvas.length; i++) {
        drawFillRect2d(
            this.ctx!,
            this.vehicleStatusCanvas[i].x,
            this.vehicleStatusCanvas[i].y,
            this.vehicleStatusCanvas[i].width,
            this.vehicleStatusCanvas[i].height,
            {fillStyle: ORDER_TYPE_HASH[this.vehicleStatusCanvas[i].status].color}
        );
    }
}

6、canvas鼠标悬浮显示对应的悬浮框

  handler(event: MouseEvent) {
    const selectX = event.clientX;
    const hoverCanvas = this.getIsExit(selectX);
    // canvas存在且车辆接单状态不为-1
    if (hoverCanvas && hoverCanvas.status !== -1) {
        this.startTime = hoverCanvas.startTime;
        this.endTime = hoverCanvas.endTime;
        this.visible = true;
        const ele: HTMLElement | null = document.querySelector('.time-scale-tooltip');
        ele!.style.left = selectX + 'px';
    } else {
        this.visible = false;
    }
}

// 计算鼠标位置是否在接单状态中
getIsExit(x: number): VehicleStatusCanvas | null {
    x = x * window.devicePixelRatio; // 注意:将css像素转为物理像素
    if (Array.isArray(this.vehicleStatusCanvas) && this.vehicleStatusCanvas.length > 0) {
        const hoverCanvas = this.vehicleStatusCanvas.filter((obj) => {
            return obj.x <= x && x <= obj.x + obj.width;
        });
        if (hoverCanvas && hoverCanvas.length > 0) {
            return hoverCanvas[0];
        }
    }
    return null;
}

上述为止 canvas时间轴及状态的展示就画完了,鼠标悬浮展示对应的tooltip时间标识。

Logo

前往低代码交流专区

更多推荐