讲讲项目里的仪表盘编辑器(二)

news2024/11/15 3:20:41

应用场景

        正常来说,编辑器应用场景应该包括:

  •         编辑器-预览
  •         编辑器
  •         最终运行时    

怎么去设计

        

        上一篇推文,我们已经大概了解了编辑器场景。接下来,我们来看预览时的设计

编辑器-预览

        点击预览按钮,执行以下逻辑:

  /** @name 预览 **/
  async handlePreview() {
    ...
    // 打开抽屉组件,并往里面放置运行时模块
    createDrawer(
      h => h(DashboardRuntime, { props: { dashboard: this.form } }),
      {
        title: '预览仪表盘',
        width: 'calc(100vw - 200px)',
      },
    );
  }

        也就是说:

        所以我们直接关注运行时表现

运行时设计

<template>
  <HabitContext :habitKey="habitKey" @init="habitContextInit">
    <!-- loading框 -->
    <a-spin v-if="loading" />
    <div
      v-else
      :style="styleCSSVariable"
    >
      <background :background="themeBackground" :class="$style.background">
          <grid-layout v-bind="layoutProps">
            <dashboard-item
              v-for="field in fields"
              :key="field.pkId"
            />
          </grid-layout>
      </background>
    </div>
  </HabitContext>
</template>

         这里套了一层HabitContext框架,是用来应用和记录用户习惯的(后面讲)。a-spin是加载层。紧接着和设计器差不多,局部变量样式集里面套了个背景框架和grid-layout布局。

        我们再看看dashboard-item的实现:

<template>
  <grid-item
    v-bind="layout"
    static
  >
    ...
  </grid-item>
</template>

         这里通过v-bind动态传入grid-item的属性(也就是拣选出来的x/y/w/h这些)。同时用static固定gird-item,使其无法缩放、拖动、被其他元素影响。

<template>
  <grid-item
    v-bind="layout"
    static
  >
    <div
      v-if="showChart"
    >
     ...
    </div>
    <!-- 没权限显示占位图 -->
    <div
      v-else
      style="height: 100%; width: 100%; display: flex; flex-direction: column"
    >
      <div>
        <span :style="titleCss">
          {{ field.name }}
        </span>
      </div>
    </div>
  </grid-item>
</template>

        这里就是简单的做了一个占位

<template>
  <grid-item
    v-bind="layout"
    static
  >
    <div
      v-if="showChart"
    >
      <div :class="$style.action">
        <template v-for="action in actions">
          <a-tooltip
            :key="action.key"
            placement="bottom"
            :mouseLeaveDelay="0"
            :title="action.name"
          >
            <x-icon
              :type="action.icon"
              @click="execAction(action)"
            />
          </a-tooltip>
        </template>
      </div>
      <component
        :is="component"
        :field="field"
      />
    </div>
    <!-- 没权限显示占位图 -->
    <div
      v-else
      style="height: 100%; width: 100%; display: flex; flex-direction: column"
    >
      <div>
        <span :style="titleCss">
          {{ field.name }}
        </span>
      </div>
    </div>
  </grid-item>
</template>

        浮层按钮还有具体的图表组件

数据流设计

        到这里,我们已经看完了编辑器功能的大概设计。接下来该写写这套系统最核心的部分,数据流设计了。

        创建一个仪表盘编辑器

        点下新增按钮后,我们传入一些系统参数【应用id,功能类别等等,在这里我们并不需要关注】储存新建仪表盘在系统的位置和属性。

        在接口储存完这些系统信息后,跳转到仪表盘页面进行最为关键的仪表盘初始化数据生成。

async handleAddForm(category) {
    // 弹窗让填写名称、图标等基础信息
    const result = await GroupForm.createModal(
      {
        data: { parentId: this.groupId, appId: this.appId, category },
      },
      {
        title: this.getCategoryName(category),
        width: '427px',
      },
    );
    // 调用接口保存
    const formId = await add(result);
    this.$message.success(this.$t('common.tips.add'));
    // 保存完毕后跳转到页面
    switch (category) {
      case FormCategoryType.DASHBOARD:
        return this.$router.push(`/dashboard-design/${formId}`);
        ...
      default:
        return this.$router.push(`/form-design/${formId}/form`);
    }
  }

        这里是通过vue-router进行跳转。这里也简单贴出路由代码

