xinglie / report-designer

⚡打印设计、可视化、标签打印、编辑器、设计器、数据分析、报表设计、组件化、表单设计、h5页面、调查问卷、pdf生成、流程图、试卷、SVG、图形元素、物联网、标签纸
https://xinglie.github.io/report-designer/
943 stars 243 forks source link

迷你模式、自定义排版及设计器插件 #55

Open xinglie opened 3 years ago

xinglie commented 3 years ago

迷你模式

该设计器可以嵌入到其他页面中的某个区域内,和现有系统更好的融合

为了保证显示的完整,嵌入的区块请保证至少宽960px

首先在页面中,添加如下的html

<div>
    some other text
</div>
<div style="display:flex;">
    <div style="width:200px">
        left
    </div>
    <div id="app" class="app" style="width:1000px;height:600px;border:solid 1px #ccc;position: relative;overflow: hidden;">
        <div class="outer"><div class="inner"></div></div>
    </div>
    <div>
        right
    </div>
</div>
<div>
    <input placeholder="外部输入框测试" />
     bottom text
</div>

请注意上面htmlidapp的节点,到时候设计器会渲染在该节点里。为了更好的体验,你可以在app这个节点里添加一些loading动画,设计器加载完渲染时,会清除app节点下的内容。

完整示例源码请查看:https://github.com/xinglie/report-designer/blob/master/mini.html 在线demo示例请查看:https://xinglie.github.io/report-designer/mini.html

自定义排版

在设计器中默认的行为中,设计器会提供自身的一些交互方案,比如顶部元素的展示、面板的拖动处理等。

在这些交互的原则中,其中之一就是严格控制设计器显示的内容在指定的区域内,比如面板不能拖动到指定的DOM元素外边来。这是因为宿主页面的界面和交互是未知的,我们不能干扰和影响宿主页面的展示行为。

如果要对迷你模式进行近一步的界面控制,可以使用拆分再重新组织界面的方式。

完整示例源码请查看:https://github.com/xinglie/report-designer/blob/master/split.html 在线demo示例请查看:https://xinglie.github.io/report-designer/split.html

以下讲解下代码和思路

首先组织自己想要展示的HTML结构,如下示例

<div id="toolbar">

</div>
<div id="header">

</div>
<div style="display:flex;">
    <div style="width:150px" id="left">
        left
    </div>
    <div id="app" class="app" style="min-width:1100px;max-width:1100px;height:600px;border:solid 1px #ccc;position: relative;overflow: hidden;">
        <div style="display: flex;align-items: center;justify-content: center; height: 100%;">
            这里可以放一个加载动画...
        </div>
    </div>
    <div id="right">
        right
    </div>
</div>

我们把设计器中工具栏渲染到外部指定的toolbar这个节点上,header同理。

在设计器初始化的代码里,我们可以看到如下的代码

designer.setup({
    rootId:'app',
    mini:true,
    panels:{
        element:{
            to:'left'
        },
        data:{
            to:'right'
        }
    },
    header:{
        to:'header',
        hidden:false
    },
    toolbar:{
        hidden:false,
        to:'toolbar'
    }
});

我们需要指定splittrue,否则会有一些样式上的差异。

同时我们配置了panelsheadertoolbar等选项,选项中to指定设计器中的相应界面展示到哪个外部节点里,hidden指示是否隐藏该界面,比如我想隐藏header又不想把它渲染到外部其它节点中,则可以这样配置

designer.setup({
    rootId:'app',
    header:{
        hidden:true
    },
});

这样设计器自身将不再显示顶部元素栏

后续可以把设计器中的界面拿到外部来展示,进行一定的控制。如果界面改动量比较大,无法通过样式控制完成时,则需要自己二次开发组织相应的界面来供使用。

以顶部元素点击、拖动添加为例,默认是这样展示的。

image

我们如果把它拿到外部其它节点里渲染时,可能不符合交互要求,此时我们可以开发相同功能的面板,即把顶部元素放到一个面板里重新组织展示。

设计器提供了一个元素面板,在面板里展示顶部元素并完成相应的功能,设计器已经自带了该示例,目前界面如下

image

元素面板里面的行为与顶部在功能上一模一样。

后续则可以利用前面提到的,把设计器中的界面拿到外部使用,渲染元素面板到外部节点里。

看过代码你会清楚,像这样功能保持不变,对界面重新组织的需求,只需要继承原来已经实现的View,在html里重新组织下界面就好。

这样我们就可以对设计器进行一定的外部控制,同时可以保护好内部的功能和界面。

setup中,panels配置的面板名称都有哪些?

/**
 * 面板配置项
 */
interface OuterPanelsConfig {
    /**
     * 结构树面板
     */
    tree?: OuterViewConfig
    /**
     * 属性面板
     */
    props?: OuterViewConfig
    /**
     * 概览图面板
     */
    outline?: OuterViewConfig
    /**
     * 历史记录面板
     */
    record?: OuterViewConfig
    /**
     * 数据源面板
     */
    data?: OuterViewConfig
    /**
     * 元素面板
     */
    element?: OuterViewConfig
    /**
     * 调试面板
     */
    debug?: OuterViewConfig,
    /**
     * 动画面板
     */
    animate?: OuterViewConfig
    /**
     * 草稿
     */
    draft?: OuterViewConfig
    /**
     * 资源
     */
    resource?:OuterViewConfig
}

技术点

如何保证js代码与现有页面中的不冲突?

  1. 代码全部采用模块化的方式,设计器本身不向全局挂载和读取变量,只在入口提供designer对象供初始化配置,所以不会冲突
  2. 模块化目前使用seajs的加载器。如果你现有的加载器与seajs的define冲突,可自行修改打包配置,换成如require等其它加载器,目前支持amd cmd iife等加载器规范

