【可视化搭建平台 | 店铺装修】魔方组件的设计与实现

1. 概述

魔方组件是店铺装修平台的常见组件,旨在帮助商家展示商品和活动海报,支持多样化布局选择和自定义模板。

开发说明:通过扩展阿里低代码引擎 提供的示例项目,扩展魔方组件物料和魔方组件设置器。

2. 效果演示

3. 功能特点

  1. 多样化的布局选择,包括一行两个、一行三个、一行四个、两左两右、一左两右、一上二下、一左三右和自定义模板。
  1. 支持上传商品图片和活动海报,并为图片添加链接。
  2. 可调整图片间隙和尺寸要求,适配不同的页面布局和设备。
  3. 自定义模板支持移动鼠标选定布局区域大小,满足商家的个性化需求。

4. 数据格式

ts 复制代码
{
    "model": "custom",
    "row": 5,
    "col": 5,
    "list": [
        {
            "y": 0,
            "x": 0,
            "height": 3,
            "width": 3,
            "image": "http://localhost:3006/1710684266282.jpg",
            "targetUrl": ""
        },
        {
            "y": 3,
            "x": 0,
            "height": 2,
            "width": 3,
            "image": "http://localhost:3006/1704280623065.jpeg",
            "targetUrl": ""
        },
        {
            "y": 0,
            "x": 3,
            "height": 3,
            "width": 2,
            "image": "http://localhost:3006/1704280618340.jpeg",
            "targetUrl": ""
        },
        {
            "y": 3,
            "x": 3,
            "height": 2,
            "width": 2,
            "image": "http://localhost:3006/1704280628992.png",
            "targetUrl": ""
        }
    ]
}

cube (object): 魔方配置对象,包含以下属性:

  • model (string): 模板类型
  • row (number): 魔方的行数。
  • col (number): 魔方的列数。
  • list (array): 魔方的数据数组,每个元素包含以下属性:
    • x (number): 小块的横向位置。
    • y (number): 小块的纵向位置。
    • width (number): 小块的宽度占比。
    • height (number): 小块的高度占比。
    • image (string): 小块的背景图片 URL。
    • targetUrl (string, optional): 点击小块时跳转的目标 URL。

5. 思路

多样化的布局选择,包括一行两个、一行三个、一行四个、两左两右、一左两右、一上二下、一左三右和自定义模板。

一行两个、一行三个、一行四个、两左两右、一左两右、一上二下、一左三右 都可以从自定义模版中演化而来。

5.1. 自定义模板

首先介绍自定义模板的设计思路(以魔方密度 6×6 为例):

  1. 把整个魔方布局区域看成一个直角坐标系区域
  2. 把一个区域划分为 n×n 等份,比如这里 n=6
  1. 外层容器绝对定位,内层每一块区域相对定位

  2. 每一个区域由 x, y 定位到位置,width, height 分别决定区域的宽高

    1. 转换成样式,x->left,y->top,width->width,height->height

5.2. 数据格式

ts 复制代码
export interface Model {
  x: number;
  y: number;
  width: number;
  height: number;
  image: string;
}

export interface InitialModels {
  [key: string]: Model[];
}

export const initialModels: InitialModels = {
  magicCube1: [
    {
      x: 0,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 1,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    }
  ],
  magicCube2: [
    {
      x: 0,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 1,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 2,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    }
  ]
};

export interface ModelOption {
  label: string;
  value: string;
  row: number;
  col: number;
}

export const modelOptions: ModelOption[] = [
  {
    label: "一行两个",
    value: "magicCube1",
    row: 1,
    col: 2
  },
  {
    label: "一行三个",
    value: "magicCube2",
    row: 1,
    col: 3
  },
  {
    label: "自定义",
    value: "custom",
    row: 5,
    col: 5
  }
];

5.3. 一行两个

比如一行两个可以看作是一行两列(row: 1, col: 2)的自定义区域