import DashboardDesign from '@/views/dashboard-design';

const DashboardDesignRoutes = [
  {
    path: '/dashboard-design/:id',
    component: DashboardDesign,
  },
  ...
];

export default DashboardDesignRoutes;

        到这里结束,一个仪表盘编辑器已经创建完毕了。它只存储了系统数据,没有仪表盘的初始数据。而当我们进入仪表盘编辑器页面的时候,完成有效编辑之后,才会以正式数据存储下来

        当然这里指的是前端数据,后端还是会根据我们穿进去的系统参数生成一份默认的接口向的仪表盘数据模板(比如默认权限、默认刷新时间上面的)

     

        进入仪表盘编辑器页面 

        先通过后端接口,拿到当前仪表盘编辑器id的接口数据

@formDesignModule.Action init;
async created() {
  ...
  await this.init(this.formId).then(() => {
      ...
  }
}

        大概长这样,记录一些系统信息或默认属性 。这里的init是vuex的action操作。为了是把数据保存到前端本地。更多关于本项目的vuex方法请看我另外一篇文章的介绍

讲讲项目里的状态存储器vuex_AI3D_WebEngineer的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/weixin_42274805/article/details/133237271?spm=1001.2014.3001.5501

         看看这个init的actions做了什么?

actions: {
    async init({ commit }, formId) {
        const form = await getFormData(formId);
        commit('saveForm', form);
    }
}
mutations: {
    saveForm(state, data) {
      state.form = data;
      ...
      state.loading = false;
      state.changed = false;
    }
}

          这里是通过调用接口获取当前仪表盘的数据,并把它存到当前的formDesignModule,也就是formDesign这个命名空间的仓库里。

        

        仪表盘编辑页面的状态管理器

        我们刚刚看到了代码,在编辑页面created里我们执行了init。其实就是非显示地获取数据。吧获取数据的过程从页面隐式地放到了状态管理器里的actions里面。并通过state返回关注的数据。这样子无论我们在仪表盘功能里怎么去跳转页面,都不需要再重新调用接口了,而是直接从仓库里拿。

  @formDesignModule.State form;
  @formDesignModule.Action init;
  @formDesignModule.State loading;
  @formDesignModule.State selectedField;
  @formDesignModule.Getter fields;
  @formDesignModule.Mutation updateSelectedField;
  @formDesignModule.Mutation selectField;
  @formDesignModule.Mutation updateSetting;
  @formDesignModule.Mutation saveForm;
  @formDesignModule.Mutation updateDashboardConfig;
  @formDesignModule.Action save;

        大概有这些属性和方法来完成编辑器的功能实现。看看就行了。紧接着我们来讲其中一些实现

        点击添加组件到仪表盘

        有两种添加方法:

        ① 点击组件按钮添加

        ② 拖拽组件添加

        点击组件


handleClickAdd() {
    ...
    // 初始化layout
    const layout = getDashboardLayoutByType(type);
    const layoutList = ensureArray(this.$refs.container.layout);
    layout.x = (layoutList.length * 2) % 60;
    layout.y = layoutList.length + 60;
    field.widget.layout = layout;

    // 初始化风格
    field = this.initFieldStyle(field);
}

        getDashboardLayoutByType是根据你点击的组件生成默认的组件layout数据。比如图片组件定义的默认layout是:

export function getDashboardLayoutByType(type) {
  const layout = getDashboardControlMeta(type, 'layout');
  return { x: 0, y: 0, ...(typeof layout === 'function' ? layout() : layout) };
}

         这时返回了一个初始化的layout即{w:30,h:15,minH:7,x:0,y:0}。

     const layoutList = ensureArray(this.$refs.container.layout);这里是直接获取设计器组件里面的layout属性(它的data值)。这个layout目前是个空数组(因为是新建的仪表盘,里面没有组件)。

layout.x = (layoutList.length * 2) % 60;
layout.y = layoutList.length + 60;
field.widget.layout = layout;


// 更新布局
this.$refs.container.syncLayout();

        很好理解啦,我们吧初始化layout的横纵坐标调整到它应该在的位置上,并吧这个调整过的layout信息存储到新增组件的布局属性里(替换掉初始化layout)。讲讲为什么这么计算:        

        可以看到实例中这两个组件的x/y值并不像上面这个逻辑计算出来的。 如果按照上面那个逻辑计算出来,则应该是{x:0,y:60...}和{x:2,y:61...}。其实这个计算过程是为了保证第n+1个组件的x和y一定大于第n个。从而避免重叠出错,而至于精准的layout数据,是借助vue-layout-grid插件行自适应生成。具体怎么做,我们看代码:

  /** @name 同步layout **/
  syncLayout() {
    this.layout = ensureArray(this.fields).map(field => ({
      ...field.widget.layout,
      i: field.pkId,
    }));
  }
<grid-layout
     ref="layout"
     :class="$style.layout"
     :layout.sync="layout"
>
    ...
</grid-layout>

        很多人看到这里就要骂了,骗人,你这不是啥都没干?只是把layout重新赋值了一遍。让我们改下代码看看:

  /** @name 同步layout **/
  async syncLayout() {
    this.layout = ensureArray(this.fields).map(field => ({
      ...field.widget.layout,
      i: field.pkId,
    }));
    console.log(this.layout);
    await this.$nextTick();
    console.log(this.layout);
  }

        第一个输出:

[
    {
            h: 10,w: 12,x: 0,y: 0
 
    },
    {
            h: 20,w: 60,x: 2,y: 61
     }   
]

        第二个输出:

[
{
   h: 10,w: 12,x: 0,y: 0,i: "39b19b29-c8ef-4fd3-8604-d7e168196ae6"
},
{
   h: 20,w: 60,x: 2,y: 10,i: "5d684834-26bd-4d35-b7ff-36d8de9d903e"
},
]   

        可以看到此时this.layout已经变了。这是因为<grid-layout>已经自适应了布局。

        由此,我们的保存仪表盘布局方法也呼之欲出了:

save() {
     // 拿到同步后的this.layout
     const layout = this.$refs.container.layout;
    // 生成组件id和layout信息的映射表
    const layoutMap = generateMap(layout, 'i', item =>
      pick(item, 'x', 'y', 'w', 'h'),
    );
    ...
}

        先看到这里,这里要生成一份类似于:'amdous123623': {w:10,h:20,x:0,y:0...}这样的映射表,是整个仪表盘布局的储存并不是直接存储类似于girdLayout的这种数组,而是由一个个组件自身的layout属性(甚至无视组件排序)拣选出来生成this.layout。也就是说仪表盘的存储结构为Array<field>这样的。

save(fields) {
    const layout = this.$refs.container.layout;
    const layoutMap = generateMap(layout, 'i', item =>
      pick(item, 'x', 'y', 'w', 'h'),
    );
    this.privateUpdateFields(
      (fields || this.fields).map(field => {
        if (!layoutMap[field.pkId]) return field;
        return {
          ...field,
          widget: {
            ...field.widget,
            layout:{
                  ...field.widget.layout,
                  ...layoutMap[field.pkId],
                }
          },
        };
      }),
    );
  }

        拖拽添加组件到仪表盘

        前面我们已经讲了拖拽添加组件的思路,和预防错位或重叠的处理。现在来讲讲具体代码实现。

        之前讲过了,在control-list.vue也就是左边的组件列表拖拽出组件,触发@dragstart方法,同时往设计器里传入dragType。设计器里根据dragType找对对应的组件初始化layout

@Watch('dragType')
  handleDragTypeChange(type) {
    this.isInChildCom = false; // 重新拖动需要重置
    if (type) {
      this.dragLayout = {
        i: 'drag',
        ...getDashboardLayoutByType(type),
      };
    } else {
      this.dragLayout = null;
    }
  }

        假设此时拖拽元素已经拖拽到在设计器(也就是gird-layout)上面。触发@dragover.native="handleDrag"

handleDrag(ev) {
    if (this.isInChildCom) return; // 进入子元素范围则无需触发
    ev.preventDefault();
    this._handleDrag(ev);
}
@throttle(100)
_handleDrag(ev) {
    if (!this.dragType || !this.$el) return;
    if (
      this.dragContext.clientX === ev.clientX &&
      this.dragContext.clientY === ev.clientY
    )
      return;
    this.dragContext.clientX = ev.clientX;
    this.dragContext.clientY = ev.clientY;
    this.updateInside(ev);
    this.updateDrag(ev);
}

         _handleDrag每100秒记录一次拖拽元素的位置,当拖拽元素发生变动时,更新设计器视图。

 updateInside(ev) {
    if (!this.dragType || !this.$el) return;
    const rect = this.$el.getBoundingClientRect();
    const errorRate = 10;
    const inside =
      ev.clientX > rect.left + errorRate &&
      ev.clientX < rect.right - errorRate &&
      ev.clientY > rect.top + errorRate &&
      ev.clientY < rect.bottom - errorRate;
    if (inside && this.dragLayoutIndex === -1) {
      this.layout.push(this.dragLayout);
    }
    if (!inside && this.dragLayoutIndex !== -1) {
      this.layout.splice(this.dragLayoutIndex, 1);
    }
  }

        这里是获取设计器边界的位置属性(errorRate为误差范围,你可以理解为设计器有padding),判断拖拽元素是否在设计器边界内,如果是,就往layout里面加入它(重复则不加入),如果已经超出设计器,则移除。

         我们往编辑器拖拽移动,可以看到这个虚线框会一直跟随变动,可能你们就要问了,上面的代码里dragLayout一但被添加进layout,那么dragLayoutIndex就不会是-1,也就是说layout里面的dragLayout不会改变(x或y)。那这个虚框是怎么还在移动的?

        其实啊,这个虚框并不由layout里的数据决定。而是由vue-grid-layout这个插件负责渲染的。在拖动的时候,this.layout是不会变的。我们只需要每100毫秒记录一次拖拽元素的当前位置this.dragLayout,直到放置生效之后,用this.dragLayout去覆盖this.layout里面的那个被拖动元素。

         所以updateDrag是为了更新this.dragLayout。通过clientY/X换算成vue-grid-layout的x,y

 const dragRef = this.getDragRef();
    if (!this.dragType || !dragRef) return;
    const rect = this.$el.getBoundingClientRect();
    const dragging = {
      top: this.dragContext.clientY - rect.top,
      left: this.dragContext.clientX - rect.left,
    };
    dragRef.dragging = dragging;
    const newLayout = dragRef.calcXY(dragging.top, dragging.left);
    this.dragLayout.x = newLayout.x;
    this.dragLayout.y = newLayout.y;
  }
