您的位置:首頁 >綜合 > 股票 >

如何實(shí)現(xiàn)巡檢報(bào)告? 今日熱聞

什么是巡檢報(bào)告

巡檢報(bào)告是指對某一個系統(tǒng)或設(shè)備進(jìn)行全面檢查,并把檢查結(jié)果及建議整理成報(bào)告的過程。

巡檢報(bào)告通常用于評估系統(tǒng)或設(shè)備的運(yùn)行狀況與性能,以發(fā)現(xiàn)問題、優(yōu)化系統(tǒng)、提高效率、降低故障率等方面提供參考。


(資料圖片僅供參考)

要實(shí)現(xiàn)什么功能自定義布局現(xiàn)報(bào)告中的面板可進(jìn)行拖拽改變布局。在拖拽的過程中限制拖拽區(qū)域,只允許在同一父級內(nèi)進(jìn)行拖拽,不允許跨目錄移動,不允許改變目錄的級別,比如把一級目錄移動到另一個一級目錄內(nèi),變成二級目錄目錄可收縮展開目錄支持收縮展開,收縮時隱藏所以子面板,展開時顯示所以子面板移動目錄時,子面板跟隨移動改變目錄后,同步更新右側(cè)的目錄面板生成目錄編號右側(cè)目錄樹生成目錄編號支持錨點(diǎn)滾動支持展開收縮與左側(cè)報(bào)告聯(lián)動數(shù)據(jù)面板根據(jù)日期范圍獲取指標(biāo)數(shù)據(jù)通過圖表的形式展示指標(biāo)信息查看詳情,刪除各面板的請求設(shè)計(jì),支持刷新請求面板導(dǎo)入統(tǒng)計(jì)目錄下選擇的面板數(shù)量導(dǎo)入新面板時,不能破壞已有布局,新面板只能跟在舊面板后導(dǎo)入已有面板時,需要進(jìn)行數(shù)據(jù)比較,有數(shù)據(jù)變更需要重新獲取最新的數(shù)據(jù)保存

在保存前,所有影響布局相關(guān)的操作,都是臨時的,包括導(dǎo)入面板。只有在點(diǎn)擊保存后,才會把當(dāng)前數(shù)據(jù)提交給后端進(jìn)行保存。

支持 pdf 和 word 導(dǎo)出巡檢報(bào)告實(shí)現(xiàn)方案數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)

先看看使用扁平結(jié)構(gòu)下的

在扁平結(jié)構(gòu)下,確定子項(xiàng)只需要找到下一個 row 面板,對于多級目錄下也是同理,只是對一級目錄需要額外處理。這種結(jié)構(gòu)上實(shí)現(xiàn)簡單,但是需求要求我們限制目錄的拖拽,限制目錄需要一個比較清晰的面板層級關(guān)系,很顯然,用樹能夠很清晰的描述一個數(shù)據(jù)的層級結(jié)構(gòu)

組件設(shè)計(jì)

與傳統(tǒng)組件編程有所區(qū)別。

在實(shí)現(xiàn)上對渲染和數(shù)據(jù)處理進(jìn)行了分離,分為兩塊:

react 組件:主要負(fù)責(zé)頁面渲染class : 負(fù)責(zé)數(shù)據(jù)的處理

DashboardModel

