J'Blog

开发记录3

开发记录3

项目记录

项目1(目标库和源库数据比对,校验数据一致性、空值、值域等)

// 技术栈:vue2 + vuex + event-bus + vue-router + axios + element-ui

// vue mixin 类似于hooks

// ********** 重点改造功能:路由 keep-alive 多级路由缓存,修改根据组件名称获取更改为路由名称进行缓存,同一个页面不同路由参数也会进行缓存
// <keep-alive>组件包含的页面都会进行缓存

// codemirror web代码编辑器
// sql-formatter sql语句格式化工具
// mavon-editor 基于vue的markdown编辑器

// 自定义指令:
import Vue from 'vue';
let MyPlugin = {};
export default MyPlugin.install = function (vue, options) {
    Vue.directive('lazy', {
        bind: function (el, binding) {
            const lazyLoadObser = new IntersectionObserver((entries, observer) => {
                entries.forEach((entry, index) => {
                    if (entry.intersectionRatio > 0) {
                        binding.value.loadingMore();
                    }
                })
            })
            lazyLoadObser.observe(el)
        }
    })
};

// 下载文件
export const download = (res) => {
    const content = res.data
    const blob = new Blob([content])
    // const fileName = '导出信息.xlsx'
    const fileName = decodeURIComponent(res.headers['content-disposition'].split('filename=')[1])
    if ('download' in document.createElement('a')) { // 非IE下载
        const link = document.createElement('a')//创建一个a标签通过a标签的点击事件区下载文件
        link.download = fileName
        link.style.display = 'none'
        link.href = URL.createObjectURL(blob)//使用blob创建一个指向类型数组的URL
        document.body.appendChild(link)
        link.click()
        URL.revokeObjectURL(link.href) // 释放URL 对象
        document.body.removeChild(link)
    } else { // IE10+下载
        navigator.msSaveBlob(blob, fileName)
    }
}

/**
 * 返回昨天日期
 * @returns {string}
 */
export const getYesterday = () => {
    let now = new Date()
    let yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
    let _year = yesterday.getFullYear()
    let _month = yesterday.getMonth() + 1 < 10 ? '0' + (yesterday.getMonth() + 1) : (yesterday.getMonth() + 1)
    let _day = yesterday.getDate() < 10 ? '0' + yesterday.getDate() : yesterday.getDate()

    return _year + '-' + _month + '-' + _day
};

/**
 * 获取当前年份
 */
export const getCurrentYear = () => {
    let now = new Date()
    return now.getFullYear()
}

/**
 * 获取当前月份
 */
export const getCurrentMonth = (n = 0) => {
    let now = new Date()
    return now.getMonth() + 1 + n
}

/**
 * 浏览器判断是否全屏
 */
export const fullscreenToggel = () => {
    if (fullscreenEnable()) {
        exitFullScreen();
    } else {
        reqFullScreen();
    }
};

/**
 * esc监听全屏
 */
export const listenfullscreen = (callback) => {
    function listen() {
        callback();
    }
    document.addEventListener('fullscreenchange', function () {
        listen();
    });
    document.addEventListener('mozfullscreenchange', function () {
        listen();
    });
    document.addEventListener('webkitfullscreenchange', function () {
        listen();
    });
    document.addEventListener('msfullscreenchange', function () {
        listen();
    });
};

/**
 * 浏览器判断是否全屏
 */
export const fullscreenEnable = () => {
    return document.isFullScreen || document.mozIsFullScreen || document.webkitIsFullScreen;
};

/**
 * 浏览器全屏
 */
export const reqFullScreen = () => {
    if (document.documentElement.requestFullScreen) {
        document.documentElement.requestFullScreen();
    } else if (document.documentElement.webkitRequestFullScreen) {
        document.documentElement.webkitRequestFullScreen();
    } else if (document.documentElement.mozRequestFullScreen) {
        document.documentElement.mozRequestFullScreen();
    }
};

/**
 * 浏览器退出全屏
 */