ts 复制代码
{
    "model": "magicCube1",
    "row": 1,
    "col": 2,
    "list": [
        {
            "x": 0,
            "y": 0,
            "height": 1,
            "width": 1,
            "image": ""
        },
        {
            "x": 1,
            "y": 0,
            "height": 1,
            "width": 1,
            "image": ""
        }
    ]
}

6. 运行态

6.1. 代码实现

tsx 复制代码
import * as React from 'react';
import { createElement } from 'react';
import './index.scss';

export interface MagicCubeProps {
  isDesigner: boolean;
  isPreview: boolean;
  attr: {
    cube?: {
      row?: number;
      col?: number;
      list?: {
        x: number;
        y: number;
        width: number;
        height: number;
        image: string;
        targetUrl?: string;
      }[];
    };
    imgMargin: number;
    imgRadius: number;
  };
}

const MagicCube: React.FC<MagicCubeProps> = ({ isDesigner, isPreview, attr }) => {
  const { cube = {}, imgMargin, imgRadius } = attr;
  const pagePadding = 0;

  // 获取容器的宽度
  const getContainerWidth = (): number => {
    return isDesigner && !isPreview ? 375 : globalThis.innerWidth;
  };

  // 获取每一列的宽度
  const getItemWidth = (): number => {
    const width = getContainerWidth() - pagePadding * 2;
    return width / cube?.col;
  };

  // 获取每一行的宽度
  const getItemHeight = (): number => {
    return getContainerWidth() / cube?.col;
  };

  // 计算得到容器的高度
  const getWrapHeight = (): number => {
    return cube?.row * getItemHeight();
  };

  const getWrapStyle = (): React.CSSProperties => {
    let result: React.CSSProperties = {};

    if (cube?.list.length > 0) {
      result.height = getWrapHeight() + 'px';
    } else {
      result.backgroundSize = '100% 100%';
      result.height = '190px';
    }

    return result;
  };

  const getMainStyle = (styles: { [key: string]: number }): React.CSSProperties => {
    const { x, y, width, height } = styles;
    const result: React.CSSProperties = {
      left: x * getItemWidth(),
      top: y * getItemHeight(),
      width: width * getItemWidth(),
      height: height * getItemHeight(),
      padding: imgMargin / 2,
    };
    return result;
  };
 
  const getItemStyle = (img: string): React.CSSProperties => {
    return {
      backgroundImage: `url(${img})`,
      borderRadius: imgRadius + 'px',
    };
  };

  // const handleClick = (url: string) => {
  // };

  return (
    <div className="magic-cube" style={getWrapStyle()}>
      <div className="cube-wrap">
        {cube?.list.map((item, index) => (
          <div
            key={index}
            className="absolute cube-item"
            style={getMainStyle(item)}
          // onClick={() => handleClick(item.targetUrl)}
          >
            <div className="cube-item-wrap" style={getItemStyle(item.image)}></div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default React.memo(MagicCube);

6.2. 代码说明

  1. 在组件内部,通过解构赋值获取 cube、imgMargin 和 imgRadius。
  2. 定义一系列辅助函数,如 getContainerWidth、getItemWidth、getItemHeight 等,用于计算容器和元素的尺寸、位置等样式属性。
  3. getWrapStyle 函数根据 cube.list 中的元素数量来设置容器高度,并设置背景样式或默认高度。
  4. getMainStyle 函数根据传入的样式参数计算并返回每个元素的位置、尺寸等样式属性。
  5. getItemStyle 函数根据传入的图片 URL 返回对应的样式对象,设置背景图片和圆角等样式。
  6. 最后通过遍历渲染 cube.list 中的每个元素,并根据其样式设置位置、背景图片等。

7. 设置器

7.1. 功能说明

  1. 容器的行数和列数由 props 传入,通过下拉框设置魔方密度来实现。
  2. 点击某个空白方块会触发编辑模式,此时可以选择多个方块来创建一个新的容器块。会避免选择的方块与已存在的容器块重叠。
  3. 已有的容器块可以被点击,进入编辑状态,可以对容器块的位置和大小进行调整,并可以上传图片、添加链接等操作。
  4. 可以删除已有的容器块。

7.2. 代码实现

结构

objectivec 复制代码
magic-cube-setter
    | index.tsx        // 父组件 MagicCubeSetter
    | index.scss       // 样式
    | CustomLayout     // 子组件 CustomLayout
    | helper.ts        // 辅助常量、函数定义
    | defaultImage.png // 默认图片

7.2.1. magic-cube-setter/index.tsx -> 父组件 MagicCubeSetter

定义一个父组件 MagicCubeSetter,其主要功能包括:

  • 根据选择的模板进行布局设置,支持自定义布局和预设模板选择。
  • 在自定义布局模式下,可以调整魔方的密度(行数和列数)。
  • 提供一个自定义布局组件CustomLayout,用于展示和操作布局区域,并在布局区域中添加图片。
tsx 复制代码
import React, { useState, useRef, useEffect } from 'react';
import { Select } from '@alifd/next';
import { event } from '@alilc/lowcode-engine';
import { cloneDeep } from 'lodash';
import { useLatest } from 'ahooks';

import CustomLayout from './CustomLayout';
import { cubeRowsList, initialModels, modelOptions } from './helper';

import './index.scss';

interface CubeValue {
  list?: any[];
  row?: number;
  col?: number;
  model?: string;
}

interface MagicCubeSetterProps {
  type: string;
  name: string;
  initialValue?: CubeValue;
  defaultValue?: CubeValue;
  value: CubeValue,
  onChange: (val: object) => void;
}

const MagicCubeSetter: React.FC<MagicCubeSetterProps> = (props) => {
  const { value: cubeValue, initialValue: defaultValue, onChange } = props;
  const [activeItem, setActiveItem] = useState(0);
  const cubeValueRef = useLatest(cubeValue);
  const activeItemRef = useLatest(activeItem);
  const layoutRef = useRef<any>(null);

  useEffect(() => {
    if (cubeValue === undefined && defaultValue) {
      onChange(defaultValue);
    }

    const bindEvent = (value: string) => {
      console.log("common:magic-cube-setter.bindEvent-on", value);
      let newValue = cloneDeep(cubeValueRef.current);
      const currentIdx = activeItemRef.current;
      if (newValue?.list?.[currentIdx]) {
        newValue.list[currentIdx].image = value;
      }
      onChange(newValue);
    };

    event.on(`common:magic-cube-setter.bindEvent`, bindEvent);

    return () => {
      // setter 是以实例为单位的,每个 setter 注销的时候需要把事件也注销掉,避免事件池过多
      event.off(`common:magic-cube-setter.bindEvent`, bindEvent);
    }
  }, []);

  const changeModel = (model: string) => {
    if (model) {
      let target = modelOptions.find((m) => m.value === model);
      // 重置模板
      layoutRef.current?.reset();

      let newValue: CubeValue = {
        list: [],
        row: target?.row || 1,
        col: target?.col || 1,
        model,
      };
      // 设置模板对应初始数据
      if (model === 'custom') {
        newValue.list = [];
      } else {
        newValue.list = JSON.parse(JSON.stringify(initialModels[model]));
      }
      onChange(newValue);
    }
  };

  const handleChangeRow = (val: string) => {
    const value = parseInt(val || '5');
    const newValue: CubeValue = {
      model: 'custom',
      list: [],
      row: value,
      col: value,
    };
    onChange(newValue);
    layoutRef.current?.reset();
  };

  const onCurIndex = (item: number) => {
    setActiveItem(item);

    const activeImageUrl = cubeValueRef.current.list?.[item]?.['image'];
    event.emit('magic-cube-setter.changeSelectValue', activeImageUrl)
  };

  const onCustomChange = (newList: []) => {
    const { model, row, col } = cubeValueRef.current;
    const newValue: CubeValue = {
      model,
      row,
      col,
      list: newList
    };
    onChange(newValue);
  }

  return (
    <div className="magic-cube-setter">
      {cubeValue.model === 'custom' && (
      <div className="common">
        <label>魔方密度</label>
        <Select value={cubeValue.row} onChange={handleChangeRow}>
            {cubeRowsList.map((key) => (
              <Select.Option key={key} value={key}>
                {key}×{key}
              </Select.Option>
            ))}
          </Select>
        </div>
      )}

      {/* <div>魔方布局</div> */}
      <div className="custom-design-tips">
        {cubeValue.model === 'custom' ? '移动鼠标选定布局区域大小' : '选定布局区域,在下方添加图片'}
      </div>
      <CustomLayout
        ref={layoutRef}
        row={cubeValue.row || 1}
        col={cubeValue.col || 2}
        model={cubeValue.model || 'magicCube1'}
        list={cubeValue.list || []}
        onCurIndex={onCurIndex}
        onCustomChange={onCustomChange}
        {...props}
      />

      <div className="common">
        <label>模板选择</label>
        <Select value={cubeValue.model} onChange={(val) => changeModel(val)}>
          {modelOptions.map((option) => (
            <Select.Option key={option.value} value={option.value}>
              {option.label}
            </Select.Option>
          ))}
        </Select>
      </div>
    </div>
  );
};

export default MagicCubeSetter;

7.2.2. magic-cube-setter/CustomLayout.tsx -> 子组件 CustomLayout

定义一个子组件 CustomLayout,用于展示一个自定义布局的网格容器,并允许用户点击和移动来选择和编辑其中的块。其主要功能包括:

  1. 渲染网格容器:根据 row 和 col 的值,生成相应数量的 ul 和 li 元素,形成一个网格布局。
  2. 处理点击事件:当用户点击某个网格块时,根据当前的编辑状态(edit)来执行不同的操作。如果当前不处于编辑状态,则记录点击的块的 key 值,并进入编辑状态。如果当前处于编辑状态,则根据记录的起始 key 值和结束 key 值,创建一个新的块,并将其添加到 list 数组中。
  3. 处理移动事件:当用户在编辑状态下移动鼠标时,根据起始 key 值和当前鼠标所在的块的 key 值,计算出需要更新的块的 key 值,并将其记录在 editKeys 数组中。
  4. 更新布局:根据 list 数组中的数据,渲染编辑容器块。用户可以通过点击和选择块来切换当前的编辑状态,并在编辑状态下删除选定的块。
tsx 复制代码
import React, { useState, useEffect, useImperativeHandle, ForwardRefRenderFunction, forwardRef, Ref, MouseEvent } from 'react';
import { cloneDeep, sortBy } from 'lodash';

import { cubeWrapWidth, customLayoutWidth, isRectangleOverlap } from './helper';
import defaultImage from './defaultImage.png';

interface CustomLayoutProps {
  list: any[];
  model: string;
  row: number;
  col: number;
  onCurIndex: (index: number) => void;
  onCustomChange: (val: []) => void;
  selectedNodeId: number;
  ref?: React.Ref<HTMLDivElement>;
}

interface CustomDivRef extends HTMLDivElement {
  reset: () => void;
}

interface SplitKey {
  y: number;
  x: number;
}

const CustomLayout: ForwardRefRenderFunction<CustomDivRef, CustomLayoutProps> = forwardRef<HTMLDivElement, CustomLayoutProps>(
  (props: CustomLayoutProps, ref: Ref<HTMLDivElement>) => {
    const { list, model, row, col, onCurIndex, onCustomChange } = props;
    const [startKey, setStartKey] = useState<number>(0);
    const [curIndex, setCurIndex] = useState<number>(-1);
    const [edit, setEdit] = useState<boolean>(false);
    const [ys, setYs] = useState<number[]>([]);
    const [xs, setXs] = useState<number[]>([]);
    const [editKeys, setEditKeys] = useState<number[]>([]);

    const getBaseW = (): number => {
      return parseInt(customLayoutWidth / col);
    };

    useEffect(() => {
      setTimeout(() => {
        updateCurIndex(0);
      }, 500);
    }, []);


    useEffect(() => {
      setYs([...Array(row).keys()]);
      setXs([...Array(col).keys()]);
    }, [row, col]);

    // 将 reset 方法暴露给父组件
    useImperativeHandle(ref, () => ({
      reset
    }));

    const updateCurIndex = (index: number) => {
      setCurIndex(index);
      onCurIndex(index);
    };

    const updateList = (updatedValue: number[]) => {
      onCustomChange(updatedValue);
    };

    const reset = () => {
      setStartKey(0);
      updateCurIndex(-1);
      setEdit(false);
      setEditKeys([]);
    };

    const clickWrap = (e: MouseEvent<HTMLDivElement>) => {
      if (!edit) {
        const key = Number((e.target as HTMLDivElement).dataset.key);
        setEditKeys([...editKeys, key]);
        setStartKey(key);
        setEdit(true);
      } else {
        let keys = cloneDeep(sortBy(editKeys));
        const start = splitKey(keys[0]);
        const end = splitKey(keys.pop());

        const temp = {
          x: start.x,
          y: start.y,
          height: end.y - start.y + 1,
          width: end.x - start.x + 1,
          image: defaultImage,
          targetUrl: ''
        };

        const updatedValue = [...list, temp];
        onCustomChange(updatedValue);
        updateCurIndex(updatedValue.length - 1);
        setEditKeys([]);
        setEdit(false);
      }
    };

    const move = (e: MouseEvent<HTMLDivElement>) => {
      if (!edit) {
        return;
      }

      const keys = [];
      const start = splitKey(startKey);
      const end = splitKey(Number((e.target as HTMLDivElement).dataset.key));
      const ys = sortBy([start.y, end.y]);
      const xs = sortBy([start.x, end.x]);

      if (antiCollision(start, end)) {
        return;
      }

      for (let i = ys[0]; i <= ys[1]; i++) {
        for (let j = xs[0]; j <= xs[1]; j++) {
          keys.push(mergeKey(i, j));
        }
      }

      setEditKeys(keys);
    };

    const antiCollision = (start: { x: number, y: number }, end: { x: number, y: number }) => {
      const rec1 = [start.x, start.y, end.x, end.y];
      for (let i = 0; i < list.length; i++) {
        const item = list[i];
        const rec2 = [item.x, item.y, item.x + item.width, item.y + item.height];
        const isRectangleOverlapRes = isRectangleOverlap(rec1, rec2);
        if (isRectangleOverlapRes) {
          return true;
        }
      }
      return false;
    };

    const mergeKey = (y: number, x: number) => {
      return Number(x + (y * 10));
    };

    const splitKey = (key: number) => {
      if (key >= 10) {
        return { y: parseInt((key % 100) / 10), x: key % 10 };
      } else {
        return { y: 0, x: Number(key) };
      }
    };

    const getWidth = () => {
      return parseInt(cubeWrapWidth / col);
    };

    const getStyle = (style) => {
      const { x, y, width, height } = style;
      const result = {
        left: `${x * getWidth() - 1}px`,
        top: `${y * getWidth() - 1}px`,
        width: `${width * getWidth() + 1}px`,
        height: `${height * getWidth() + 1}px`,
      };
      return result;
    };

    const deleteEditWrap = (index: number) => {
      const updatedValue = [...list];
      updatedValue.splice(index, 1);
      updateList(updatedValue);
      updateCurIndex(updatedValue.length - 1);
    };


    return (
      <div className="custom-layout" style={{ width: `${cubeWrapWidth}px` }}>
        {ys.map((y) => (
          <ul key={y} className="custom-layout-ul">
            {xs.map((x) => {
              const key = mergeKey(y, x);
              const dataKey = key.toString();
              const dataY = y.toString();
              const dataX = x.toString();
              const isActive = editKeys.includes(key);

              const width = getWidth();

              return (
                <li
                  key={key}
                  data-key={dataKey}
                  data-y={dataY}
                  data-x={dataX}
                  style={{
                    width,
                    height: width,
                    textAlign: 'center'
                  }}
                  className={`wrap-item flex-center ${isActive ? 'move-wrap' : ''}`}
                  onClick={clickWrap}
                  onMouseOver={move}
                >
                  <i style={{ lineHeight: `${width}px` }} className={`gscm-designer-font icon-jia1`} />
                </li>
              );
            })}
          </ul>
        ))}

        {/* 编辑容器块 */}
        {list.map((item, index) => {
          const isActive = curIndex === index;
          const style = getStyle(item);
          const isImageEmpty = item.image === defaultImage || !item.image;
          const backgroundImage = isImageEmpty ? 'none' : `url(${item.image})`;
          return (
            <div
              key={index}
              className={`edit-wrap flex-column flex-center ${isActive ? 'edit-wrap-active' : ''}`}
              style={style}
              onClick={() => updateCurIndex(index)}
            >
              {model === 'custom' && (
                <div className="edit-wrap-close" onClick={() => deleteEditWrap(index)}>
                  <i className="gscm-designer-font icon-guanbi"></i>
                </div>
              )}
              <div className='edit-warp-text' style={{ backgroundImage }}>
                {isImageEmpty && <div>{`${parseInt(item.width * getBaseW())}x${parseInt(item.height * getBaseW())}`}</div>}
                {/* {item.width > 1 && <div>或同等比例</div>} */}
              </div>
            </div>
          );
        })}
      </div>
    );
  }
);

export default CustomLayout;

7.2.3. magic-cube-setter/helper.ts

  1. cubeRowsList: 这是一个包含数字的数组,表示了一些魔方的行数。
  2. cubeWrapWidth: 表示魔方的包裹宽度。
  3. customLayoutWidth: 表示自定义布局的宽度。
  4. Model 接口定义了一个模型对象的结构,包括 x、y 坐标、宽度、高度和图片路径。
  5. InitialModels 接口定义了一个初始模型对象的结构,是一个 key 值为字符串,值为 Model 数组的对象。
  6. initialModels: 是一个包含多个魔方初始模型的对象,每个魔方有不同数量的模型组成。
  7. ModelOption 接口定义了一个模型选项的结构,包括标签、值、行数和列数等信息。
  8. modelOptions: 是一个包含多个模型选项的数组,用于描述不同类型的模型布局选项。
  9. rectangleFormat 函数用于格式化矩形的坐标信息,确保左上角坐标值小于右下角坐标值。根据 temp 参数的设置,可以返回不同的格式化结果。
  10. isRectangleOverlap 函数用于判断两个矩形是否重叠,内部调用了 rectangleFormat 函数来格式化矩形坐标信息,并进行比较判断是否重叠。
ts 复制代码
import { sortBy } from 'lodash';

import defaultImage from './defaultImage.png';

export const cubeRowsList: number[] = [4, 5, 6, 7];

export const cubeWrapWidth: number = 220;

export const customLayoutWidth: number = 750;

export interface Model {
  x: number;
  y: number;
  width: number;
  height: number;
  image: string;
}

export interface InitialModels {
  [key: string]: Model[];
}

export const initialModels: InitialModels = {
  magicCube1: [
    {
      x: 0,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 1,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    }
  ],
  magicCube2: [
    {
      x: 0,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 1,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 2,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    }
  ],
  magicCube3: [
    {
      x: 0,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 1,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 2,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 3,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    }
  ],
  magicCube4: [
    {
      x: 0,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 1,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 0,
      y: 1,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 1,
      y: 1,
      height: 1,
      width: 1,
      image: defaultImage
    }
  ],
  magicCube5: [
    {
      x: 0,
      y: 0,
      height: 2,
      width: 1,
      image: defaultImage
    },
    {
      x: 1,
      y: 0,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 1,
      y: 1,
      height: 1,
      width: 1,
      image: defaultImage
    }
  ],
  magicCube6: [
    {
      x: 0,
      y: 0,
      height: 1,
      width: 2,
      image: defaultImage
    },
    {
      x: 0,
      y: 1,
      height: 1,
      width: 1,
      image: defaultImage
    },
    {
      x: 1,
      y: 1,
      height: 1,
      width: 1,
      image: defaultImage
    }
  ],
  magicCube7: [
    {
      x: 0,
      y: 0,
      height: 4,
      width: 2,
      image: defaultImage
    },
    {
      x: 2,
      y: 0,
      height: 2,
      width: 2,
      image: defaultImage
    },
    {
      x: 2,
      y: 2,
      height: 2,
      width: 1,
      image: defaultImage
    },
    {
      x: 3,
      y: 2,
      height: 2,
      width: 1,
      image: defaultImage
    }
  ]
};

export interface ModelOption {
  label: string;
  value: string;
  row: number;
  col: number;
}

export const modelOptions: ModelOption[] = [
  {
    label: "一行两个",
    value: "magicCube1",
    row: 1,
    col: 2
  },
  {
    label: "一行三个",
    value: "magicCube2",
    row: 1,
    col: 3
  },
  {
    label: "一行四个",
    value: "magicCube3",
    row: 1,
    col: 4
  },
  {
    label: "两左两右",
    value: "magicCube4",
    row: 2,
    col: 2
  },
  {
    label: "一左两右",
    value: "magicCube5",
    row: 2,
    col: 2
  },
  {
    label: "一上二下",
    value: "magicCube6",
    row: 2,
    col: 2
  },
  {
    label: "一左三右",
    value: "magicCube7",
    row: 4,
    col: 4
  },
  {
    label: "自定义",
    value: "custom",
    row: 5,
    col: 5
  }
];

const rectangleFormat = (rec, temp) => {
  const xs = sortBy([rec[0], rec[2]]);
  const ys = sortBy([rec[1], rec[3]]);
  if (temp) {
    return [xs[0], ys[0], xs[1] + 1, ys[1] + 1];
  } else {
    return [xs[0], ys[0], xs[1], ys[1]];
  }
};

export const isRectangleOverlap = function (rec1, rec2) {
  const rectangle1 = rectangleFormat(rec1, true);
  const rectangle2 = rectangleFormat(rec2);
  return rectangle2[0] < rectangle1[2] && rectangle2[1] < rectangle1[3] && rectangle2[2] > rectangle1[0] && rectangle2[3] > rectangle1[1];
};

8. 小结

魔方组件基本可以满足商家对于产品展示和活动海报的灵活需求。它具有以下特点:

  1. 提供多样化的布局选择,包括预设模板和自定义模板。
  2. 支持上传商品图片和活动海报,并为图片添加链接。
  3. 可调整图片间隙和尺寸要求,适配不同的页面布局和设备。
  4. 自定义模板支持移动鼠标选定布局区域大小,满足商家的个性化需求。
  5. 设置器组件用于配置魔方的布局和属性。
    • 可以选择不同的模板和布局密度,同时支持自定义布局。
    • 在布局区域中,用户可以选择图片并进行编辑操作,以满足不同的展示需求。
相关推荐
高山我梦口香糖17 分钟前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_7482352420 分钟前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240251 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar1 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人2 小时前
前端知识补充—CSS
前端·css
GISer_Jing2 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245522 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v2 小时前
webpack最基础的配置
前端·webpack·node.js
pubuzhixing2 小时前
开源白板新方案:Plait 同时支持 Angular 和 React 啦!
前端·开源·github
2401_857600953 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js