Torne a operação e a manutenção livres de preocupações com a análise prática do plano de implementação da função de relatório de inspeção

Com a evolução da tecnologia de big data e a crescente demanda por segurança da informação, a expansão contínua da escala de dados trouxe graves desafios para a operação e manutenção de dados. Confrontados com a forte pressão de gestão causada por dados massivos, o pessoal de operação e manutenção enfrenta gargalos de eficiência, e os crescentes custos laborais tornam já não prático depender apenas da expansão da equipa de operação e manutenção para resolver problemas.

Percebe-se que inteligência, eficiência e comodidade são os rumos inevitáveis ​​para o desenvolvimento da operação e manutenção. A função de relatório de inspeção lançada pela Kangaroo Cloud é justamente para cumprir esse objetivo e tem o compromisso de fornecer soluções otimizadas.

O que é um relatório de inspeção?

Relatório de inspeção refere-se ao processo de realização de uma inspeção abrangente de um determinado sistema ou equipamento e organização dos resultados e sugestões da inspeção em um relatório. Os relatórios de inspeção geralmente são utilizados para avaliar o status operacional e o desempenho de sistemas ou equipamentos, fornecendo referência para identificação de problemas, otimização de sistemas, melhoria de eficiência e redução de taxas de falhas.

arquivo

Este artigo irá detalhar as diversas características funcionais do relatório de inspeção e seu plano de implementação, fornecendo uma referência prática para usuários com tais necessidades.

Função de implementação do relatório de inspeção

● Layout personalizado

· Os painéis do relatório podem ser arrastados e soltos para alterar o layout

· Limitar a área de arrastar durante o processo de arrastar só é permitido dentro do mesmo nível pai. Não são permitidas alterações no nível do diretório, como mover um diretório de primeiro nível para outro diretório de primeiro nível. Torne-se um diretório secundário

● O diretório pode ser reduzido e expandido

· O diretório suporta redução e expansão. Ao reduzir, todos os subpainéis são ocultados e, quando expandidos, todos os subpainéis são exibidos.

· Ao mover um diretório, o subpainel seguirá a movimentação

· Após alterar o diretório, o painel de diretório à direita será atualizado simultaneamente.

· Gerar número de catálogo

arquivo

● Árvore de diretórios à direita

· Gerar número de catálogo

· Suporta rolagem de âncora

· Apoiar expansão e contração

· Vinculado ao relatório à esquerda

arquivo

● Painel de dados

· Obtenha dados do indicador com base no intervalo de datas

· Exibir informações dos indicadores na forma de gráficos

· Ver detalhes, excluir

· Solicite design para cada painel para suportar solicitações de atualização

arquivo

arquivo

●Importação de painel

· Contar a quantidade de painéis selecionados no catálogo

· Ao importar um novo painel, o layout existente não pode ser destruído e o novo painel só pode seguir o painel antigo.

· Ao importar um painel existente, é necessário realizar uma comparação de dados . Se houver alterações nos dados, os dados mais recentes deverão ser obtidos novamente.

arquivo

● Salvar

Antes de salvar, todas as operações relacionadas ao layout são temporárias, inclusive a importação de painéis. Somente após clicar em Salvar os dados atuais serão enviados ao backend para serem salvos.

● Suporta exportação de PDF e Word

arquivo

Plano de implementação do relatório de inspeção

Então, como é implementado esse conjunto de funções de relatório de inspeção? A seguir apresentaremos cada aspecto do design da estrutura de dados , design de componentes , diretório, painel, etc.

Projeto de estrutura de dados

Vejamos primeiro o diagrama usando uma estrutura plana:

arquivo

Em uma estrutura plana, você só precisa encontrar o painel da próxima linha para determinar o filho . O mesmo se aplica aos diretórios de vários níveis, mas o diretório de primeiro nível requer processamento adicional.

Embora a estrutura plana seja relativamente simples de implementar, para atender necessidades específicas, ou seja, para limitar o arrastar e soltar de diretórios. Restringir o diretório requer um relacionamento hierárquico de painel relativamente claro. Obviamente, a estrutura de dados em árvore pode descrever a estrutura hierárquica de um dado de maneira muito adequada e clara.