export const exitFullScreen = () => {
    if (document.documentElement.requestFullScreen) {
        document.exitFullScreen();
    } else if (document.documentElement.webkitRequestFullScreen) {
        document.webkitCancelFullScreen();
    } else if (document.documentElement.mozRequestFullScreen) {
        document.mozCancelFullScreen();
    }
};

项目2

// 技术栈:vue3 + ts + vite2 + pinia + ant-design-vue + antv/x6(流程图)
// echarts 和 D3.js的区别就是echarts是使用canvas绘制,D3使用svg绘制;echarts是封装好的,灵活性差,D3比较灵活,支持事件处理器操作dom,但是学习成本高,看具体开发需求
// go.js也是绘制力导向图的一个js库

项目3

// 技术栈: react + typescript + vite + cbdesign + react-router-dom + mobx + pnpm

// 根据问题智能推荐不同图表数据进行展示并支持切换图标展示,可导出到pdf

/** 重点改造功能: */
/** 1. 手写eventBus */
/** event-emitter.ts */
import { pull } from 'lodash';

export type EventHandler<E = any> = (e: E) => void;

/*这个就是事件的原型*/
class EventEmitter<E = any> {
    private _events: Record<string, EventHandler<E>[]>;

    constructor() {
        this._events = {};
    }

    private _getFns(event: string) {
        return this._events[event] || (this._events[event] = []);
    }

    public on<T = E>(event: string, cb: EventHandler<T>) {
        const fns = this._getFns(event);
        fns.push(cb as any);
    }

    public off(event: string, cb?: EventHandler<E>) {
        if (cb) {
            const fns = this._getFns(event);
            pull(fns, cb);
        } else {
            delete this._events[event];
        }
    }

    public once<T = E>(event: string, cb: EventHandler<T>) {
        const fn2: EventHandler<E> = (e) => {
            this.off(event, fn2);
            cb(e as any);
        };
        this.on(event, fn2);
    }

    /* 同步调用 */
    public emit<T = E>(event: string, param?: T) {
        const fns = this._getFns(event);
        for (let i = 0; i < fns.length; i++) {
            const fn = fns[i] as EventHandler<any>;

            fn(param);
        }
    }

    /* 可以异步调用,返回一个Promise */
    public invoke<T = E>(event: string, param?: T): Promise<any> {
        const fns = this._getFns(event);

        flag: for (let i = 0; i < fns.length; i++) {
            const fn = fns[i] as EventHandler<any>;
            return new Promise < any > ((resolve) => {
                resolve(fn(param));
            });
            break flag;
        }
        return Promise.reject();
    }
}

export default EventEmitter;

/** events.ts */
import EventEmitter, { EventHandler } from './event-emitter'
import { useEffect } from 'react'

const events = new EventEmitter()

export default events

export function useEventBus<T = any>(event: string, cb: EventHandler<T>) {
    useEffect(() => {
        events.on(event, cb)
        return () => {
            events.off(event, cb)
        }
    })
}

/** 如何使用eventBus */
useEventBus('exportPdf', handleExportPdf)  // 监听exportPdf事件,如果触发了exportPdf事件则执行handleExportPdf方法

events.emit('exportPdf')  // 触发exportPdf事件

/** 2. jspdf插件集合html2canvas实现导出图片到pdf文件 */
export const exportPdf = (canvas: HTMLCanvasElement) => {
    const contentWidth = canvas.width
    const contentHeight = canvas.height
    // 一页pdf显示html页面生成的canvas高度;
    const pageHeight = (contentWidth / 592.28) * 841.89
    // 未生成pdf的html页面高度
    let leftHeight = contentHeight
    // 页面偏移
    let position = 0
    // a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
    const imgWidth = 595.28
    const imgHeight = (595.28 / contentWidth) * contentHeight
    const pageData = canvas.toDataURL('image/jpeg', 1.0)
    const pdf = new JsPDF('p', 'pt', 'a4')
    // 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
    // 当内容未超过pdf一页显示的范围,无需分页
    if (leftHeight < pageHeight) {
        pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
    } else {
        while (leftHeight > 0) {
            pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
            leftHeight -= pageHeight
            // 避免添加空白页
            position -= 841.89
            if (leftHeight > 0) {
                pdf.addPage()
            }
        }
    }
    const nowDate = moment(new Date()).format('YYYY-MM-DD')
    // if (isPrint) {
    //     //打印
    //     pdf.autoPrint();
    //     pdf.output('dataurlnewwindow');
    // } else {
    //     //下载
    pdf.save('test-' + nowDate + '.pdf')
    // }
}

