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 为例):
- 把整个魔方布局区域看成一个直角坐标系区域
- 把一个区域划分为 n×n 等份,比如这里 n=6
-
外层容器绝对定位,内层每一块区域相对定位
-
每一个区域由 x, y 定位到位置,width, height 分别决定区域的宽高
- 转换成样式,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. 代码说明
- 在组件内部,通过解构赋值获取 cube、imgMargin 和 imgRadius。
- 定义一系列辅助函数,如 getContainerWidth、getItemWidth、getItemHeight 等,用于计算容器和元素的尺寸、位置等样式属性。
- getWrapStyle 函数根据 cube.list 中的元素数量来设置容器高度,并设置背景样式或默认高度。
- getMainStyle 函数根据传入的样式参数计算并返回每个元素的位置、尺寸等样式属性。
- getItemStyle 函数根据传入的图片 URL 返回对应的样式对象,设置背景图片和圆角等样式。
- 最后通过遍历渲染 cube.list 中的每个元素,并根据其样式设置位置、背景图片等。
7. 设置器
7.1. 功能说明
- 容器的行数和列数由 props 传入,通过下拉框设置魔方密度来实现。
- 点击某个空白方块会触发编辑模式,此时可以选择多个方块来创建一个新的容器块。会避免选择的方块与已存在的容器块重叠。
- 已有的容器块可以被点击,进入编辑状态,可以对容器块的位置和大小进行调整,并可以上传图片、添加链接等操作。
- 可以删除已有的容器块。
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,用于展示一个自定义布局的网格容器,并允许用户点击和移动来选择和编辑其中的块。其主要功能包括:
- 渲染网格容器:根据 row 和 col 的值,生成相应数量的 ul 和 li 元素,形成一个网格布局。
- 处理点击事件:当用户点击某个网格块时,根据当前的编辑状态(edit)来执行不同的操作。如果当前不处于编辑状态,则记录点击的块的 key 值,并进入编辑状态。如果当前处于编辑状态,则根据记录的起始 key 值和结束 key 值,创建一个新的块,并将其添加到 list 数组中。
- 处理移动事件:当用户在编辑状态下移动鼠标时,根据起始 key 值和当前鼠标所在的块的 key 值,计算出需要更新的块的 key 值,并将其记录在 editKeys 数组中。
- 更新布局:根据 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
- cubeRowsList: 这是一个包含数字的数组,表示了一些魔方的行数。
- cubeWrapWidth: 表示魔方的包裹宽度。
- customLayoutWidth: 表示自定义布局的宽度。
- Model 接口定义了一个模型对象的结构,包括 x、y 坐标、宽度、高度和图片路径。
- InitialModels 接口定义了一个初始模型对象的结构,是一个 key 值为字符串,值为 Model 数组的对象。
- initialModels: 是一个包含多个魔方初始模型的对象,每个魔方有不同数量的模型组成。
- ModelOption 接口定义了一个模型选项的结构,包括标签、值、行数和列数等信息。
- modelOptions: 是一个包含多个模型选项的数组,用于描述不同类型的模型布局选项。
- rectangleFormat 函数用于格式化矩形的坐标信息,确保左上角坐标值小于右下角坐标值。根据 temp 参数的设置,可以返回不同的格式化结果。
- 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. 小结
魔方组件基本可以满足商家对于产品展示和活动海报的灵活需求。它具有以下特点:
- 提供多样化的布局选择,包括预设模板和自定义模板。
- 支持上传商品图片和活动海报,并为图片添加链接。
- 可调整图片间隙和尺寸要求,适配不同的页面布局和设备。
- 自定义模板支持移动鼠标选定布局区域大小,满足商家的个性化需求。
- 设置器组件用于配置魔方的布局和属性。
- 可以选择不同的模板和布局密度,同时支持自定义布局。
- 在布局区域中,用户可以选择图片并进行编辑操作,以满足不同的展示需求。