Разработайте визуальный редактор H5 с перетаскиванием (React) от 0 до 1.

React.js

Демонстрация видео станции B~

код гитхаба

1614245026361

Годы и годы спустя я был относительно бездействующим, поэтому я использовал React для создания простой платформы с низким кодом с функциями, показанными на анимации выше. Далее мы представим завершенные функциональные точки, в основном в том числе:

  1. редактор
  2. государственное управление
  3. Компоненты, сгенерированные пользователем (в настоящее время полный текст, кнопка, компоненты изображения)
  4. тянуть
  5. Редактирование свойств компонента
  6. увеличить, уменьшить
  7. Удалить компоненты, настроить уровни слоев
  8. отменить повторить
  9. анимация
  10. Строитель

представлять

Платформа с низким кодом довольно распространена.В настоящее время в Интернете есть более зрелые и распространенные, такие как Rabbit Exhibition, Yiqixiu, Code Card, Map Driver и т. д. Однако для персонализированных настроек, таких как доступ к базе данных компании, многие компании также используют собственную платформу с низким кодом, например Ali, Baidu, Tencent и так далее. Платформа lowcode на самом деле генерирует страницы, перетаскивая или нажимая предопределенные компоненты.Многие сценарии приложений заключаются в том, что менеджеры по продуктам, эксплуатации и обслуживанию или продажам могут настроить создание страниц действий.Эта операция не очень хороша, потому что в ней больше нет необходимости.Куча Менеджеры по продукту, взаимодействие, дизайн и программисты должны встретиться, чтобы заполнить страницу активности. В течение этого периода, включая настройку полки и срока годности, программистам не нужно вмешиваться.

Если вы еще не сделали такой проект, то давайте посмотрим, как сделать такой проект.

Этот проект можно разделить на две части: одна — редактор, а другая — генератор. Редактор используется для создания страницы, фактически он генерирует значение объекта, которое содержит всю информацию о компонентах страницы, как показано ниже:

image-20210218115726982

Затем вы можете преобразовать этот объект в строку и сохранить ее в базе данных, соответствующую идентификатору.

Что должен сделать генератор, так это проанализировать соответствующую строку в соответствии с идентификатором, а затем преобразовать проанализированный объект в компонент для представления, который является страницей, которую мы создали в редакторе ранее.

структура данных компонента

Как видно из предыдущего рисунка, в данных холста есть массив cmps, в котором находятся все компоненты.

image-20210219152248091

  • Каждый компонент имеет случайно сгенерированный onlyKey в качестве уникального идентификатора, который можно использовать для удаления, поиска, обновления и т. д.

  • desc и type определяют тип компонента.Первый представляет собой описание китайского символа, которое можно использовать для отображения страницы, а последний в основном определяет тип компонента.

  • Значение определяется по-разному в разных компонентах, таких как текстовый компонент или кнопка для представления отображаемого текста и компонент изображения для записи адреса изображения.

  • style записывает стиль компонента

редактор

Во-первых, давайте взглянем на макет редактора, который можно разделить на четыре основных модуля:

image-20210225151516082

В это время код выглядит следующим образом:

export default function App() {
  return (
    <div id="app" className={styles.main}>
      {/* 模块1:组件选择区 */}
      <Cmps />
      {/* 模块2和模块4:画布模块和画布操作模块 */}
      <Content />
      {/* 模块3:画布属性操作模块 */}
      <Edit />
    </div>
  );
}

государственное управление

(Что касается управления состоянием, первая часть ниже — это мой процесс выбора. Если вы не хотите читать его, вы можете перейти к последнему абзацу, чтобы увидеть решение, которое я в итоге выбрал.)

После того, как мы узнали, какой тип редактора мы хотим создать, нам нужно решить следующую важную вещь: куда поместить данные холста? Прежде всего, необходимо знать, что при изменении данных холста должны обновляться и связанные с ними компоненты, то есть модуль 234 должен получать уведомление об изменении, что является так называемым управлением состоянием.