getDragRef() {
    // vue-grid-layout默认在$children内存在一个组件实例了, 其实每次拖动直接取最后一个实例应该就可以了
    return this.$refs.layout.$children[this.$refs.layout.$children.length - 1];
}

        当我们放手时,触发 <grid-layout>组件上的drop事件,我们来看看@drop.native="handleDrop"的handleDrop方法

 async handleDrop() {
    if (this.isInChildCom) return; // 进入子元素范围则无需触发
    if (!this.dragType) return;
    ...
}

        重叠和空类型直接当做无效动作处理

 async handleDrop() {
    if (this.isInChildCom) return; // 进入子元素范围则无需触发
    if (!this.dragType) return;
    try {...}
    catch (e) {
      this.layout.splice(this.dragLayoutIndex, 1);
      throw e;
    }
    finally {
        this.$emit('update:dragType', null);
    }
}

         这个try catch我们之前已经讲过了。try里面的逻辑也很简单

try {
     let field = createDashboardField(this.dragType);
     ...
     field.widget.layout = pick(this.dragLayout, 'x', 'y', 'w', 'h');
     ...
     // 更新布局
      this.layout.splice(this.dragLayoutIndex, 1, {
        ...field.widget.layout,
        i: field.pkId,
      });
     // 提交数据存储
      this.$emit('add', field);
}

        拖拽移动组件位置

         由插件处理,会自动更新到this.layout

        放大缩小组件

        由插件处理,会自动更新到this.layout

        删除组件

