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

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. 设置器组件用于配置魔方的布局和属性。
    • 可以选择不同的模板和布局密度,同时支持自定义布局。
    • 在布局区域中,用户可以选择图片并进行编辑操作,以满足不同的展示需求。
相关推荐
程序猿阿伟10 分钟前
《Flutter社交应用暗黑奥秘:模式适配与色彩的艺术》
前端·flutter
rafael(一只小鱼)14 分钟前
黑马点评实战笔记
前端·firefox
weifont14 分钟前
React中的useSyncExternalStore使用
前端·javascript·react.js
初遇你时动了情19 分钟前
js fetch流式请求 AI动态生成文本,实现逐字生成渲染效果
前端·javascript·react.js
影子信息33 分钟前
css 点击后改变样式
前端·css
几何心凉1 小时前
如何使用 React Hooks 替代类组件的生命周期方法?
前端·javascript·react.js
小堃学编程1 小时前
前端学习(1)—— 使用HTML编写一个简单的个人简历展示页面
前端·javascript·html
hnlucky2 小时前
通俗易懂版知识点:Keepalived + LVS + Web + NFS 高可用集群到底是干什么的?
linux·前端·学习·github·web·可用性测试·lvs
懒羊羊我小弟2 小时前
使用 ECharts GL 实现交互式 3D 饼图:技术解析与实践
前端·vue.js·3d·前端框架·echarts
前端小巷子2 小时前
CSS3 遮罩
前端·css·面试·css3