Что касается управления состоянием, я рассмотрел следующие варианты:

  1. Поместите данные холста в состояние приложения. Учитывая сложную логику изменения данных холста, useReducer можно использовать для компонентов функции приложения.
  2. Используйте избыточность для управления данными холста и используйте функцию редуктора для определения правил изменения данных холста, как на схеме 1.

В то же время, поскольку модуль 1234 и их подкомпоненты используются для использования данных холста, на этот раз следует рассмотреть передачу межуровневых данных, конечно, для этого может использоваться контекст, схема 12 может использовать контекст.

Когда я использую программу 1, потому что данные холста слишком велики, плюс множество операционных данных HALVAS, окончательный компонент приложений очень раздутый, я чувствую, что вид и слой данных являются липкими вместе, добавить к добавлению и углублению изменений Когда функция очень тяжелая, это не хорошо. Программа водного разряда 1.

Поскольку представление и данные схемы 1 слишком липкие, я переключился на редукцию в качестве третьей стороны для управления данными холста.Поначалу небольшая операция была очень хорошей, но поскольку она включает в себя изменение стиля, значения и т. д. в данных компонента уровень вложенности немного глубокий, и есть много функций добавления, удаления, изменения и проверки, которые необходимо использовать повторно, но если используется редюсер, в компоненте необходимо написать много логики модификации, но Я не хочу, чтобы компонент был слишком раздутым, разделенным на инструментальные функции и отделенным от представления и данных, что непросто поддерживать.

Итак, в конце концов, мне нужно хранилище данных для хранения данных моего холста, и это хранилище также предоставляет множество функций для добавления, удаления, изменения и проверки.

В конце концов, я выбрал третий вариант и сам определил класс Canvas для управления данными моего холста.

В этом классе я определяю следующие данные:

this.canvas: object

Сохраните все данные холста, а именно эти:

image-20210218115726982

Затем предоставьте некоторые функциональные функции, такие как getCanvas может получить данные this.canvas, которые в основном используются для отправки и публикации обновлений в базе данных, updateCanvas используется для обновления данных холста, emptyCanvas используется для очистки данных холста, а updateCanvasStyle используется для Обновите стиль холста.

this.listeners: array

Функция прослушивания. Если это.canvas изменилось, что я должен делать, на самом деле, это этот. CANVAS изменился, и все приложение обновляется. В это время мне нужно только добавить подписку в компоненте приложения:

export default function App() {
  const forceUpdate = useForceUpdate();

  // 所有组件
  const globalCanvas = useCanvas();

  useLayoutEffect(() => {
    const unsubscribe = globalCanvas.subscribe(() => {
      forceUpdate();
    });
    return () => {
      unsubscribe();
    };
  }, [globalCanvas, forceUpdate]);
  return (
    <div id="app" className={styles.main}>
      <CanvasContext.Provider value={globalCanvas}>
        <Cmps />
        <Content />
        <Edit />
      </CanvasContext.Provider>
    </div>
  );
}

Функция подписки в классе Canvas выглядит следующим образом:

  subscribe = (listener) => {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter((lis) => lis !== listener);
    };
  };

Многословно, хотя я всегда подчеркивал раньше, что подписка и отписка должны быть сопряжены.

Конечно, если вы понимаете принципы redux и Antd4 Form, то обнаружите, что логика здесь очень похожа на них.

this.selectedCmp:object

Запишите текущий выбранный компонент. Все данные компонента хранятся в this.canvas, но из-за глубокого уровня найти значение сложнее, поэтому каждый раз, когда вы обновляете выбранный компонент, при обновлении thia.canvas вы также можете обновлять это значение синхронно.

this.canvasChangeHistory: массив и this.canvasIndex:число

Первый записывает историю изменений холста, которая используется для отмены и повтора в верхнем модуле 2. Последний записывает, в какой истории изменений он находится в данный момент. Текущие данные холста записываются каждый раз при обновлении холста, например при обновлении компонентов, очистке холста и т. д. Функция для записи истории изменений данных холста выглядит следующим образом:

  recordCanvasChangeHistory = () => {
    this.canvasChangeHistory.push(this.canvas);
    this.canvasIndex = this.canvasChangeHistory.length - 1; //2;
  };