如何保证样式代码不冲突?

https://github.com/thx/magix-composer 打包工具会对设计器中使用到的样式做全局编译,且编译时可对生成的选择器做规则定制,比如可以对所有选择器统一添加一个report-desinger的前缀,这样只要你项目中没有以report-desinger开头的样式则不会冲突。

风格统一

目前使用less管理相关的界面皮肤,主题已支持css3 变量,所以界面风格可以定制修改,与你当前系统页面相匹配

界面定制

设计器本身也是采用区块化开发的方式,把一个个界面区块独立开发,然后在入口页面对它们统一布局管理。这样当后期需要对它们重新布局或更新功能时非常方便。

其他像面板,顶部元素面板均可以去掉或定制到其它dom节点内,以和现有的页面更好的整合。

其它技术方案插件

React插件

封装designer.tsx文件


import React from 'react';
let idCounter = 0;
let scriptSource = '//localhost/report-designer/dist/designer.js';

let promisePools = {}; let LoadScript = url => { let key = 'script' + url; let p = promisePools[key]; if (!p) { p = promisePools[key] = new Promise(resolve => { let script = document.createElement('script'); script.onload = script.onerror = () => { script.parentNode.removeChild(script); setTimeout(resolve, 20); }; script.src = url; document.body.append(script); }); } return p; }; export default class extends React.Component { constructor() { super(); this.state = { nodeId: 'designer' + idCounter++ }; } async updateView() { await LoadScript(scriptSource); if (window.designer) { window.designer.setup({ rootId: this.state.nodeId, mini:true }); } } componentDidMount() { this.updateView(); } componentWillUnmount() { if (window.designer) { window.designer.destroy(); } } render() { return (<div id={this.state.nodeId} {...this.props}>{this.props.children}

) } }

> 使用`designer.tsx`文件

```js
import Designer from './designer.tsx';
//...

return (
        <>
          <Designer style={{position:'relative',width:900,height:400}}/>
    </>)

vue3插件

vue项目中新建Designer.vue文件

Designer.vue中的内容如下

<template>
    <div :id="designerId"></div>
</template>

<script lang="ts">
    import { defineComponent, onMounted, onBeforeUnmount } from 'vue'
    let idCounter = 0;
    let scriptSource = '//localhost/report-designer/dist/designer.js';

    let promisePools = {};
    let LoadScript = url => {
        let key = 'script_' + url;
        let p = promisePools[key];
        if (!p) {
            p = promisePools[key] = new Promise(resolve => {
                let script = document.createElement('script');
                script.onload = script.onerror = () => {
                    script.parentNode.removeChild(script);
                    setTimeout(resolve, 20);
                };
                script.src = url;
                document.body.append(script);
            });
        }
        return p;
    };
    export default defineComponent({
        setup() {
            let designerId = 'report-desinger-' + (idCounter++);
            onBeforeUnmount(() => {
                if (window.designer) {
                    window.designer.destroy();
                }
            });

            onMounted(async () => {
                await LoadScript(scriptSource);
                if (window.designer) {
                    window.designer.setup({
                        rootId: designerId,
                        mini: true
                    });
                }
            });
            return {
                designerId
            }
        },
    })
</script>

使用

<script setup>
import Designer from './path/to/Designer.vue'
</script>

<template>
    <Designer style="width:1000px;height:80vh"/>
</template>

vue插件2

<!doctype html>
<html>

<head>
  <meta charset="utf-8">
  <title>测试</title>
  <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
  <div id="app"></div>
</body>

</html>

<script>
  let ReportDesigner = () => {
    let idCounter = 0;
    let scriptSource = '//localhost/report-designer/dist/designer.js';
    let promisePools = {};
    let LoadScript = url => {
      let key = 'script_' + url;
      let p = promisePools[key];
      if (!p) {
        p = promisePools[key] = new Promise(resolve => {
          let script = document.createElement('script');
          script.onload = script.onerror = () => {
            script.parentNode.removeChild(script);
            setTimeout(resolve, 20);
          };
          script.src = url;
          document.body.append(script);
        });
      }
      return p;
    };
    return {
      data() {
        return {
          designerId: 'rd-' + idCounter++
        }
      },
      async mounted() {
        await LoadScript(scriptSource);
        if (window.designer) {
          window.designer.setup({
            rootId: this.designerId,
            mini: true
          });
        }
      },
      beforeUnmount() {
        if (window.designer) {
          window.designer.destroy();
        }
      },
      render() {
        return Vue.h('div', { id: this.designerId,style:'width:980px;height:400px' });
      }
    }
  };
  Vue.createApp(ReportDesigner()).mount('#app');
</script>

通用方案

也可以像mini.html那样,在入口页面引入designer.js,如,然后在合适的时候,通过调用designer.setup安装和designer.destroy销毁

<!DOCTYPE html>
<html>
<head>
    <title>report designer</title>
   <!--这里引入打包后的文件-->
    <script src="//localhost/report-designer/dist/designer.js"></script>
</head>
<body>
    <!--可以用样式进行修饰设计器的外轮廓-->
    <div id="rd" style="width:1000px;height:80vh"></div>
</body>
</html>
<script>
    //此时widnow上存在designer对象,可以调用designer.setup进行安装
    designer.setup({
        rootId:'rd',//设计器展示在哪个节点里
        mini:true//迷你模式,精简界面
    });
    //在合适的时候进行销毁,这里模拟10s后销毁
    setTimeout(()=>{
        designer.destroy();
    },10000);
</script>
a847704969 commented 2 years ago

大佬,数据源可以绑定 input 吗

xinglie commented 2 years ago

大佬,数据源可以绑定 input 吗

可以,配置一下就好了