async handleDelete(pkId) {
    const cloneFields = deepClone(this.fields);
    // 摘除删除的组件数据
    this.updateFields(
      cloneFields.filter(field => {
        return field.pkId !== pkId;
      }),
    );
    await this.$nextTick();
    this.$refs.container.syncLayout();
}
 /** @name 同步layout **/
  async syncLayout() {
    this.layout = ensureArray(this.fields).map(field => ({
      ...field.widget.layout,
      i: field.pkId,
    }));
    await this.$nextTick();
  }

        额外讲一下选中组件对组件进行修改

        当我们选中组件的时候,需要在vuex里登记一下当前的选中状态

<grid-item
    v-for="layoutItem in layout"
    ...
    @mousedown.native="handlePointerDown"
    @mouseup.native="handlePointerUp($event, layoutItem.i)"
>
    ...
</grid-item>

      加了一些位置判断,以防这个组件位置出错或已经不在布局里

  /** @name 鼠标设备按下与抬起事件处理 **/
  _pointerContext = null;
  handlePointerDown(ev) {
    this._pointerContext = {
      x: ev.clientX,
      y: ev.clientY,
    };
  }
  handlePointerUp(ev, pkId) {
    if (!this._pointerContext || !this.fieldMap[pkId]) return;
    const { x, y } = this._pointerContext;
    if (x !== ev.clientX || y !== ev.clientY) return;
    this.selectField(this.fieldMap[pkId]);
  }
 @formDesignModule.Mutation selectField;

        再来看看仓库的代码

    // fromdesign.js
    selectField(state, field) {
      state.selectedField = field;
    },

         如果当前组件的内容或属性发送变更,则执行

 commit('selectField', newField);

        

        

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1048104.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电脑显示系统错误怎么办?