Приобретение таких манипулятивных функций

 // 返回画布数据的增删改查函数
  getCanvas = () => {
    const returnFuncs = [
      "getCanvasData",
      "recordCanvasChangeHistory",
      "goPrevCanvasHistory",
      "goNextCanvasHistory",
      "updateCanvas",
      "emptyCanvas",
      "getCanvasStyle",
      "updateCanvasStyle",
      "registerStoreChangeCmps",
      "registerCmpsEntity",
      "getCmp",
      "getCmps",
      "setCmps",
      "addCmp",
      "getSelectedCmp",
      "setSelectedCmp",
      "updateSelectedCmpStyle",
      "updateSelectedCmpValue",
      "deleteSelectedCmp",
      "changeCmpIndex",
      "subscribe",
    ];
    const obj = {};
    returnFuncs.forEach((func) => {
      obj[func] = this[func];
    });
    return obj;
  };

Передача данных между слоями

Класс Canvas создан, следующим шагом будет создание экземпляра этого класса, а затем передача его через Context.

export default function App() {
  const forceUpdate = useForceUpdate();

  // 所有组件
  const globalCanvas = useCanvas();

  useLayoutEffect(() => {
    const unsubscribe = globalCanvas.subscribe(() => {
      forceUpdate();
    });
    return () => {
      unsubscribe();
    };
  }, [globalCanvas, forceUpdate]);
  return (
    <div id="app" className={styles.main}>
      <CanvasContext.Provider value={globalCanvas}>
        <Cmps />
        <Content />
        <Edit />
      </CanvasContext.Provider>
    </div>
  );
}

Создайте пользовательский класс Canvas в useCanvas и верните метод getCanvas.

export function useCanvas(canvas) {
  const canvasRef = useRef();

  if (!canvasRef.current) {
    if (canvas) {
      canvasRef.current = canvas;
    } else {
      const globalCanvas = new Canvas();
      canvasRef.current = globalCanvas.getCanvas();
    }
  }
  return canvasRef.current;
}

пользовательские компоненты сборки

Модуль 1 является частью пользовательского компонента, который должен учитывать две вещи:

  1. Получите все пользовательские компоненты и начальные значения, массив выглядит следующим образом:

  1. Во-вторых, при добавлении компонентов на холст есть два пути:
    • Нажмите на новый компонент, положение по умолчанию — левый верхний угол холста, то есть и сверху, и слева — 0
    • Перетащите компонент на холст.В отличие от щелчка, при перетаскивании необходимо записать положение перетаскивания и назначить его атрибуту стиля нового компонента.

Код модуля 1 выглядит следующим образом:

export default function Cmps(props) {
  const globalCanvas = useContext(CanvasContext);

  const [list, setList] = useState(null);
  const handleDragStart = (e, cmp) => {
    if (cmp.data.type === isImgComponent) {
      return;
    }
    e.dataTransfer.setData("add-component", JSON.stringify(cmp));
  };

  const handleClick = (e, cmp) => {
    e.preventDefault();
    e.stopPropagation();
    if (
      cmp.data.type === isTextComponent ||
      cmp.data.type === isButtonComponent
    ) {
      globalCanvas.addCmp(cmp);
      return;
    }
    // 图片组件
    if (list) {
      setList(null);
    } else {
      let l = null;
      switch (cmp.data.type) {
        case isImgComponent:
          l = <Img baseCmp={cmp} />;
          break;
        default:
          l = null;
      }
      setList(l);
    }
  };

  return (
    <div id="cmps" className={styles.main}>
      <div className={styles.cmpList}>
        {menus.map((item) => (
          <div
            key={item.desc}
            className={styles.cmp}
            draggable={item.data.type !== isImgComponent}
            onDragStart={(e) => handleDragStart(e, item)}
            onClick={(e) => handleClick(e, item)}>
            {item.desc}
          </div>
        ))}
      </div>
      {list && (
        <button
          className={classnames("iconfont icon-close", styles.close)}
          onClick={() => setList(null)}></button>
      )}
      {list && <ul className={styles.detailList}> {list}</ul>}
    </div>
  );
}

Редактирование свойств компонента

新增组件2

После добавления компонента на холст этот компонент выбирается по умолчанию.В это время модуль редактирования справа должен отображать свойства компонента, и он доступен для редактирования.

Перетащите компоненты

Компоненты на холсте должны быть перетаскиваемыми, а положение контролируется перетаскиванием.В это время фактически получается расстояние перемещения по осям x и y, поэтому вам нужно только вычесть начальное значение position из этого положения. Кроме того, следует отметить, что, поскольку перетаскивание будет часто изменять данные холста, а также из-за ранее установленной функции мониторинга, компонент необходимо часто обновлять, но на самом деле нет необходимости обновлять компонент каждый раз, когда он перемещается. , его можно регулировать. Улучшите производительность, например, обновляя каждые 500 мс, код события выглядит следующим образом:

Запишите исходное положение:

handleDragStart = (e) => {
    this.setActive(e);
    let pageX = e.pageX;
    let pageY = e.pageY;
    e.dataTransfer.setData("startPos", JSON.stringify({pageX, pageY}));
  };

Событие перетаскивания на холсте.В это время необходимо решить, является ли это новым или существующим компонентом, который нужно перетащить и изменить.

  const handleDrop = (e) => {
    e.preventDefault();
    e.stopPropagation();

    // 新增的组件
    let addingCmp = e.dataTransfer.getData("add-component");

    if (addingCmp) {
      // 拖拽进来新增的组件
      addingCmp = JSON.parse(addingCmp);
      const top = e.pageY - canvasPos.top - 15;
      const left = e.pageX - canvasPos.left - 40;
      let resData = {
        ...addingCmp,
        data: {
          ...addingCmp.data,
          style: {
            ...addingCmp.data.style,
            top,
            left,
          },
        },
      };
      globalCanvas.addCmp(resData);
    } else {
      // 拖拽画布内的组件
      let startPos = e.dataTransfer.getData("startPos");
      startPos = JSON.parse(startPos);

      let disX = e.pageX - startPos.pageX;
      let disY = e.pageY - startPos.pageY;

      // 获取当前选中的组件的最新信息
      const selectedCmp = globalCanvas.getSelectedCmp();

      const top = selectedCmp.data.style.top + disY;
      const left = selectedCmp.data.style.left + disX;
      globalCanvas.updateSelectedCmpStyle({top, left});
    }
  };

увеличить, уменьшить

放大缩小

Компоненты на холсте также должны иметь возможность увеличивать и уменьшать масштаб в восьми направлениях.Как и при перетаскивании, вам нужно только записать расстояние перемещения мыши, а затем изменить ширину, высоту, верхнюю и левую сторону. обратите внимание на положение компонента, он позиционируется по верхнему и левому, поэтому вам не нужно модифицировать верхнее и левое при движении вниз, правое и нижнее правое, потому что координаты левого верхнего угла не изменились при на этот раз, и код события выглядит следующим образом:

  handleMouseDown = (e, direction) => {
    e.stopPropagation();
    e.preventDefault();

    const cmp = this.context.getCmp(this.props.index);

    let startX = e.pageX;
    let startY = e.pageY;

    const move = (e) => {
      let x = e.pageX;
      let y = e.pageY;

      let disX = x - startX;
      let disY = y - startY;
      let newStyle = {};

      if (direction) {
        if (direction.indexOf("top") >= 0) {
          disY = 0 - disY;
          newStyle.top = cmp.data.style.top - disY;
        }

        if (direction.indexOf("left") >= 0) {
          disX = 0 - disX;
          newStyle.left = cmp.data.style.left - disX;
        }
      }

      // 特别频繁改变,加上一个标记,
      debounce(
        this.context.updateSelectedCmpStyle(
          {
            ...newStyle,
            width: cmp.data.style.width + disX,
            height: cmp.data.style.height + disY,
          },
          "frequently"
        )
      );
    };

    const up = () => {
      document.removeEventListener("mousemove", move);
      document.removeEventListener("mouseup", up);
      this.context.recordCanvasChangeHistory();
    };

    document.addEventListener("mousemove", move);
    document.addEventListener("mouseup", up);
  };

вращать

Подобно перетаскиванию, вращающийся компонент фактически записывает расстояние между осями x и y движения мыши, затем вычисляет угол движения мыши и обновляет значение поворота преобразования компонента. код показывает, как показано ниже:

handleMouseDownofRotate = (e) => {
    e.stopPropagation();
    e.preventDefault();

    const {getCmp, updateSelectedCmpStyle} = this.context;

    const cmp = getCmp(this.props.index);

    let startX = e.pageX;
    let startY = e.pageY;

    const move = (e) => {
      let x = e.pageX;
      let y = e.pageY;

      let disX = x - startX;
      let disY = y - startY;

      const deg = (360 * Math.atan2(disY, disX)) / (2 * Math.PI);

      // 特别频繁改变,加上一个标记,
      debounce(
        updateSelectedCmpStyle(
          {
            transform: `rotate(${deg}deg)`,
          },
          "frequently"
        )
      );
    };

    const up = () => {
      document.removeEventListener("mousemove", move);
      document.removeEventListener("mouseup", up);
      this.context.recordCanvasChangeHistory();
    };

    document.addEventListener("mousemove", move);
    document.addEventListener("mouseup", up);
  };

контекстное меню

image-20210225173640073

Выберите компонент, щелкните правой кнопкой мыши, и должно появиться меню для отображения функций копирования, удаления, приклеивания к началу и отображения всех компонентов компонента. Отображение этого компонента оценивается по значению состояния.Выберите компонент, щелкните правой кнопкой мыши, чтобы отобразить его, и щелкните другие области, чтобы скрыть это меню:

        {showContextMenu && (
          <ContextMenu
            index={index}
            pos={{top: style.top - 80, left: style.left + 60}}
            cmp={cmp}
          />
        )}

копировать

Копировать — это фактически скопировать данные выбранного компонента, а затем добавить их.

  const copy = () => {
    globalCanvas.addCmp(cmp);
  };

В Canvas есть функция добавления компонентов, нужно отметить, что onlyKey нужно перегенерировать, а выбранный компонент обновится до вновь скопированного компонента:

  addCmp = (_cmp) => {
    this.selectedCmp = {
      ..._cmp,
      onlyKey: getOnlyKey(),
    };
    const cmps = this.getCmps();
    this.updateCmps([...cmps, this.selectedCmp]);
  };

удалять

Самый простой способ удаления - это найти данные компонента в cmps this.canvas по onlyKey текущего компонента.Не забудьте установить для this.seletedCmp значение null, так как область редактирования компонента отображается по this.seletedCmp, а затем обновите холст с помощью Edit the area component и все.

  // 点击组件,右键删除组件
  deleteSelectedCmp = (_cmp) => {
    this.setSelectedCmp(null);

    const cmps = this.getCmps();
    this.updateCmps(cmps.filter((cmp) => cmp.onlyKey !== _cmp.onlyKey));
  };

Верх и низ

Здесь иерархическое отношение всех компонентов контролируется z-index, а значение z-index является нижним индексом компонента в массиве cmps, поэтому для настройки иерархического отношения достаточно обновить порядок компонентов в массиве. Затем верхний должен поменять местами позицию последнего компонента и выбранного компонента в cmps, а нижний — поменять местами позицию 0-го компонента и выбранного компонента в cmps.

  const beTop = (e) => {
    globalCanvas.changeCmpIndex(index);
  };

  const beBottom = (e) => {
    globalCanvas.changeCmpIndex(index, 0);
  };

Показать все компоненты

展示所有

Другая функция контекстного меню — отображение всех компонентов, потому что, когда компонентов слишком много, некоторые компоненты будут закрыты, но закрытые компоненты нельзя будет выбрать на холсте.В это время вы можете просмотреть все компоненты через контекстное меню Компонент, если навести указатель мыши, будет отображен соответствующий компонент, а если щелкнуть его, отобразится выбранная функция. Код события выглядит следующим образом:

  const cmps = globalCanvas.getCmps();

  const mouseOver = (e, _cmp) => {
    let cmpTarget = document.getElementById("cmp" + _cmp.onlyKey);
    let prevClassName = cmpTarget.className;
    if (prevClassName.indexOf("hover") === -1) {
      cmpTarget.setAttribute("class", prevClassName + " hover");
    }
  };

  const mouseLeave = (e, _cmp) => {
    let cmpTarget = document.getElementById("cmp" + _cmp.onlyKey);
    let prevClassName = cmpTarget.className;

    if (prevClassName.indexOf("hover") > -1) {
      cmpTarget.setAttribute("class", prevClassName.slice(0, -6));
    }
  };

  const selectCmp = (e, cmp) => {
    globalCanvas.setSelectedCmp(cmp);
  };

предыдущий шаг следующий шаг

По сути, это машина времени, если вы хотите вернуться в какой-то момент, то вам нужно записать свою историю изменений. На этот раз требуется два значения this.canvasChangeHistory и this.canvasIndex. При изменении данных холста и данных компонентов выполните функцию this.recordCanvasChangeHistory() для записи истории. Щелкните предыдущий шаг, чтобы получить предыдущее значение this.canvasIndex в this.canvasChangeHistory, и следующий шаг, чтобы получить следующее значение. Просто обратите внимание на 0-е и последнее граничные значения теста.

анимация

动画

Компонент может добавлять некоторые свойства анимации.Здесь три анимации копируются из выставки кроликов, и можно изменить одиночную продолжительность, повторения и время задержки анимации.

Выберите шаблон

模板

Слишком медленно каждый раз создавать с 0, можно сделать несколько предустановленных шаблонов, а потом заполнить холст данными.

export default function Tpl({openOrCloseTpl, globalCanvas}) {
  const updateCmps = (cmps) => {
    globalCanvas.updateCanvas(JSON.parse(cmps));
    openOrCloseTpl(false);
  };

  return (
    <ul className={styles.main}>
      <li className={styles.close} onClick={openOrCloseTpl}>
        <i className="iconfont icon-close"></i>
      </li>
      {tplData.map((item) => (
        <li
          key={item.id}
          className={styles.item}
          onClick={() => updateCmps(item.cmps)}>
          <div className={styles.name}>{item.name}</div>
          <div className={styles.thumbnail}>
            <img src={item.img} />
          </div>
        </li>
      ))}
    </ul>
  );
}

Строитель

После того, как редактор будет готов, вы можете вынуть часть холста и создать проект генератора.

function App() {
  const [canvas, setCanvas] = useState(null);

  const {cmps, style} = canvas || {};

  useEffect(() => {
    let cc = JSON.parse(
      // ! 元宵节
            '{"style":{"width":320,"height":568,"backgroundColor":"#fc0000ff","backgroundImage":"https://img.tusij.com/ips_asset/16/11/10/44/54/a5/a57d2950001941a5e65fc3ac73fe8cb8.png!l800_i_w?auth_key=1639324800-0-0-d94f8946bfa0f7eca8fc8094a1516003","backgroundPosition":"center","backgroundSize":"cover","backgroundRepeat":"no-repeat","boxSizing":"content-box"},"cmps":[{"desc":"图片","data":{"type":2,"value":"https://img.tusij.com/ips_asset/16/11/10/44/54/a5/a57d2950001941a5e65fc3ac73fe8cb8.png!l800_i_w?auth_key=1639324800-0-0-d94f8946bfa0f7eca8fc8094a1516003","style":{"top":-1,"left":-1,"width":321,"height":153,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff"}},"onlyKey":0.27364639468523455},{"desc":"图片","data":{"type":2,"value":"https://img.tusij.com/ips_asset/16/11/10/44/54/a5/a57d2950001941a5e65fc3ac73fe8cb8.png!l800_i_w?auth_key=1639324800-0-0-d94f8946bfa0f7eca8fc8094a1516003","style":{"top":155,"left":-3,"width":321,"height":153,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff"}},"onlyKey":0.7545885469950053},{"desc":"图片","data":{"type":2,"value":"https://img.tusij.com/ips_asset/16/11/10/44/54/a5/a57d2950001941a5e65fc3ac73fe8cb8.png!l800_i_w?auth_key=1639324800-0-0-d94f8946bfa0f7eca8fc8094a1516003","style":{"top":420,"left":-3,"width":321,"height":153,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff"}},"onlyKey":0.7590306166672274},{"desc":"图片","data":{"type":2,"value":"https://img.tusij.com/ips_asset/16/11/10/44/53/ca/ca7ebd1a9683109e61f374e75e87fc85.png!l800_i_w?auth_key=1639324800-0-0-04d5239353f80379a2430dc74d1ac11a","style":{"top":18,"left":211,"width":89,"height":81,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff"}},"onlyKey":0.14191580299167428},{"desc":"图片","data":{"type":2,"value":"https://img.tusij.com/ips_asset/16/11/10/44/54/70/70913bd41742596a4a0dd68b088e6551.png!l800_i_w?auth_key=1639324800-0-0-2a8cd9567a9d2a9aa2ddd8acc4a24450","style":{"top":460,"left":0,"width":320,"height":110,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff"}},"onlyKey":0.5399342806341869},{"desc":"图片","data":{"type":2,"value":"https://img.tusij.com/ips_asset/16/11/10/44/53/9a/9a353760e02b49cbdd2706f5c452291b.png!l800_i_w?auth_key=1639324800-0-0-8825104eb9f4bd5ca42b9ff8c3690c9c","style":{"top":403,"left":10,"width":121,"height":50,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff"}},"onlyKey":0.27065004352847866},{"desc":"图片","data":{"type":2,"value":"https://img.tusij.com/ips_asset/16/11/10/44/53/69/6917ec339fa98e4cb97cf596cc9179df.png!l800_i_w?auth_key=1639324800-0-0-31958bfca526c4f4f87f4363b8b16b61","style":{"top":461,"left":28,"width":97,"height":49,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff"}},"onlyKey":0.3396974553981347},{"desc":"图片","data":{"type":2,"value":"https://img.tusij.com/ips_asset/16/11/10/44/53/e7/e722646ec5596c852c8b193b2ef09db9.png!l800_i_w?auth_key=1639324800-0-0-0e5dcd8e08ad1e7f0de72c2dad23419c","style":{"top":439,"left":158,"width":100,"height":47,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff"}},"onlyKey":0.02766075271613433},{"desc":"图片","data":{"type":2,"value":"https://img.tusij.com/ips_asset/16/11/10/44/54/09/09917bf7e35711c91d353fd7aebf2a38.png!l800_i_w?auth_key=1639324800-0-0-bd838424e74c24b3f0787ae4c4fb11d6","style":{"top":215,"left":116,"width":114,"height":154,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff"}},"onlyKey":0.6929555070607207},{"desc":"图片","data":{"type":2,"value":"https://tva1.sinaimg.cn/large/008eGmZEly1gnqdhx1eprj303m03mjrm.jpg","style":{"top":388,"left":245,"width":41,"height":58,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff","animationName":"wobble","animationDelay":0,"animationDuration":"8","animationIterationCount":"infinite"}},"onlyKey":0.7708575276016363},{"desc":"图片","data":{"type":2,"value":"https://img.tusij.com/ips_asset/15/48/39/56/74/56/564896077cb72510ff3b920732d8c53c.png!l800_i_w?auth_key=1639152000-0-0-456d31b72cda757ae3945425296bd646","style":{"top":173,"left":248,"width":51,"height":58,"borderRadius":"0%","borderStyle":"none","borderWidth":"0","borderColor":"#fff"}},"onlyKey":0.540328523257599}]}'
    );
    setCanvas(cc);
  }, []);

  return canvas ? (
    <div
      className={styles.main}
      style={{
        ...formatStyle(style),
        backgroundImage: `url(${style.backgroundImage})`,
      }}>
      {cmps.map((cmp, index) => (
        <Draggable
          key={cmp.onlyKey}
          cmp={cmp}
          index={index}
          canvasWidth={style.width}
          canvasHeight={style.height}
        />
      ))}
    </div>
  ) : (
    <div>
      <i className="iconfont icon-loading"></i>
    </div>
  );
}

закончил~


Не забудьте поставить лайк статье. Также приглашаем вас обратить внимание на публичный аккаунт [Front of Huaguo Mountain] и мою станцию ​​B. В будущем будет загружено больше оригинальных видео.