arquivo

Projeto de componentes

É diferente da programação de componentes tradicional. Em termos de implementação, a renderização e o processamento de dados são separados e divididos em duas partes:

· Componente React: principal responsável pela renderização da página

· Turma: Responsável pelo tratamento dos dados

arquivo

DashboardModel

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

Modelo de Painel

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

Cada componente Dashboard corresponde a um DashboardModel e cada componente Panel corresponde a um PanelModel .

Os componentes do React são renderizados com base nos dados da instância da classe. Depois que a instância for produzida, ela não será facilmente destruída ou o endereço de referência será alterado. Isso evita que os componentes do React que dependem dos dados da instância para renderização acionem a renderização de atualização.

Precisamos de uma maneira de acionar manualmente a renderização de atualização do componente depois que os dados na instância forem alterados.

● Controle de renderização de componentes

Como usamos o componente Hooks antes, ao contrário do componente Class, o componente pode ser acionado chamando o método forceUpdate.

Há um novo recurso no react18, useSyncExternalStore , que nos permite assinar dados externos. Se os dados mudarem, isso irá acionar a renderização do componente.

Na verdade, o princípio de acionar a renderização do componente useSyncExternalStore é manter um estado internamente. Quando o valor do estado é alterado, ele causa a renderização de componentes externos.

Com base nessa ideia, simplesmente implementamos um método useForceUpdate que pode acionar a renderização de componentes .

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

Embora useForceUpdate seja implementado, no uso real, o evento precisa ser removido quando o componente é destruído. UseSyncExternalStore foi implementado internamente e pode ser usado diretamente.

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

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

De acordo com o uso de useSyncExternalStore, os métodos subscribe e getSnapshot são adicionados respectivamente .

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

Quando os dados forem alterados e a renderização do componente precisar ser acionada, basta executar forceUpdate.

painel

●Arrastar painel

Os plug-ins de arrastar e soltar mais populares no mercado incluem o seguinte:

· reagir-belo-dnd

· reagir-dnd

· layout de grade de reação

Após comparação, descobriu-se que o layout da grade de reação é muito adequado para a função de arrastar e soltar do painel. O react-grid-layout em si é simples de usar e basicamente não há limite para começar. Finalmente, decidi usar o react-grid-layout. Instruções detalhadas podem ser encontradas neste link: https://github.com/react-grid-layout/react-grid-layout

Após a alteração do layout do painel, o método onLayoutChange do react-grid-layout é acionado para obter os dados de posição mais recentes de todos os painéis após o layout.

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

PanelMap é um mapa, a chave é Panel.key e o valor é o painel, que está pronto quando nosso componente é renderizado.

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

Para atualizar os dados de layout do painel, você pode localizar com precisão o painel correspondente por meio do PanelMap e chamar seu método updateGridPos para executar a operação de atualização de layout.

Neste ponto, concluímos apenas a atualização dos dados do próprio painel e também precisamos executar o método sortPanelsByGridPos do painel para classificar todos os painéis.

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

●Intervalo de arrasto do painel

A faixa de arrasto atual é todo o painel, que pode ser arrastado à vontade. Verde é a área arrastável do painel e cinza é o painel. do seguinte modo:

arquivo

Se forem necessárias restrições, será necessário alterá-la para a estrutura mostrada abaixo:

arquivo

Com base no original, ele é dividido em diretórios. Verde é a área móvel geral, amarelo é o bloco de diretório de primeiro nível, que pode ser arrastado na área verde. Ao arrastar, todo o bloco amarelo é usado para arrastar. e roxo é o diretório de segundo nível. Os blocos podem ser arrastados dentro da área amarela atual e não podem ser separados do bloco amarelo atual.

Precisa ser transformado com base na estrutura de dados original:

arquivo

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

● Importar design do painel

arquivo

Os dados retornados pelo backend são uma árvore com três níveis. Após obtê-los, mantemos os dados em três mapas: ModuleMap, DashboardMap e 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>(),
});

Ao renderizarmos o módulo, percorremos o ModuleMap e encontramos o diretório secundário por meio das informações dos painéis do Módulo.

Defina o diretório de primeiro nível como não selecionável na interação Quando o diretório de segundo nível é selecionado, os painéis relevantes são encontrados através dos painéis do painel do diretório secundário e renderizados na área direita.

Para as operações destes três Mapas, eles são mantidos em useHandleData e exportados:

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

●Preenchimento de seleção de painel

Ao entrar no gerenciamento de painéis, os painéis selecionados precisam ser preenchidos. Podemos obter as informações do relatório de inspeção atual através de getSaveModel e armazenar as informações selecionadas correspondentes em selectPanels.

Agora só precisamos alterar o valor em selectPanels para selecionar o painel correspondente.

● Redefinição da seleção do painel

Percorra o DashboardMap diretamente e redefina cada selectPanels.

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

● Inserção de painel

Após selecionarmos o painel, existem diversas situações na hora de inserir o painel selecionado:

· O painel que existia originalmente no relatório de inspeção também é selecionado desta vez. Os dados serão comparados durante a inserção. Se os dados forem alterados, eles deverão ser solicitados e renderizados com base nas informações mais recentes da fonte de dados.

· Os painéis que existiam originalmente no relatório de inspeção não estão selecionados desta vez. Ao inserir, os painéis não selecionados precisam ser excluídos.

· O painel recém-selecionado será inserido no final do diretório correspondente ao inserir

Adicionar um novo painel requer algo semelhante à redução de diretório , exceto:

· A redução de diretório tem como alvo apenas um diretório, enquanto a inserção tem como alvo o diretório inteiro.

· A redução do diretório aumenta diretamente dos nós filhos, enquanto a inserção começa no nó raiz e é inserida para baixo. Após a conclusão da inserção, o layout é atualizado com base nos dados mais recentes do diretório.

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

● Solicitação de painel

needRequest controla se o painel precisa fazer uma solicitação. Se for verdade, a solicitação será feita na próxima vez que o painel for renderizado, e o processamento da solicitação também será colocado no 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) => { /* ... */ }
}

Nossos componentes de renderização de dados geralmente têm um nível profundo e parâmetros externos, como intervalos de tempo, são necessários ao fazer solicitações. Esses parâmetros são mantidos na forma de variáveis ​​globais e useParams. O componente superior usa alterações para modificar parâmetros, e o componente de renderização de dados faz solicitações com base nos parâmetros lançados.

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;

● Atualização do painel

Pesquise para baixo a partir do nó raiz para encontrar o nó folha e acionar a solicitação correspondente.

arquivo

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

● Exclusão de painéis

Para excluir um painel, precisamos apenas removê-lo do Dashboard correspondente. Após a exclusão, a altura atual do Dashboard será alterada. Este processo é consistente com a redução do diretório abaixo.

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

● Salve o painel

Após a comunicação com o back-end, a estrutura de dados do relatório de inspeção atual é mantida de forma independente pelo front-end e, finalmente, uma string é fornecida ao back-end. Obtenha os dados atuais do painel e converta-os com JSON.

O processo de aquisição de informações do painel começa no nó raiz, atravessa os nós folha e depois começa nos nós folha e retorna para cima camada por camada, que é o processo de retrocesso.

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

● Exibição de detalhes do painel

arquivo

Ao visualizar o painel, você pode modificar a hora, etc. Essas operações afetarão os dados da instância e você precisará distinguir os dados originais dos dados nos detalhes.

Ao regenerar uma instância PanelModel a partir dos dados do painel originais, qualquer operação nesta instância não afetará os dados originais.

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

No dom, a página de detalhes utiliza posicionamento absoluto e abrange o relatório de inspeção.

Índice

● Diminuir e expandir o diretório

Mantenha uma propriedade recolhida para o painel do diretório para controlar a ocultação e exibição do painel.

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

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

Quando você reduz e expande o diretório, sua altura muda. Agora você precisa sincronizar essa altura alterada com o painel de nível superior.

O que o nível superior precisa fazer é semelhante ao processamento do nosso diretório de controle. Da seguinte forma, controle a redução do primeiro diretório secundário :

arquivo

Quando ocorrem alterações no painel, o painel superior precisa ser notificado e as operações correspondentes realizadas.

arquivo

Adicione um top para obter a instância pai .

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