有时我们在开机时会发现电脑无法开机&#xff0c;并显示系统错误&#xff0c;那么这该怎么办呢&#xff1f;下面我们就一起来了解一下。 方法1. 替换SAM文件解决问题 1. 重启电脑并进入安全模式。 Win8/10系统&#xff1a;在启动电脑看到Windows标志时&#xff0c;长按电源键…

SD-WAN网络升级攻略:企业如何快速上手部署?

随着企业信息化的升级&#xff0c;传统网络架构已经无法满足企业复杂的、多样化的组网互联需求。 企业多样化的组网需求包括但不限于以下内容&#xff1a; 一是需要将各办公点互联起来进行数据传输、资源共享&#xff1b; 二是视频会议、ERP、OA、邮箱系统、云服务应用程序等…

分享一下微信优惠券怎么制作

微信优惠券是一种有效的营销工具&#xff0c;可以吸引用户关注、提升购买意愿、促进消费等。下面我们将介绍如何制作微信优惠券&#xff0c;以及相关的注意事项。 一、优惠券的制作 首先&#xff0c;需要在微信公众平台上注册账号并完成企业认证。然后&#xff0c;按照以下步骤…

【LeetCode热题100】--2.两数相加

2.两数相加 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数…

关键词搜索淘宝商品数据接口

关键词搜索淘宝商品数据接口&#xff08;item_search&#xff09;是淘宝开放业务的形式之一&#xff0c;接口平台与淘宝签订协议之后&#xff0c;通过合法渠道获取到淘宝的官方数据&#xff0c;然后将这些数据储存在接口的数据库当中&#xff0c;供开发者进行调用。企业接入淘宝…

什么是推挽电路?

推挽电路原理&#xff1a; 可以简单理解为推和拉&#xff1b; 此电路总共用到两个元器件&#xff0c;对应图中的Q1----NPN三极管&#xff0c;Q2----PNP三极管&#xff0c;两个电阻R1和R2起到限流的作用&#xff1b;两个三极管的中间对应信号的输出。 下面就举例说明是如何工作的…

VMware VSAN 入门

一、虚拟化的存储 1.1、对于数据中心来说最重要的是数据&#xff0c;而承载数据的设备就是存储设备&#xff08;Storage&#xff09; 1.2、物理服务器的本地存储阵列 与 虚拟化服务器的本地存储阵列 对比 1.3、避免单台服务器故障的虚拟化高级特性&#xff1a;vSphere HA技术 …

记一次edu实战

最近刚好想着挖点洞练练手&#xff0c;像我这种菜鸡肯定是挖不到企业或者专属SRC&#xff08;呜呜呜&#xff09;&#xff0c;只能转向教育SRC&#xff0c;找点软柿子捏 0x00 前言 最近刚好不是很忙&#xff0c;想着挖点洞练练手&#xff0c;像我这种菜鸡肯定是挖不到企业或者…

天猫商品详情和京东商品详情数据比价接口