class DashboardModel {    id: string | number;    panels: PanelModel[]; // 各個面板// ...}

PanelModel

class PanelModel {    key?: string;    id!: number;    gridPos!: GridPos; // 位置信息    title?: string;    type: string;    panels: PanelModel[]; // 目錄面板需要維護(hù)當(dāng)前目錄下的面板信息    // ...}

每一個 Dashboard 組件對應(yīng)一個 DashboardModel,每一個 Panel 組件對應(yīng)一個 PanelModel。

react 組建根據(jù)類實(shí)例中的數(shù)據(jù)進(jìn)行渲染。

實(shí)例生產(chǎn)后,不會輕易的銷毀,或者改變引用地址,這讓依賴實(shí)例數(shù)據(jù)進(jìn)行渲染的 React 組件無法觸發(fā)更新渲染。

需要一個方式,在實(shí)例內(nèi)數(shù)據(jù)發(fā)生改變后,由我們手動觸發(fā)組件的更新渲染。

組件渲染控制

由于我們采用的是 hooks組件,不像 class組件有 forceUpdate方法觸發(fā)組件的方法。

而在 react18中有一個新特性 useSyncExternalStore,可以讓我們訂閱外部的數(shù)據(jù),如果數(shù)據(jù)發(fā)生改變了,會觸發(fā)組件的渲染。

實(shí)際上 useSyncExternalStore觸發(fā)組件渲染的原理就是在內(nèi)部維護(hù)了一個 state,當(dāng)更改了 state值,引起了外部組件的渲染。

基于這個思路簡單的實(shí)現(xiàn)了一個能夠觸發(fā)組件渲染的 useForceUpdate方法。

export function useForceUpdate() {    const [_, setValue] = useState(0);    return debounce(() => setValue((prevState) => prevState + 1), 0);}

雖說實(shí)現(xiàn)了 useForceUpdate,但是在實(shí)際使用的過程中,還需要在組件銷毀時移除事件。

useSyncExternalStore已經(jīng)內(nèi)部已經(jīng)實(shí)現(xiàn)了,直接使用即可。

useSyncExternalStore(dashboard?.subscribe ?? (() => {}), dashboard?.getSnapshot ?? (() => 0));useSyncExternalStore(panel?.subscribe ?? (() => {}), panel?.getSnapshot ?? (() => 0));

根據(jù)useSyncExternalStore使用,分別添加了 subscribe 和 getSnapshot 方法。

class DashboardModel {// PanelModel 一樣     count = 0;    forceUpdate() {    this.count += 1;    eventEmitter.emit(this.key);    }    /**     * useSyncExternalStore 的第一個入?yún)?,?zhí)行 listener 可以觸發(fā)組件的重渲染     * @param listener     * @returns     */    subscribe = (listener: () => void) => {        eventEmitter.on(this.key, listener);        return () => {            eventEmitter.off(this.key, listener);        };    };    /**     * useSyncExternalStore 的第二個入?yún)?,count 在這里改變后觸發(fā)diff的通過。     * @param listener     * @returns     */    getSnapshot = () => {        return this.count;    };}

當(dāng)改變數(shù)據(jù)后,需要觸發(fā)組件的渲染,只需要執(zhí)行forceUpdate即可。

面板拖拽

市面上比較大眾的拖拽插件有以下幾個:

react-beautiful-dndreact-dndreact-grid-layout

經(jīng)過比較后,發(fā)現(xiàn) react-grid-layout非常適合用來做面板的拖拽功能,react-grid-layout本身使用簡單,基本無上手門檻,最終決定使用 react-grid-layout詳細(xì)說明可以查看以下鏈接:

react-grid-layout

在面板布局改變后觸發(fā)react-grid-layoutonLayoutChange方法,可以拿到布局后的所有面板最新的位置數(shù)據(jù)。

const onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => {    for (const newPos of newLayout) {        panelMap[newPos.i!].updateGridPos(newPos);    }    dashboard!.sortPanelsByGridPos();};

panelMap 是一個 map,key 為 Panel.key, value 為面板。是在我們組件渲染時就已經(jīng)準(zhǔn)備好了。

const panelMap: Record = {};

可以通過 panelMap 找到對應(yīng)的面板,執(zhí)行面板的 updateGridPos方法進(jìn)行更新面板的布局?jǐn)?shù)據(jù)。到這,我們只是完成了面板本身數(shù)據(jù)更新,還需要執(zhí)行儀表盤的 sortPanelsByGridPos方法,對所有的面板進(jìn)行排序。

class DashboardModel {    sortPanelsByGridPos() {        this.panels.sort((panelA, panelB) => {            if (panelA.gridPos.y === panelB.gridPos.y) {                return panelA.gridPos.x - panelB.gridPos.x;            } else {                return panelA.gridPos.y - panelB.gridPos.y;            }        });    }    // ...}
面板拖動范圍

目前的拖動范圍是整個儀表盤,可隨意拖動,如下:

綠色是儀表盤可拖拽區(qū)域,灰色為面板。

如果需要限制就需要改成如下的結(jié)構(gòu):

在原本的基礎(chǔ)上,以目錄為單位區(qū)分,綠色為整體的可移動區(qū)域,黃色為一級目錄塊,可在綠色區(qū)域拖動,拖動時以整個黃色塊進(jìn)行拖動,紫色為二級目錄塊,可在當(dāng)前黃色區(qū)域內(nèi)拖動,不可脫離當(dāng)前黃色塊,灰色的面板只能在當(dāng)前目錄下拖動。

在原先數(shù)據(jù)結(jié)構(gòu)基礎(chǔ)上進(jìn)行改造:

class PanelModel {    dashboard?: DashboardModel; // 當(dāng)前目錄下的 dashboard    // ...}
目錄目錄收縮展開

為目錄面板維護(hù)一個 collapsed屬性用來控制面板的隱藏顯示

class PanelModel {    collapsed?: boolean; // type = row    // ...}// 組件渲染{!collapsed && }

對目錄進(jìn)行收縮展開時,會改變自身的高度,現(xiàn)在還需要把這個改變的高度同步給上一級的儀表盤。上一級需要做的就是類似我們控制目錄的處理。如下,控制第一個二級目錄收縮:

當(dāng)面板發(fā)生變更時,需要通知上級面板,進(jìn)行對應(yīng)的操作。

增加一個 top 用來獲取到父級實(shí)例。

class DashboardModel {    top?: null | PanelModel; // 最近的 panel 面板    /**     * 面板高度變更,同步修改其他面板進(jìn)行對應(yīng)高度 Y 軸的變更     * @param row 變更高度的 row 面板     * @param h 變更高度     */    togglePanelHeight(row: PanelModel, h: number) {        const rowIndex = this.getIndexById(row.id);        for (let panelIndex = rowIndex + 1; panelIndex < this.panels.length; panelIndex++) {            this.panels[panelIndex].gridPos.y += h;        }        this.panels = [...this.panels];        // 頂級 dashBoard 容器沒有 top        this.top?.changeHeight(h);        this.forceUpdate();    }    // ...}class PanelModel {    top: DashboardModel; // 最近的 dashboard 面板    /**     * @returns h 展開收起影響的高度     */    toggleRow() {        this.collapsed = !this.collapsed;        let h = this.dashboard?.getHeight();        h = this.collapsed ? -h : h;        this.changeHeight(h);    }    /**     *     * @param h 變更的高度     */    changeHeight(h: number) {        this.updateGridPos({ ...this.gridPos, h: this.gridPos.h + h }); // 更改自身面板的高度        this.top.togglePanelHeight(this, h); // 觸發(fā)父級變更        this.forceUpdate();    }    // ...}

整理流程與冒泡類型,一直到最頂級的 Dashboard。

展開收縮同理。

面板的刪除

對于面板的刪除,我們只需要在對應(yīng)的 Dashboard 下進(jìn)行移除,刪除后會改變當(dāng)前 Dashboard 高度,這塊的處理與上面的目錄收縮一致。

class DashboardModel {    /**     * @param panel 刪除的面板     */    removePanel(panel: PanelModel) {        this.panels = this.filterPanelsByPanels([panel]);        // 冒泡父容器,減少的高度        const h = -panel.gridPos.h;        this.top?.changeHeight(h);        this.forceUpdate();    }    /**     * 根據(jù)傳入的面板進(jìn)行過濾     * @param panels 需要過濾的面板數(shù)組     * @returns 過濾后的面板     */    filterPanelsByPanels(panels: PanelModel[]) {        return this.panels.filter((panel) => !panels.includes(panel));    }    // ...}
面板的保存

PS:與后端溝通后,當(dāng)前巡檢報(bào)告數(shù)據(jù)結(jié)構(gòu)由前端自主維護(hù),最終給后端一個字符串就好。獲取到目前的面板數(shù)據(jù),用 JSON 進(jìn)行轉(zhuǎn)換即可。

面板的信息獲取過程,先從根節(jié)點(diǎn)出發(fā),遍歷至葉子結(jié)點(diǎn),再從葉子結(jié)點(diǎn)開始,一層層向上進(jìn)行返回,也就是回溯的過程。

class DashboardModel {    /**     * 獲取所有面板數(shù)據(jù)     * @returns     */    getSaveModel() {        const panels: PanelData[] = this.panels.map((panel) => panel.getSaveModel());        return panels;    }    // ...}// 最終保存時所需要的屬性,其他的都不需要const persistedProperties: { [str: string]: boolean } = {    id: true,    title: true,    type: true,    gridPos: true,    collapsed: true,    target: true,};class PanelModel {    /**     * 獲取所有面板數(shù)據(jù)     * @returns     */    getSaveModel() {        const model: any = {};        for (const property in this) {            if (persistedProperties[property] && this.hasOwnProperty(property)) {                model[property] = cloneDeep(this[property]);            }        }        model.panels = this.dashboard?.getSaveModel() ?? [];        return model;    }    // ...}
面板面板的導(dǎo)入設(shè)計(jì)

后端返回的數(shù)據(jù)是一顆有著三級層級的樹,我們拿到后,在數(shù)據(jù)上維護(hù)成 moduleMapdashboardMappanelMap3個Map。

import { createContext } from "react";export interface Module { // 一級目錄    key: string;    label: string;    dashboards?: string[];    sub_module?: Dashboard[];}export interface Dashboard { // 二級目錄    key: string;    dashboard_key: string;    label: string;    panels?: number[];    selectPanels?: number[];    metrics?: Panel[];}export interface Panel {    expr: Expr[]; // 數(shù)據(jù)源語句信息    label: string;    panel_id: number;}type Expr = {    expr: string;    legendFormat: string;};export const DashboardContext = createContext({    moduleMap: new Map(),    dashboardMap: new Map(),    panelMap: new Map(),});

我們在渲染模塊時,遍歷 moduleMap,并通過 Module內(nèi)的dashboards信息找到二級目錄。

在交互上設(shè)置一級目錄不可選中,當(dāng)選中二級目錄時,通過二級目錄 Dashboardpanels找到相關(guān)的面板渲染到右側(cè)區(qū)域。對于這3個Map的操作,維護(hù)在 useHandleData中,導(dǎo)出:

{    ...map, // moduleMap、dashboardMap、panelMap    getData, // 生成巡檢報(bào)告的數(shù)據(jù)結(jié)構(gòu)    init: initData, // 初始化 Map}
面板選中回填

在進(jìn)入面板管理時,需要回填已選中的面板。我們可以通過 getSaveModel獲取到當(dāng)前巡檢報(bào)告的信息。把對應(yīng)的選中信息存放到 selectPanels中。

現(xiàn)在我們只需要改變 selectPanels中的值,就可以做到對應(yīng)面板的選中。

面板選中重置

直接遍歷 dashboardMap,并把每個selectPanels重置。

dashboardMap.forEach((dashboard) => {    dashboard.selectPanels = [];});
面板插入

在我們選中面板后,對選中面板進(jìn)行插入時,有幾種情況:

巡檢報(bào)告原本存在的面板,這次也選中,在插入時會比較數(shù)據(jù),如果數(shù)據(jù)發(fā)生改變,需要根據(jù)最新的數(shù)據(jù)源信息進(jìn)行請求,并渲染。巡檢報(bào)告原本存在的面板,這次未選中,在插入時,需要刪除掉未選中的面板。新選中的面板,在插入時,在對應(yīng)目錄的末尾進(jìn)行插入。

添加新面板需要,與目錄收縮類似,不同的是:

目錄收縮針對只有一個目錄,而插入在針對的是整體。目錄收縮是直接從子節(jié)點(diǎn)開始向上冒泡,而插入是先從根節(jié)點(diǎn)開始向下插入,插入完成后在根據(jù)最新的目錄數(shù)據(jù),更新一遍布局。
class DashboardModel {    update(panels: PanelData[]) {        this.updatePanels(panels); // 更新面板        this.resetDashboardGridPos(); // 重新布局        this.forceUpdate();    }    /**     * 以當(dāng)前與傳入的進(jìn)行對比,以傳入的數(shù)據(jù)為準(zhǔn),并在當(dāng)前的順序上進(jìn)行修改     * @param panels     */    updatePanels(panels: PanelData[]) {        const panelMap = new Map();        panels.forEach((panel) => panelMap.set(panel.id, panel));        this.panels = this.panels.filter((panel) => {            if (panelMap.has(panel.id)) {                panel.update(panelMap.get(panel.id));                panelMap.delete(panel.id);                return true;            }            return false;        });        panelMap.forEach((panel) => {            this.addPanel(panel);        });    }    addPanel(panelData: any) {        this.panels = [...this.panels, new PanelModel({ ...panelData, top: this })];    }    resetDashboardGridPos(panels: PanelModel[] = this.panels) {        let sumH = 0;        panels?.forEach((panel: any | PanelModel) => {            let h = ROW_HEIGHT;            if (isRowPanel(panel)) {                h += this.resetDashboardGridPos(panel.dashboard.panels);            } else {                h = panel.getHeight();            }            const gridPos = {                ...panel.gridPos,                y: sumH,                h,            };            panel.updateGridPos({ ...gridPos });            sumH += h;        });        return sumH;    }}class PanelModel {    /**     * 更新     * @param panel     */    update(panel: PanelData) {        // 數(shù)據(jù)源語句發(fā)生變化需要重新獲取數(shù)據(jù)        if (this.target !== panel.target) {            this.needRequest = true;        }        this.restoreModel(panel);        if (this.dashboard) {            this.dashboard.updatePanels(panel.panels ?? []);        }        this.needRequest && this.forceUpdate();    }}
面板請求

needRequest控制面板是否需要進(jìn)行請求,如果為 true在面板下一次進(jìn)行渲染時,會進(jìn)行請求。請求的處理也放在了 PanelModel 中。(是否單獨(dú)維護(hù)請求的邏輯?)

import { Params, params as fetchParams } from "../../components/useParams";class PanelModel {    target: string; // 數(shù)據(jù)源信息    getParams() {        return {            targets: this.target,            ...fetchParams,        } as Params;    }    request = () => {        if (!this.needRequest) return;        this.fetchData(this.getParams());    };    fetchData = async (params: Params) => {        const data = await this.fetch(params);        this.data = data;        this.needRequest = false;        this.forceUpdate();    };    fetch = async (params: Params) => { /* ... */ }}

我們數(shù)據(jù)渲染組件一般層級較深,而請求時會需要時間區(qū)間等外部參數(shù)。對于這部分參數(shù)采用全局變量的方式,用 useParams進(jìn)行維護(hù)。上層組件使用 change 修改參數(shù),數(shù)據(jù)渲染組件根據(jù)拋出的 params進(jìn)行請求。

export let params: Params = {    decimal: 1,    unit: null,};function useParams() {    const change = (next: (() => Params) | Params) => {        if (typeof next === "function") params = next();        params = { ...params, ...next } as Params;    };    return { params, change };}export default useParams;
面板刷新

從根節(jié)點(diǎn)向下查找,找到葉子節(jié)點(diǎn),在觸發(fā)對應(yīng)的請求。

class DashboardModel {    /**     * 刷新子面板     */    reloadPanels() {        this.panels.forEach((panel) => {            panel.reload();        });    }}class PanelModel {    /**     * 刷新     */    reload() {        if (isRowPanel(this)) {            this.dashboard.reloadPanels();        } else {            this.reRequest();        }    }    reRequest() {        this.needRequest = true;        this.request();    }}
右側(cè)目錄渲染錨點(diǎn)/序號

錨點(diǎn)采用 Anchor + id 選中組件。

序號根據(jù)每次渲染進(jìn)行生成。

采用發(fā)布訂閱管理渲染

每當(dāng)儀表盤改變布局的動作時,右側(cè)目錄就需要進(jìn)行同步更新。而任意一個面板都有可能需要觸發(fā)右側(cè)目錄的更新。如果我們采用實(shí)例內(nèi)維護(hù)對應(yīng)組件的渲染事件,有幾個問題:

需要進(jìn)行區(qū)分,比如刷新面板時,不需要觸發(fā)右側(cè)目錄的渲染。每個面板如何訂閱右側(cè)目錄的渲染事件?

最終采用了發(fā)布訂閱者模式,對事件進(jìn)行管理。

class EventEmitter {    list: Record = {};    /**     * 訂閱     * @param event 訂閱事件     * @param fn 訂閱事件回調(diào)     * @returns     */    on(event: string, fn: () => void) {}    /**     * 取消訂閱     * @param event 訂閱事件     * @param fn 訂閱事件回調(diào)     * @returns     */    off(event: string, fn: () => void) {}    /**     * 發(fā)布     * @param event 訂閱事件     * @param arg 額外參數(shù)     * @returns     */    emit(event: string, ...arg: any[]) {}
eventEmitter.emit(this.key); // 觸發(fā)面板的訂閱事件eventEmitter.emit(GLOBAL); // 觸發(fā)頂級訂閱事件,就包括右側(cè)目錄的更新
面板詳情展示

對面板進(jìn)行查看時,可修改時間等,這些操作會影響到實(shí)例中的數(shù)據(jù),需要對原數(shù)據(jù)與詳情中的數(shù)據(jù)進(jìn)行區(qū)分。

通過對原面板數(shù)據(jù)的重新生成一個 PanelModel實(shí)例,對這個實(shí)例進(jìn)行任意操作,都不會影響到原數(shù)據(jù)。

const model = panel.getSaveModel();const newPanel = new PanelModel({ ...model, top: panel.top }); // 創(chuàng)建一個新的實(shí)例setEditPanel(newPanel); // 設(shè)置為詳情

dom上,詳情頁面是采用絕對定位,覆蓋著巡檢報(bào)告。

pdf/word 導(dǎo)出

pdf 導(dǎo)出由 html2Canvas + jsPDF 實(shí)現(xiàn)。需要注意的是,當(dāng)圖片過長pdf會對圖片進(jìn)行切分,有可能出現(xiàn)切分的時內(nèi)容區(qū)域。

需要手動計(jì)算面板的高度,是否超出當(dāng)前文檔,如果超出需要我們提前進(jìn)行分割,添加到下一頁中。盡可能把目錄面板和數(shù)據(jù)面板一塊切分。

word 導(dǎo)出由 html-docx-js 實(shí)現(xiàn), 需要保留目錄的結(jié)構(gòu),并可以在面板下添加總結(jié),這就需要我們分別對每一個面板進(jìn)行圖片的轉(zhuǎn)換。

實(shí)現(xiàn)的思路是根據(jù) panels 遍歷,找到目錄面板就是用 h1、h2標(biāo)簽插入,如果是數(shù)據(jù)面板,在數(shù)據(jù)面板中維護(hù)一個 ref的屬性,能讓我們拿到當(dāng)前面板的 dom信息,根據(jù)這個進(jìn)行圖片轉(zhuǎn)換,并為 base64 的格式(word 只支持 base64 的圖片插入)。

關(guān)鍵詞:
最新動態(tài)
相關(guān)文章
如何實(shí)現(xiàn)巡檢報(bào)告? 今日熱聞
民樂合奏曲目推薦_民樂合奏曲
雪魚的做法大全紅燒(雪魚的做法大全)
全球新資訊:天津市氣象臺更新高溫紅色...
省人民醫(yī)院屯昌分院揭牌 合作提升屯昌...
特斯拉人形機(jī)器人將于7月亮相上海-天天精選