Organize processos e tipos de bolhas até o painel de nível superior. Expansão e contração são iguais.

arquivo

● Renderização correta do diretório

Ponto de ancoragem/número de série

· O ponto de ancoragem usa Anchor + id para selecionar o componente

· Os números de série são gerados com base em cada renderização

Use publicar e assinar para gerenciar a renderização

Sempre que o layout do painel muda, o diretório à direita precisa ser atualizado de forma síncrona e qualquer painel pode precisar acionar a atualização do diretório à direita.

Se mantivermos os eventos de renderização dos componentes correspondentes dentro da instância, existem dois problemas:

· É necessário distinguir, por exemplo, ao atualizar o painel, não há necessidade de acionar a renderização do diretório à direita

· Como cada painel assina os eventos de renderização do diretório à direita

Por fim, foi adotado o modelo de publicação-assinante para gerenciamento de eventos.

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); // 触发顶级订阅事件,就包括右侧目录的更新

exportação de pdf/palavra

A exportação de PDF é implementada por html2Canvas + jsPDF. Deve-se observar que quando a imagem for muito longa, o PDF irá segmentar a imagem e a área de conteúdo poderá ser segmentada. Precisamos calcular manualmente a altura do painel para ver se excede o documento atual. Se exceder a altura, precisamos dividi-lo antecipadamente e adicioná-lo à próxima página. Tente dividir o painel do diretório e o painel de dados juntos. tanto quanto possível.

A exportação do Word é implementada por html-docx-js. Ele precisa manter a estrutura do diretório e adicionar um resumo no painel. Isso exige que convertamos as imagens para cada painel separadamente.

A ideia da implementação é percorrer os painéis . Para encontrar o painel do diretório, insira-o usando as tags h1 e h2. Se for um painel de dados, mantenha um atributo ref no painel de dados, que nos permite obter as informações do DOM. o painel atual e realizar a conversão de imagem com base neste e no formato base64 (o Word suporta apenas a inserção de imagens em base64).

escreva no final

A versão atual do relatório de inspeção ainda está em sua infância e não está em sua forma final. Com atualizações iterativas subsequentes, adicionaremos gradualmente diversas funções, incluindo explicações resumidas.

Depois de ser implementada da maneira atual, se a interface da IU precisar ser ajustada no futuro, apenas os componentes relevantes da IU precisarão ser modificados de maneira direcionada, como adicionar gráficos de pizza, tabelas, etc. Para alterações no nível de interação de dados, basta inserir DashboardModel e PanelModel para fazer as atualizações necessárias. Além disso, para cenários específicos, também podemos extrair com flexibilidade classes especiais para processamento, garantindo que todo o processo de iteração seja mais modular e eficiente.

Endereço de download do "White Paper do produto Dutstack": https://www.dtstack.com/resources/1004?src=szsm

Endereço para download do "White Paper sobre práticas da indústria de governança de dados": https://www.dtstack.com/resources/1001?src=szsm

Para quem deseja conhecer ou consultar mais sobre produtos de big data, soluções industriais e cases de clientes, visite o site oficial da Kangaroo Cloud: https://www.dtstack.com/?src=szkyzg

Linus assumiu a responsabilidade de evitar que os desenvolvedores do kernel substituíssem tabulações por espaços. Seu pai é um dos poucos líderes que sabe escrever código, seu segundo filho é o diretor do departamento de tecnologia de código aberto e seu filho mais novo é um núcleo de código aberto. contribuidor Robin Li: A linguagem natural se tornará uma nova linguagem de programação universal. O modelo de código aberto ficará cada vez mais atrás da Huawei: levará 1 ano para migrar totalmente 5.000 aplicativos móveis comumente usados ​​para Hongmeng. vulnerabilidades de terceiros. O editor de rich text Quill 2.0 foi lançado com recursos, confiabilidade e desenvolvedores. A experiência foi bastante melhorada. fonte de Laoxiangji não é o código, as razões por trás disso são muito comoventes. O Google anunciou uma reestruturação em grande escala.
{{o.nome}}
{{m.nome}}

Acho que você gosta

Origin my.oschina.net/u/3869098/blog/11046131
Recomendado
Clasificación