可以通过以下方式获取天猫商品详情和京东商品详情数据&#xff1a; 天猫商品详情数据接口&#xff1a;taobao.item_get&#xff0c;可以获取淘宝天猫商品详情。天猫商品列表数据接口&#xff1a;taobao.item_search&#xff0c;可以获取淘宝天猫商品列表。天猫店铺所有商品数据…

向量数据库风起时,闭源「墨奇AI数据库」想成为第三种存在

AI大模型时代下,图片、视频、自然语言等多模态的非结构化数据量陡增,而大模型支持的token数有限,虽然可以在RLHF的配合下具备一定程度的“短期记忆”,但正是因为“长期记忆”的缺失,导致大模型经常会出现“一本正经地胡说八道”的情况。 区别于用来处理结构化数据的传统数…

只是因为上了那个网站,就被公安局没收百万财产!

事件简介 9 月 24 日&#xff0c;有人在某社交网站发文称其为境外公司提供工作&#xff0c;访问国际互联网&#xff0c;被河北承德双桥公安处罚 105.8 万元&#xff0c;国庆之后在当地提起行政诉讼&#xff0c;并寻求律师的帮助&#xff1a; 这篇帖子一经发出&#xff0c;就引…

一个基于SpringBoot+vue前后端分离智慧仓库管理系统的设计实现

目录 一. 项目背景 二. 相关技术 2.1 HTTP协议 2.2 Java语言与JDK开发环境 2.3 HTML网页技术 2.4 MySQL数据库 三. 功能模块 四. 项目展示 4.1 登陆页面 4.2 主页 4.3 物资入库 4.4 发放物资 4.5 申领物资 4.6 物资信息 4.7 用户报表 4.8 仓库信息 4.9 用户管…

凹凸贴图如何提高物体的真实感

什么是凹凸贴图 凹凸贴图&#xff08;Bump Mapping&#xff09;是一种计算机图形学中的技术&#xff0c;用于在表面上模拟微小的凹凸形状&#xff0c;从而增加了物体的细节和真实感。它可以在不改变物体几何形状的情况下&#xff0c;通过修改光照的反应&#xff0c;使表面看起来…

文化适应与海外网红营销:化妆品品牌全球扩张的关键因素

随着全球化的不断发展&#xff0c;化妆品行业也在积极寻求海外市场的扩张。在这个竞争激烈的市场中&#xff0c;海外推广变得尤为重要&#xff1b;其中&#xff0c;海外网红营销成为许多品牌成功推广产品的关键策略之一。当前风口浪尖上的花西子&#xff0c;其实现海外市场扩张…

Win开启Telnet功能

Win10 开启Telnet功能 很多时候&#xff0c;我们在cmd下使用telnet功能发现显示&#xff1a;‘telnet’不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。 那要怎么办呢&#xff0c;接下来我们以win10为例&#xff0c;教大家如何打开telnet功能&#xff01; 1.打…

《家的温暖,国庆团圆》

目录 &#x1f4d6; 引言 &#x1f4dd; 假日放假表 &#x1f365; 中秋节 &#x1f4da; 中秋节的由来 中秋节的仪式 &#x1f4da; 赏月 &#x1f4da; 吃月饼 &#x1f4da; 猜灯谜 &#x1f4da; 品茶赏花 &#x1f4da; 舞狮龙 &#x1f4da; 中秋节的感触 &am…

「喜报」亚洲诚信TrustAsia成功入选上海市专利工作试点企业!

近日&#xff0c;上海市知识产权局公示了上海市专利工作试点示范单位名单。经自主申报、审核推荐、专家评审等程序&#xff0c;并经市知识产权局研究决定&#xff0c;亚数信息科技&#xff08;上海&#xff09;有限公司&#xff08;以下简称亚洲诚信TrustAsia&#xff09;凭借良…

OpenShift 介绍

OpenShift 1. OpenShift 简介1.1 OpenShift 核心功能1.2 OpenShift 特性1.3 OCP和OKD介绍 2. OpenShift 架构2.1 OpenShift 架构概述2.2 Master和Nodes 3. 管理 OpenShift3.1 OpenShift 项目及应用3.2 使用Source-to-image构建映像3.3 管理OpenShift资源 4. OpenShift 网络/持久…

iframe内的通信(桥接方法),使用postMessage和使用自定义事件

1、首先看一下我的文档目录 2、 接下来&#xff0c;上代码 outer.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta…