Facilitez l'exploitation et la maintenance grâce à une analyse pratique du plan de mise en œuvre de la fonction de rapport d'inspection

Avec l'évolution de la technologie du Big Data et la demande croissante en matière de sécurité de l'information, l'expansion continue de l'échelle des données a posé de sérieux défis aux travaux d'exploitation et de maintenance des données. Confronté à la forte pression de gestion provoquée par des données massives, le personnel d'exploitation et de maintenance est confronté à des goulots d'étranglement en termes d'efficacité, et l'augmentation des coûts de main-d'œuvre fait qu'il n'est plus pratique de compter uniquement sur l'élargissement de l'équipe d'exploitation et de maintenance pour résoudre les problèmes.

On voit que l'intelligence, l'efficacité et la commodité sont les orientations inévitables du développement de l'exploitation et de la maintenance. La fonction de rapport d'inspection lancée par Kangaroo Cloud s'inscrit précisément dans cet objectif et s'engage à apporter des solutions optimisées.

Qu'est-ce qu'un rapport d'inspection ?

Le rapport d'inspection fait référence au processus consistant à effectuer une inspection complète d'un certain système ou équipement et à organiser les résultats et les suggestions de l'inspection dans un rapport. Les rapports d'inspection sont généralement utilisés pour évaluer l'état de fonctionnement et les performances des systèmes ou des équipements, fournissant ainsi une référence pour identifier les problèmes, optimiser les systèmes, améliorer l'efficacité et réduire les taux de défaillance.

déposer

Cet article détaillera les différentes caractéristiques fonctionnelles du rapport d'inspection et son plan de mise en œuvre, fournissant une référence pratique aux utilisateurs ayant de tels besoins.

Fonction de mise en œuvre du rapport d’inspection

● Mise en page personnalisée

· Les panneaux du rapport peuvent être glissés et déposés pour modifier la mise en page

· Limiter la zone de déplacement pendant le processus de déplacement. Le déplacement n'est autorisé qu'au sein du même niveau parent. Les changements au niveau du répertoire ne sont pas autorisés, comme le déplacement d'un répertoire de premier niveau vers un autre répertoire de premier niveau. Devenez un annuaire secondaire

● Le répertoire peut être réduit et étendu

· Le répertoire prend en charge la réduction et l'expansion. Lors de la réduction, tous les sous-panneaux sont masqués et lorsqu'ils sont développés, tous les sous-panneaux sont affichés.

· Lors du déplacement d'un répertoire, le sous-panneau suivra le déplacement

· Après avoir changé le répertoire, le panneau du répertoire sur la droite sera mis à jour simultanément.

· Générer un numéro de catalogue

déposer

● Arborescence des répertoires à droite

· Générer un numéro de catalogue

· Prise en charge du défilement d'ancre

· Soutenir l'expansion et la contraction

· Lié au rapport à gauche

déposer

● Panneau de données

· Obtenez des données d'indicateur basées sur une plage de dates

· Afficher les informations sur les indicateurs sous forme de graphiques

· Afficher les détails, supprimer

· Demander une conception pour chaque panneau afin de prendre en charge les demandes de rafraîchissement

déposer

déposer

●Importation du panneau

· Compter le nombre de panneaux sélectionnés dans le catalogue

· Lors de l'importation d'un nouveau panneau, la mise en page existante ne peut pas être détruite et le nouveau panneau ne peut que suivre l'ancien panneau.

· Lors de l'importation d'un panneau existant, une comparaison des données doit être effectuée . En cas de modifications des données, les dernières données doivent être obtenues à nouveau.

déposer

● Enregistrer

Avant la sauvegarde, toutes les opérations liées à la mise en page sont temporaires, y compris l'importation des panneaux. Ce n'est qu'après avoir cliqué sur Enregistrer que les données actuelles seront soumises au backend pour être enregistrées.

● Prend en charge l'exportation PDF et Word

déposer

Plan de mise en œuvre du rapport d’inspection

Alors, comment cet ensemble de fonctions de rapport d’inspection est-il mis en œuvre ? Ce qui suit présentera chaque aspect de la conception de la structure des données , de la conception des composants , du répertoire, du panneau, etc.

Conception de structures de données

Regardons d'abord le diagramme utilisant une structure plate :

déposer

Dans une structure plate, il suffit de trouver le panneau de ligne suivant pour déterminer l'enfant . Il en va de même pour les répertoires à plusieurs niveaux, mais le répertoire de premier niveau nécessite un traitement supplémentaire.

Bien que la structure plate soit relativement simple à mettre en œuvre, afin de répondre à des besoins spécifiques, c'est-à-dire limiter le glisser-déposer de répertoires. Restreindre le répertoire nécessite une relation hiérarchique de panneau relativement claire. De toute évidence, la structure arborescente des données peut décrire la structure hiérarchique d'une donnée de manière très appropriée et claire.

déposer

Conception des composants

C’est différent de la programmation de composants traditionnelle. En termes de mise en œuvre, le rendu et le traitement des données sont séparés et divisés en deux parties :

· Composant React : principalement responsable du rendu des pages

· Classe : Responsable du traitement des données

déposer

Modèle de tableau de bord

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

Modèle de panneau

class PanelModel {
    key?: string;
    id!: number;
    gridPos!: GridPos; // 位置信息
    title?: string;
    type: string;
    panels: PanelModel[]; // 目录面板需要维护当前目录下的面板信息
    // ...
}

Chaque composant Dashboard correspond à un DashboardModel et chaque composant Panel correspond à un PanelModel .

Les composants React sont rendus en fonction des données de l'instance de classe. Une fois l'instance produite, elle ne sera pas facilement détruite ou l'adresse de référence sera modifiée. Cela empêche les composants React qui s'appuient sur les données d'instance pour le rendu de déclencher un rendu de mise à jour.

Nous avons besoin d'un moyen de déclencher manuellement le rendu de mise à jour du composant après la modification des données de l'instance.

● Contrôle du rendu des composants

Puisque nous avons utilisé le composant Hooks auparavant, contrairement au composant Class, le composant peut être déclenché en appelant la méthode forceUpdate.

Il existe une nouvelle fonctionnalité dans React18, useSyncExternalStore , qui nous permet de nous abonner à des données externes. Si les données changent, cela déclenchera le rendu du composant.

En fait, le principe du déclenchement du rendu des composants par useSyncExternalStore est de maintenir un état en interne. Lorsque la valeur de l'état est modifiée, cela provoque le rendu des composants externes.

Sur la base de cette idée, nous avons simplement implémenté une méthode useForceUpdate qui peut déclencher le rendu des composants .

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

Bien que useForceUpdate soit implémenté, en utilisation réelle, l'événement doit être supprimé lorsque le composant est détruit. UseSyncExternalStore a été implémenté en interne et peut être utilisé directement.

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

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

Selon l'utilisation de useSyncExternalStore, les méthodes Subscribe et getSnapshot sont ajoutées respectivement .

class DashboardModel {  // PanelModel 一样 
    count = 0;

    forceUpdate() {
        this.count += 1;
        eventEmitter.emit(this.key);
    }

    /**
     * useSyncExternalStore 的第一个入参,执行 listener 可以触发组件的重渲染
     * @param listener
     * @returns
     */
    subscribe = (listener: () => void) => {
        eventEmitter.on(this.key, listener);
        return () => {
            eventEmitter.off(this.key, listener);
        };
    };

    /**
     * useSyncExternalStore 的第二个入参,count 在这里改变后触发diff的通过。
     * @param listener
     * @returns
     */
    getSnapshot = () => {
        return this.count;
    };
}

Lorsque les données sont modifiées et que le rendu du composant doit être déclenché, exécutez simplement forceUpdate.

panneau

● Glissement du panneau

Les plug-ins glisser-déposer les plus populaires sur le marché sont les suivants :

· réagir-belle-dnd

· réagir-dnd

· disposition de la grille de réaction

Après comparaison, il a été constaté que React-Grid-Layout est très approprié pour la fonction glisser-déposer du panneau. React-grid-layout lui-même est simple à utiliser et il n'y a fondamentalement aucun seuil pour commencer. Enfin, j'ai décidé d'utiliser React-grid-layout. Des instructions détaillées peuvent être trouvées sur ce lien : https://github.com/react-grid-layout/react-grid-layout

Une fois la disposition des panneaux modifiée, la méthode onLayoutChange de réagir-grid-layout est déclenchée pour obtenir les dernières données de position de tous les panneaux après la disposition.

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

PanelMap est une carte, la clé est Panel.key et la valeur est le panneau, qui est prêt lorsque notre composant est rendu.

const panelMap: Record<PanelModel['key'], PanelModel> = {};

Pour mettre à jour les données de disposition du panneau, vous pouvez localiser avec précision le panneau correspondant via PanelMap, puis appeler sa méthode updateGridPos pour effectuer l'opération de mise à jour de la disposition.

À ce stade, nous avons seulement terminé la mise à jour des données du panneau lui-même et nous devons également exécuter la méthode sortPanelsByGridPos du tableau de bord pour trier tous les panneaux.

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

●Plage de déplacement du panneau

La plage de déplacement actuelle correspond à l'ensemble du tableau de bord, qui peut être déplacé à volonté. Le vert est la zone déplaçable du tableau de bord et le gris est le panneau. comme suit:

déposer

Si des restrictions sont nécessaires, elles doivent être modifiées par la structure comme indiqué ci-dessous :

déposer

Sur la base de l'original, il est divisé en répertoires. Le vert est la zone mobile globale, le jaune est le bloc de répertoire de premier niveau, qui peut être glissé dans la zone verte, tout le bloc jaune est utilisé pour le glisser. et le violet est le répertoire de deuxième niveau. Les blocs peuvent être déplacés dans la zone jaune actuelle et ne peuvent pas être séparés du bloc jaune actuel. Les panneaux gris ne peuvent être déplacés que dans le répertoire actuel.

Doit être transformé en fonction de la structure de données d'origine :

déposer

class PanelModel {
    dashboard?: DashboardModel; // 当前目录下的 dashboard
    // ...
}

● Importer la conception du panneau

déposer

Les données renvoyées par le backend sont une arborescence à trois niveaux. Une fois obtenues, nous conservons les données dans trois cartes : ModuleMap, DashboardMap et PanelMap.

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[]; // 数据源语句信息
    label: string;
    panel_id: number;
}

type Expr = {
    expr: string;
    legendFormat: string;
};

export const DashboardContext = createContext({
    moduleMap: new Map<string, Module>(),
    dashboardMap: new Map<string, Dashboard>(),
    panelMap: new Map<number, Panel>(),
});

Lorsque nous rendons le module, nous parcourons le ModuleMap et trouvons le répertoire secondaire via les informations des tableaux de bord dans le module.

Définissez le répertoire de premier niveau pour qu'il ne soit pas sélectionnable dans l'interaction. Lorsque le répertoire de deuxième niveau est sélectionné, les panneaux pertinents sont trouvés via les panneaux du tableau de bord du répertoire secondaire et affichés dans la zone de droite.

Pour les opérations de ces trois Maps, elles sont conservées dans useHandleData et exportées :

{
    ...map, // moduleMap、dashboardMap、panelMap
    getData, // 生成巡检报告的数据结构
    init: initData, // 初始化 Map
}

●Remplissage de sélection de panneau

Lors de l'entrée dans la gestion des panneaux, les panneaux sélectionnés doivent être remplis. Nous pouvons obtenir les informations du rapport d'inspection actuel via getSaveModel et stocker les informations sélectionnées correspondantes dans selectPanels.

