[实现]datav中使用select2库实现下拉菜单

756 阅读3分钟

写在前面:datav视觉展示能力强大,但用户交互能力弱,无法应对产品经理的需求。原本在工程内的逻辑,要拆分成很多块来写。需要用原生js或者jquery实现。datav的底层实现没有渠道快速获知。遇到卡点的时候,得咨询datav工作人员才得解。如果不是项目要求,个人是不会尝试去使用这样的工具的。

一.涉及datav中的使用要点:

1.自定义组件

2.画布编辑器

3.蓝图编辑器

4.hook

二.UED:

三.需求实现要点:

1.按钮的文案是动态的,要调用接口去获取,按钮最多展示3个,其他的收到更多下拉当中。

2.点击按钮时,请求对应按钮的数据

3.默认展示第一个按钮对应的数据

四.自定义组件实现:

组件的文件组织结构如图1,其中dist为本地编译utils文件夹后的结果,编译的插件是

button-list.ts

import "./button-list.css";
var $ = require("jquery");

type ButtonListProps = {
  config: {
    showMore: Boolean;
    showLength: number;
  };
  data: Record<string, string | number>[];
  firstSelected: any;
  buttonListChange: (val) => void;
};

function render(props: ButtonListProps) {
  const { config, data, firstSelected, buttonListChange } = props;
  const { showMore, showLength } = config;
  let curData = [];
  let selectorData = [];

  if (showMore && showLength) {
    curData = data.slice(0, showLength);
    selectorData = data.slice(showLength);
  } else if (showLength) {
    curData = data.slice(0, showLength);
  } else if (showMore) {
    selectorData = data;
  }

  $("[name=button_list_select]").val(undefined);

  function renderSelect() {
    return `<div class="button_list_select_container" id="button-list-id">
    <select class="button_list_select" name="button_list_select">
    <option></option>
    ${selectorData
      .map((item) => {
        const { label, value } = item;
        return `<option value=${value}>${label}</option>`;
      })
      .join("")}
      </select>
      </div>`;
  }

  function renderButtonList() {
    return `${curData
      .map((item) => {
        const { label, value } = item;
        return `<div class="button ${
          firstSelected === value ? "selected" : ""
        }" data-value=${value}><span class="text">${label}</span></div>`;
      })
      .join("")}`;
  }

  const container = $(`<div class="button_list">
    ${curData.length > 0 ? renderButtonList() : ""}
    ${selectorData.length > 0 ? renderSelect() : ""}
  </div>`);
  return container;
}

export { render };

button-list.less

.button_list {
  margin-bottom: 10px;
  display: flex;
  justify-content: center;
  font-family: "Microsoft Yahei";
  .button {
    height: 26px;
    min-width: 77px;
    background-color: rgba(255, 255, 255, 0.13);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 4px;
    color: rgba(255, 255, 255, 0.8);
    font-size: 16px;
    line-height: 22px;
    text-align: center;
    cursor: pointer;

    .text {
      width: 100%;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    &:not(:last-child) {
      margin-right: 12px;
    }
    &:hover,
    &.selected {
      background-color: rgba(0, 229, 255, 0.4);
    }
  }
  .button_list_select_container {
    position: relative;
    .button_list_select {
      display: none;
    }
    // 对select2 4.0.10 的样式覆盖
    .select2-container.select2-container--default {
      width: 70px;
      font-size: 16px;
    }
    .select2-container.select2-container--default {
      font-size: 16px;
      .select2-results__option[aria-selected="true"] {
        background-color: rgba(255, 255, 255, 0.13);
      }
      &:hover,
      &.selected {
        background-color: rgba(0, 229, 255, 0.4);
      }
      .select2-results__option--highlighted[aria-selected] {
        background-color: rgba(0, 229, 255, 0.4);
      }
      .select2-search--dropdown {
        display: none;
      }
      .select2-selection--single {
        border-radius: 0px;
        background-color: rgba(255, 255, 255, 0.13);
        border: none;
        .select2-selection__clear {
          color: #fff;
          font-size: 2em;
        }
        .select2-selection__rendered {
          color: rgba(255, 255, 255, 0.8);
          font-size: 16px;
        }
        .select2-selection__arrow b {
          border-color: rgba(255, 255, 255, 0.13) transparent transparent
            transparent;
        }
      }
      .select2-dropdown {
        border: 0px;
        background-color: rgba(70, 74, 77);
      }
    }

    .select2-results__option--selectable {
      background-color: rgba(255, 255, 255, 0.13);
      font-size: 16px;
      color: rgba(255, 255, 255, 0.8);
      display: inline-block;
      width: 100%;
      &.select2-results__option--selected {
        background-color: rgba(0, 116, 140, 1);
      }
      &.select2-results__option--highlighted {
        background-color: rgba(0, 116, 140, 1);
      }
    }
  }
}

index.js

var Event = require("bcore/event");
var $ = require("jquery");
var _ = require("lodash");
// require("select2")($); // 4.0.10
// require("select2/dist/css/select2.min.css");

var utils = require("./utils/dist/button-list");

/**
 * 马良基础类
 */
module.exports = Event.extend(
  function Base(container, config) {
    this.config = {
      theme: {},
    };
    this.container = $(container); //容器
    this.apis = config.apis; //hook一定要有
    this._data = null; //数据
    this.chart = null; //图表
    this.init(config);
  },
  {
    render: function (data, config) {
      data = this.data(data);
      var cfg = this.mergeConfig(config);

      const emitFunc = (val) => {
        this.emit("value_change", val);
      };
      const eleDom = data.data;
      const firstSelected = eleDom[0].value;
      if (eleDom && eleDom.length > 0) {
        this.emit("button_list_success", "success");
      }
      const app = utils.render({
        config: cfg,
        data: eleDom || {},
        firstSelected: firstSelected,
        buttonListChange: (val) => {
          emitFunc(val);
        },
      });
      emitFunc(firstSelected);
      this.container.html(app);

      //如果有需要的话,更新样式
      this.updateStyle();
    },
  }
);

hook.js的代码见六

package.json

{
  "name": "@xxx/button-list",
  "version": "0.0.33",
  "dependencies": {
    "bcore": "0.0.18",
    "jquery": "2.1.4",
    "lodash": "4.6.1"
  },
  "datav": {
    "cn_name": "按钮组件",
    "icon": "",
    "protocol": 2,
    "type": [
      "regular"
    ],
    "view": {
      "width": "400",
      "height": "200",
      "minWidth": "200",
      "minHeight": "100"
    },
    "apis": {
      "source": {
        "handler": "render",
        "description": "接口描述",
        "fields": {
          "value": {
            "description": "值说明"
          }
        }
      }
    },
    "config": {
      "width": {
        "type": "text",
        "name": "按钮区域的最大宽度",
        "default": 400
      },
      "height": {
        "type": "text",
        "name": "按钮区域的最大高度",
        "default": 26
      },
      "showMore": {
        "name": "是否展示更多下拉",
        "type": "switch",
        "statusText": "是",
        "default": true
      },
      "showLength": {
        "name": "展示非下拉按钮的个数",
        "type": "stepper",
        "step": 1,
        "min": 1,
        "default": 3
      }
    },
    "api_data": {
      "source": {
        "data": [
          {
            "label": "1001",
            "value": "1001"
          },
          {
            "label": "1002",
            "value": "1002"
          },
          {
            "label": "1003",
            "value": "1003"
          },
          {
            "label": "1004",
            "value": "1004"
          },
          {
            "label": "1005",
            "value": "1005"
          },
          {
            "label": "1006",
            "value": "1006"
          },
          {
            "label": "1007",
            "value": "1007"
          }
        ]
      }
    },
    "events": {
      "value_change": {
        "description": "当值发生变化时"
      },
      "button_list_success": {
        "description": "按钮数据获取完毕"
      }
    }
  }
}

图1:

图2:

五.蓝图编辑器配置:

1.请求数据,并转化数据成按钮组件支持的数据结构,数据处理完成导入到按钮组件中(请求数据需要用到画布编辑器中的api数据源,可以自定义一个视觉层面上不可见组件,用以调用接口)

2.按钮组件的数据源配置

数据响应结果:受控模式、静态数据源

按钮组件的接收的数据结构

3.按钮点击时,设置回调id,供待展示数据的api数据源做请求入参

4.待展示数据的数据源配置

数据源类型:API

请求方式:GET(这种动态入参的方式,datav截止目前只支持get请求的方式)

http://{api地址}?bqbm=:bqbm

六.HOOK代码(目前专业版的datav也有官网上说尊享版才有的hook功能)

执行流程:

1.按钮数据要请求

2.请求select2的css

3.请求select2的js

4.需要要确保dom.select2()在dom绘制完成后执行

////////// select2 /////////

/**
 * @param {Stage} stage
 */
module.exports = (stage) => {
  $(document).ready(function () {
    console.log("------ready ------ DOM ready!");
  });
  // 疑问:以下为啥不会执行???
  // document.addEventListener('DOMContentLoaded', function () {
  //   console.log("------ready ------ DOMContentLoaded!");
  // });

  // 专业版dom
  const dom = stage.get("@xxx_button-list_XFLUi");
  const container = $(dom.container);

  $(container.find(".button_list_select")).ready(function () {
    console.log("------ready ------ container -- button_list_select ready!");
  });

  // 获取select2的css文件
  $("head").append(
    `<link href = "https://cdn.jsdelivr.net/npm/select2@4.0.10/dist/css/select2.min.css" rel = "stylesheet" />`
  );

  //方式2:监听回调id事件
  dom.on("button_list_success", function (data) {
    if (data === "success") {
      let script = document.createElement("script");
      /* 
      为使onload内的方法执行,需要避免浏览器的disk cache。
      解决方法是:在请求js时加上随机值 
      */
      script.src = `https://cdn.jsdelivr.net/npm/select2@4.0.10/dist/js/select2.min.js?${+new Date()}`;
      script.onload = function () {
        console.log("------select2 ------ script.onload!");
        excuteMain();
      };
      document.head.append(script);
    }
  });

  function excuteMain() {
    const select2dom = container.find(".button_list_select").select2({
      dropdownParent: container.find("#button-list-id"),
      placeholder: "更多",
      width: 72,
    });
    const emitFunc = (val) => {
      dom.emit("value_change", val);
    };
    // 交互事件注册
    container.find(".button").on("click", function (e) {
      const val = $(e.currentTarget).data("value");
      container.find(".button").removeClass("selected");
      select2dom.val(undefined).trigger("change");
      select2dom.siblings(".select2-container").removeClass("selected");
      $(e.currentTarget).addClass("selected");
      emitFunc(val);
    });
    select2dom.on("change", function (e) {
      const val = $(e.currentTarget).val();
      container.find(".button").removeClass("selected");
      select2dom.siblings(".select2-container").addClass("selected");
      if (val) {
        emitFunc(val);
      }
    });
  }
};

七.卡点问题沟通,及实现沟通(跟datav新人群的工作人员的沟通过程):

datav技术人员@钉钉群21931738:

  • 不要用动态添加js以及全局的document对象来设置,用npm的方式引入
    • 以下逻辑本地datav run生效,datav package之后到datav屏中引入不生效
      • $("head").append(
        <link href = "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel = "stylesheet" />
        );
        let script = document.createElement("script");
        script.src =
        "cdn.jsdelivr.net/npm/select2…";
        document.head.append(script);
  • 问:index.js里的render是npm包加载完后执行的么?render方法是依赖都拉完之后执行的么?render是 = $(document).ready()的回调不??
    • 答:只要不是动态追加的js标签,都是引入完成之后执行的
    • 2.$(document).ready()回调里调用select2,dom元素只要在select2执行前已经绘制应该就没有问题
    • 3.容器内不可操作容器外的dom,比如容器内操作document
  • 问:render方法里写select2的调用***.select2()方法不生效的问题如何解决?select2官网是说,要在$(document).ready()回调里面执行:***.select2
    • 答:这个应该只是参考把。dom元素只要在他执行前已经绘制应该就没有问题把
  • 问:组件包上传报错
    • 答:检查一下package是否定义了相应的依赖包 看看network请求或者dom元素 对应的js是否有加载。
    • 答:上传之后 等几分钟在添加看看 打包需要点时间 并且需要强刷下浏览器
    • 答:降低点版本把 版本太高了 可能会打包失败