// 整页导出pdf
export const imgDownloadPdf = (
    img: any,
    pdfName: any = moment(new Date()).format('YYYY-MM-DD'),
    x: any = 0,
    y: any = 30
) => {
    document.body.classList.add('hide_Pdf_body')
    // 获取需转图片的范围元素
    document.documentElement.scrollTop = 0
    document.body.scrollTop = 0
    // 利用 html2canvas 下载 canvas
    html2canvas(img).then(function (canvas) {
        //转化为本地的图片地址
        const filename = pdfName
        const pageWidth = canvas.width
        const pageHeight = canvas.height
        const pageData = canvas.toDataURL('image/jpeg', 1.0)
        const pdf = new jsPDF({
            orientation: pageHeight > pageWidth ? 'portrait' : 'landscape',
            unit: 'px',
            format: [pageHeight, pageWidth]
        })
        pdf.addImage(pageData, x || x == 0 ? x : 0, y || y == 0 ? y : 100, pageWidth, pageHeight - y)
        //转pdf下载
        const nowDate = moment(new Date()).format('YYYY-MM-DD')
        pdf.save(filename || '报告-' + nowDate)
    })
    setTimeout(() => {
        document.body.classList.remove('hide_Pdf_body')
    }, 500)
}

/** 3. 是否在可视范围内hooks */
import React from "react";

export function useObserver(ref: any) {

    const [isIntersecting, setIsIntersecting] = React.useState(false);

    // OPTIONS
    const options = {
        // root: target.current,
        rootMargin: '0px',
        threshold: 0.5, // A threshold of 1.0 means that when 100% of the target is visible within the element specified by the root option, the callback is invoked.
    };
    // Observer
    const observer = new IntersectionObserver(([entry]) => setIsIntersecting(entry.isIntersecting), options);

    // Use Effect
    React.useEffect(() => {

        observer.observe(ref.current)
        // DISCONNECT 
        return () => observer.disconnect()

    }, []);
    // Return
    return isIntersecting
};

/** 4. 是否改变了窗口大小hooks */

import { useEffect, useState } from 'react';

const useResizeObserver = (ref: any) => {
    const [dimensions, setDimensions] = useState < { width: number; height: number } | null > (null);

    useEffect(() => {
        const observer = new ResizeObserver((entries) => {
            const { width, height } = entries[0].contentRect;
            setDimensions({ width, height });
        });

        observer.observe(ref.current);

        return () => {
            if (ref.current) {
                observer.unobserve(ref.current);
            }
        };
    }, [ref]);

    return dimensions;
};

export default useResizeObserver;

/** 5. 模拟打字效果 */
const search: any = (data: any) => {
    const str = data?.title
    const strLen = str.length
    let index = 0
    let timer: any = null
    let cur = currentAnswer
    let curLoading = currentAnswerLoading

    timer = setInterval(() => {
        if (index >= strLen) {
            setCurrentAnswerLoading(false)
            clearInterval(timer)
        } else {
            if (!curLoading) curLoading = true
            const randomLen = Math.ceil(Math.random() * (strLen - index > 10 ? 10 : strLen - index))
            const curIndex = index
            index += randomLen
            cur += str?.slice(curIndex, index)
            setCurrentAnswer(cur)
            setCurrentAnswerLoading(curLoading)
        }
    }, Math.random() * 500)
}

// 给文本一个类,设置伪元素光标闪烁
// .typingLoading {
//     &::after {
//         content: ' ▌';
//         animation: changeBg 1s infinite;
//     }
// }

// @keyframes changeBg {
//     from {
//         opacity: 1;
//     }

//     to {
//         opacity: 0;
//     }
// }

项目4

// 重点改造功能:
// 1. electron-vite-react
// 1. B端&C端打开弹窗,B端通过调用方法,postMessage传输数据;C端electron约定协议打开桌面应用内嵌web页面,涉及到electron于内嵌web页面之间通信方式
// electron功能点:
// 1. 自定义标题栏, 首先配置隐藏默认标题栏,react自定义标题栏,设置css控制部分不可拖拽
// 2. electron和react通信,react页面通过ipcRenderer 的send或on方法
// 3. 注册自定义协议,实现浏览器或桌面应用点击打开我们的桌面应用,app.setAsDefaultProtocolClient("")
// 4. iframe和内部react页面之间的通信postMessage
// 5. 打开新窗口,涉及到打包后本地hash url,loadUrl, loadFile

// 2. 封装权限hooks
import { useMemo } from 'react'
import { isAuthorized } from '@/assets/js/publicFunc'

export const useAuth = (key: string) => {
    const auth = useMemo(() => {
        return isAuthorized(key)
    }, [key])
    return auth
}

// 使用forwardRef, useImperativeHandle调用子组件中的方法

/** 获取文件大小 */
export function formatSizeUnits(bytes) {
    if (bytes >= 1073741824) { bytes = (bytes / 1073741824).toFixed(2) + " GB"; }
    else if (bytes >= 1048576) { bytes = (bytes / 1048576).toFixed(2) + " MB"; }
    else if (bytes >= 1024) { bytes = (bytes / 1024).toFixed(2) + " KB"; }
    else if (bytes > 1) { bytes = bytes + " bytes"; }
    else if (bytes == 1) { bytes = bytes + " byte"; }
    else { bytes = "0 bytes"; }
    return bytes;
}

项目5

// 重点改造功能:
// 1. 递归数据处理,表格拆分合并单元格,表格单元格编辑等
// 2. 拆分文本组件封装
import { getSelectedText } from '@/utils/index'
import { PlusOutlined } from '@ant-design/icons'
import { useEffect, useRef, useState } from 'react'

interface Props {
    text: string;
    addSplitHandle: any
}

const SplitText = ({ text, addSplitHandle }: Props) => {
    const [showAddIcon, setShowAddIcon] = useState < any > (false)
    const [curSelectionText, setCurSelectionText] = useState < any > ('')
    const textRef = useRef < any > (null)

    const mouseUpHandle = (e: any) => {
        const selectionText = getSelectedText();
        if (selectionText) {
            setCurSelectionText(selectionText)
            if (!showAddIcon) setShowAddIcon(true)
            const addIcon = document.querySelector('.split-add-icon');
            addIcon?.setAttribute('style', `left: ${e.offsetX + 5}px;top: ${e.offsetY - 7}px`)
        } else {
            setShowAddIcon(false)
        }
    }

    const mouseDownHandle = () => {
        textRef.current?.addEventListener('mouseup', mouseUpHandle)
        if (showAddIcon) setShowAddIcon(false)
    }

    const clickHandle = () => {
        addSplitHandle(curSelectionText)
        setShowAddIcon(false)
    }

    useEffect(() => {
        textRef.current?.addEventListener('mousedown', mouseDownHandle)
    }, [])
    return (
        <p className='split-text' ref={textRef}>
            {text}
            <span className='split-add-icon' style={{ opacity: showAddIcon ? 1 : 0 }} onClick={clickHandle}><PlusOutlined key='addIcon' /></span>
        </p>
    )
}

export default SplitText

/**
 * 获取选中文本
 */
export const getSelectedText = () => {
    let selectedText = ''
    if (window.getSelection) {
        selectedText = (window.getSelection() as any).toString()
    } else if ((document as any).selection && (document as any).selection.type !== 'Control') {
        selectedText = (document as any).selection.createRange().text
    }
    return selectedText
}