Il ne nous reste plus qu'à modifier la valeur dans selectPanels pour sélectionner le panneau correspondant.

● Réinitialisation de la sélection du panneau

Parcourez directement le DashboardMap et réinitialisez chaque selectPanels.

dashboardMap.forEach((dashboard) => {
    dashboard.selectPanels = [];
});

● Insertion de panneaux

Après avoir sélectionné le panneau, il existe plusieurs situations lors de l'insertion du panneau sélectionné :

· Le panneau qui existait à l'origine dans le rapport d'inspection est également sélectionné cette fois. Les données seront comparées lors de l'insertion. Si les données changent, elles doivent être demandées et rendues en fonction des dernières informations de la source de données.

· Les panneaux qui existaient à l'origine dans le rapport d'inspection ne sont pas sélectionnés cette fois-ci. Lors de l'insertion, les panneaux non sélectionnés doivent être supprimés.

· Le panneau nouvellement sélectionné sera inséré à la fin du répertoire correspondant lors de l'insertion

L'ajout d'un nouveau panneau nécessite, de la même manière que la réduction du répertoire , sauf :

· La réduction du répertoire cible un seul répertoire, tandis que l'insertion cible le répertoire entier.

· La réduction du répertoire remonte directement à partir des nœuds enfants, tandis que l'insertion commence à partir du nœud racine et s'insère vers le bas. Une fois l'insertion terminée, la mise en page est mise à jour en fonction des dernières données du répertoire.

class DashboardModel {
    update(panels: PanelData[]) {
        this.updatePanels(panels); // 更新面板
        this.resetDashboardGridPos(); // 重新布局
        this.forceUpdate();
    }

    /**
     * 以当前与传入的进行对比,以传入的数据为准,并在当前的顺序上进行修改
     * @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) {
        // 数据源语句发生变化需要重新获取数据
        if (this.target !== panel.target) {
            this.needRequest = true;
        }

        this.restoreModel(panel);

        if (this.dashboard) {
            this.dashboard.updatePanels(panel.panels ?? []);
        }

        this.needRequest && this.forceUpdate();
    }
}

● Demande de panel

needRequest contrôle si le panneau doit faire une demande. Si c'est vrai, la demande sera faite lors du prochain rendu du panneau, et le traitement de la demande est également placé dans le PanelModel.

import { Params, params as fetchParams } from '../../components/useParams';

class PanelModel {
    target: string; // 数据源信息

    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) => { /* ... */ }
}

Nos composants de rendu de données ont généralement un niveau profond et des paramètres externes tels que des intervalles de temps sont requis lors des requêtes. Ces paramètres sont conservés sous la forme de variables globales et useParams. Le composant supérieur utilise change pour modifier les paramètres, et le composant de rendu des données effectue des requêtes basées sur les paramètres émis.

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;

● Actualisation du panneau

Recherchez vers le bas à partir du nœud racine pour trouver le nœud feuille et déclencher la requête correspondante.

déposer

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

● Suppression de panneaux

Pour supprimer un panneau, il suffit de le supprimer sous le tableau de bord correspondant. Après la suppression, la hauteur actuelle du tableau de bord sera modifiée. Ce processus est cohérent avec le rétrécissement du répertoire ci-dessous.

class DashboardModel {
    /**
     * @param panel 删除的面板
     */
    removePanel(panel: PanelModel) {
        this.panels = this.filterPanelsByPanels([panel]);

        // 冒泡父容器,减少的高度
        const h = -panel.gridPos.h;
        this.top?.changeHeight(h);

        this.forceUpdate();
    }

    /**
     * 根据传入的面板进行过滤
     * @param panels 需要过滤的面板数组
     * @returns 过滤后的面板
     */
    filterPanelsByPanels(panels: PanelModel[]) {
        return this.panels.filter((panel) => !panels.includes(panel));
    }
    // ...
}

● Enregistrez le panneau

Après avoir communiqué avec le backend, la structure de données actuelle du rapport d'inspection est maintenue indépendamment par le frontend, et enfin une chaîne est transmise au backend. Obtenez les données actuelles du panneau et convertissez-les avec JSON.

Le processus d'acquisition d'informations du panneau commence à partir du nœud racine, traverse les nœuds feuilles , puis commence à partir des nœuds feuilles et remonte couche par couche, ce qui est le processus de retour en arrière.

class DashboardModel {
    /**
     * 获取所有面板数据
     * @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 {
    /**
     * 获取所有面板数据
     * @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;
    }
    // ...
}

● Affichage des détails du panneau

déposer

Lors de la visualisation du panneau, vous pouvez modifier l'heure, etc. Ces opérations affecteront les données de l'instance et vous devez distinguer les données d'origine des données des détails.

En régénérant une instance PanelModel à partir des données du panneau d'origine, toute opération sur cette instance n'affectera pas les données d'origine.

const model = panel.getSaveModel();
const newPanel = new PanelModel({ ...model, top: panel.top }); // 创建一个新的实例
setEditPanel(newPanel); // 设置为详情

Sur le dom, la page de détails utilise un positionnement absolu et couvre le rapport d'inspection.

Table des matières

● Réduire et développer le répertoire

Conservez une propriété réduite pour le panneau de répertoire afin de contrôler le masquage et l'affichage du panneau.

class PanelModel {
    collapsed?: boolean; // type = row
    // ...
}

// 组件渲染
{!collapsed && <DashBoard dashboard={panel.dashboard} serialNumber={serialNumber} />}

Lorsque vous réduisez et agrandissez le répertoire, sa hauteur change. Vous devez maintenant synchroniser cette hauteur modifiée avec le tableau de bord de niveau supérieur.

Ce que le niveau supérieur doit faire est similaire au traitement de notre répertoire de contrôle. Comme suit, contrôlez le rétrécissement du premier répertoire secondaire :

déposer

Lorsque des modifications surviennent dans le panneau, le panneau supérieur doit être informé et les opérations correspondantes doivent être effectuées.

déposer

Ajoutez un top pour obtenir l'instance parent .

class DashboardModel {
    top?: null | PanelModel; // 最近的 panel 面板

    /**
     * 面板高度变更,同步修改其他面板进行对应高度 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); // 触发父级变更
        this.forceUpdate();
    }
    // ...
}

Organisez les processus et les types de bulles, jusqu'au tableau de bord de niveau supérieur. L'expansion et la contraction sont les mêmes.

déposer

● Rendu du répertoire droit

Point d'ancrage/numéro de série

· Le point d'ancrage utilise Anchor + id pour sélectionner le composant

· Les numéros de série sont générés en fonction de chaque rendu

Utilisez la publication et l'abonnement pour gérer le rendu

Chaque fois que le tableau de bord modifie la présentation, le répertoire de droite doit être mis à jour de manière synchrone et n'importe quel panneau peut devoir déclencher la mise à jour du répertoire de droite.

Si nous conservons les événements de rendu des composants correspondants au sein de l'instance, il y a deux problèmes :

· Il faut distinguer, par exemple, lors du rafraîchissement du panneau, qu'il n'y a pas besoin de déclencher le rendu du répertoire de droite

· Comment chaque panneau s'abonne aux événements de rendu du répertoire de droite

Enfin, le modèle publication-abonnement a été adopté pour gérer les événements.

class EventEmitter {
    list: Record<string, any[]> = {};

    /**
     * 订阅
     * @param event 订阅事件
     * @param fn 订阅事件回调
     * @returns
     */
    on(event: string, fn: () => void) {}

    /**
     * 取消订阅
     * @param event 订阅事件
     * @param fn 订阅事件回调
     * @returns
     */
    off(event: string, fn: () => void) {}

    /**
     * 发布
     * @param event 订阅事件
     * @param arg 额外参数
     * @returns
     */
    emit(event: string, ...arg: any[]) {
}
eventEmitter.emit(this.key); // 触发面板的订阅事件

eventEmitter.emit(GLOBAL); // 触发顶级订阅事件,就包括右侧目录的更新

exportation pdf/mot

L'exportation PDF est implémentée par html2Canvas + jsPDF. Il est à noter que lorsque l'image est trop longue, le PDF segmentera l'image, et la zone de contenu pourra être segmentée. Nous devons calculer manuellement la hauteur du panneau pour voir si elle dépasse le document actuel. Si elle dépasse la hauteur, nous devons le diviser à l'avance et l'ajouter à la page suivante. Essayez de diviser le panneau de répertoire et le panneau de données ensemble. autant que possible.

L'exportation Word est implémentée par html-docx-js. Elle doit conserver la structure du répertoire et ajouter un résumé sous le panneau. Cela nous oblige à convertir les images de chaque panneau séparément.

L'idée de mise en œuvre est de parcourir les panneaux . Pour trouver le panneau de répertoire, insérez-le à l'aide des balises h1 et h2. S'il s'agit d'un panneau de données, conservez un attribut ref dans le panneau de données, ce qui nous permet d'obtenir les informations DOM de. le panneau actuel et effectuez une conversion d'image sur cette base et au format base64 (word ne prend en charge que l'insertion d'images base64).

écris à la fin

La version actuelle du rapport d'inspection en est encore à ses balbutiements et n'est pas dans sa forme définitive. Avec les mises à niveau itératives ultérieures, nous ajouterons progressivement plusieurs fonctions, notamment des explications sommaires.

Après avoir été implémentée de la manière actuelle, si l'interface de l'interface utilisateur doit être ajustée à l'avenir, seuls les composants pertinents de l'interface utilisateur doivent être modifiés de manière ciblée, par exemple en ajoutant des diagrammes circulaires, des tableaux, etc. Pour les modifications au niveau de l'interaction des données, il vous suffit de saisir DashboardModel et PanelModel pour effectuer les mises à jour nécessaires. De plus, pour des scénarios spécifiques, nous pouvons également extraire de manière flexible des classes spéciales pour le traitement afin de garantir que l'ensemble du processus d'itération est plus modulaire et efficace.

Adresse de téléchargement du « Livre blanc sur les produits Dutstack » : https://www.dtstack.com/resources/1004?src=szsm

Adresse de téléchargement du « Livre blanc sur les pratiques de l'industrie de la gouvernance des données » : https://www.dtstack.com/resources/1001?src=szsm

Pour ceux qui souhaitent en savoir ou en savoir plus sur les produits Big Data, les solutions industrielles et les cas clients, visitez le site officiel de Kangaroo Cloud : https://www.dtstack.com/?src=szkyzg

Linus a pris sur lui d'empêcher les développeurs du noyau de remplacer les tabulations par des espaces. Son père est l'un des rares dirigeants capables d'écrire du code, son deuxième fils est directeur du département de technologie open source et son plus jeune fils est un noyau open source. contributeur. Robin Li : Le langage naturel deviendra un nouveau langage de programmation universel. Le modèle open source prendra de plus en plus de retard sur Huawei : il faudra 1 an pour migrer complètement 5 000 applications mobiles couramment utilisées vers Java, qui est le langage le plus enclin . vulnérabilités tierces. L'éditeur de texte riche Quill 2.0 a été publié avec des fonctionnalités, une fiabilité et des développeurs. L'expérience a été grandement améliorée. Bien que l'ouverture soit terminée, Meta Llama 3 a été officiellement publié. la source de Laoxiangji n'est pas le code, les raisons derrière cela sont très réconfortantes. Google a annoncé une restructuration à grande échelle.
{{o.name}}
{{m.nom}}

Je suppose que tu aimes

Origine my.oschina.net/u/3869098/blog/11046131
conseillé
Classement