最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 可视化拖拽页面编辑器 四

    正文概述 掘金(Miller)   2021-02-25   567

    一、搭建框架

    二、组件拖拽与渲染

    三、拖拽调整组件大小

    十、组件拖拽辅助线对齐

    可视化拖拽页面编辑器 四

    1、数据准备

    • 声明 VisualEditorMarkLine 作为辅助线的数据类型
    export interface VisualEditorMarkLine {
      x: { left: number; showLeft: number }[];
      y: { top: number; showTop: number }[];
    }
    
    • 先画一下辅助线的样式, 在container中添加
    <div class="mark-line-x" style={{ left: `200px` }}></div>
    <div class="mark-line-y" style={{ top: `200px` }}></div>
    

    样式

    	.mark-line-y {
              position: absolute;
              left: 0;
              right: 0;
              border-top: 1px dashed $primary;
            }
            .mark-line-x {
              position: absolute;
              top: 0;
              bottom: 0;
              border-left: 1px dashed $primary;
            }
    

    2.缓存当前选中的block

    • 定义存储对象
        const state = reactive({
          selectBlock: null as null | VisualEditorBlockData,
        });
    
    
    • 在block的 onMousedown监听事件中给 selectBlock赋值
    • containeronMousedown监听事件中将 selectBlock置为空

    3.辅助线显示逻辑

    • blockDragger内定义mark对象,并返回给外部使用
    • 在拖动block时,匹配到对应坐标,就赋值给mark显示对应的对齐辅助线
        // 用于视图展示的辅助线
          const mark = reactive({
            x: null as null | number,
            y: null as null | number,
          });
          
        // 视图: 如果mark的x坐标有值,就显示垂直辅助线,如果y坐标有值,就显示水平辅助线
    	    {blockDragger.mark.x && (
                    <div
                      class="mark-line-x"
                      style={{ left: `${blockDragger.mark.x}px` }}
                    ></div>
                  )}
                  {blockDragger.mark.y && (
                    <div
                      class="mark-line-y"
                      style={{ top: `${blockDragger.mark.y}px` }}
                    ></div>
                  )}
    
    
    • 当选中block后 mousedown事件,在dragState中记住初始位置,计算出选中block与所有未选中block,左右、上下对齐的各种情况的坐标位置,并进行缓存。
      • 上下对齐:顶对顶、中对中、底对底、顶对底、底对顶
      • 左右对齐:左对左、中对中、右对右、左对右、右对左
          let dragState = {
            startX: 0,
            startY: 0,
            startPos: [] as { left: number; top: number }[],
    
    +        startLeft: 0,
    +        startTop: 0,
    +        markLines: {} as VisualEditorMarkLine,
          };
          
          
    	const mousedown = (e: MouseEvent) => {
            dragState = {
              startX: e.clientX,
              startY: e.clientY,
              startPos: focusData.value.focus.map(({ top, left }) => ({
                top,
                left,
              })),
    +         startTop: state.selectBlock!.top,
    +         startLeft: state.selectBlock!.left,
    +         markLines: (() => {
    +           const { focus, unfocus } = focusData.value;
    +           // 当前选中的block
    +           const { top, left, width, height } = state.selectBlock!;
    +           let lines = { x: [], y: [] } as VisualEditorMarkLine;
    +           unfocus.forEach((block) => {
    +             const { top: t, left: l, width: w, height: h } = block;+
    +             // y轴对齐方
    +             lines.y.push({ top: t, showTop: t }); // 顶对顶
    +             lines.y.push({ top: t + h, showTop: t + h }); // 底对底
    +             lines.y.push({ top: t + h / 2 - height / 2, showTop: t + h / 2 }); // 中对中
    +             lines.y.push({ top: t - height, showTop: t }); // 顶对底
    +             lines.y.push({ top: t + h - height, showTop: t + h }); //
    
    +             // x轴对齐方式
    +             lines.x.push({ left: l, showLeft: l }); // 顶对顶
    +             lines.x.push({ left: l + w, showLeft: l + w }); // 底对底
    +             lines.x.push({
    +               left: l + w / 2 - width / 2,
    +               showLeft: l + w / 2,
    +             }); // 中对中
    +             lines.x.push({ left: l - width, showLeft: l }); // 顶对底
    +             lines.x.push({ left: l + w - width, showLeft: l + w }); // 中对中
    +           });
    +           return lines;
    +         })(),
            };
            document.addEventListener("mousemove", mousemove);
            document.addEventListener("mouseup", mouseup);
          };
    
    
    • 拖动选中的block时,将block的当前位置与 缓存的计算好的 跟其他block的对齐位置进行匹配,如果相对位置在5以内就显示辅助线位置
        const mousemove = (e: MouseEvent) => {
            let { clientX: moveX, clientY: moveY } = e;
            const { startX, startY } = dragState;
    
            // 按下shift键时,组件只能横向或纵向移动
            if (e.shiftKey) {
              // 当鼠标横向移动的距离 大于 纵向移动的距离,将纵向的偏移置为0
              if (Math.abs(e.clientX - startX) > Math.abs(e.clientY - startY)) {
                moveY = startY;
              } else {
                moveX = startX;
              }
            }
    		// 当前block的位置
            const currentLeft = dragState.startLeft + moveX - startX;
            const currentTop = dragState.startTop + moveY - startY;
            const currentMark = {
              x: null as null | number,
              y: null as null | number,
            };
    		
            // 在缓存位置中查找,是否有匹配坐标
            for (let i = 0; i < dragState.markLines.y.length; i++) {
              const { top, showTop } = dragState.markLines.y[i];
              // 相对位置在5以内就显示
              if (Math.abs(top - currentTop) < 5) {
                moveY = top + startY - dragState.startTop;
                currentMark.y = showTop;
                break;
              }
            }
    
            for (let i = 0; i < dragState.markLines.x.length; i++) {
              const { left, showLeft } = dragState.markLines.x[i];
              if (Math.abs(left - currentLeft) < 5) {
                moveX = left + startX - dragState.startLeft;
                currentMark.x = showLeft;
                break;
              }
            }
    
            const durY = moveY - startY;
            const durX = moveX - startX;
    
            focusData.value.focus.forEach((block, i) => {
              block.top = dragState.startPos[i].top + durY;
              block.left = dragState.startPos[i].left + durX;
            });
    		// 赋值给mark对象,在视图中显示
            mark.x = currentMark.x;
            mark.y = currentMark.y;
          };
    

    commit代码

    十一、属性面板与组件属性设置

    可视化拖拽页面编辑器 四

    1. 数据准备

    • 给block添加 props属性,用于控制组件属性

    • 新建visual-editor.props.tsx文件,用户 属性值类型定义

      • VisualEditorPropsType定义属性值 展示的几种类型,如按钮标题 需要录入用input,按钮类型用select下拉选择
      • createEditorInputProps createEditorSelectProps 定义创建输入框和下拉选择框属性的方法,统一进行创建
    export enum VisualEditorPropsType {
      input = "input",
      color = "color",
      select = "select",
    }
    
    export interface VisualEditorProps {
      type: VisualEditorPropsType;
      label: string;
      options?: VisualEditorSelectOptions;
    }
    
    /** ------input------- */
    export function createEditorInputProps(label: string): VisualEditorProps {
      return {
        type: VisualEditorPropsType.input,
        label,
      };
    }
    
    /** ------select------- */
    export type VisualEditorSelectOptions = {
      label: string;
      val: string;
    }[];
    
    export function createEditorSelectProps(
      label: string,
      options: VisualEditorSelectOptions
    ): VisualEditorProps {
      return {
        type: VisualEditorPropsType.select,
        label,
        options,
      };
    }
    

    2.在visual.config.tsx配置中定义组件属性和渲染规则

    • 渲染组件时,将block中定义的props数据传入,将props传入组件进行渲染
    visualConfig.registry("button", {
      label: "按钮",
      preview: () => <ElButton>按钮</ElButton>,
      render: ({ props, size }) => {
        return (
          <ElButton
            type={props.type}
            size={props.size}
            style={{
              width: size.width ? `${size.width}px` : undefined,
              height: size.height ? `${size.height}px` : undefined,
            }}
          >
            {props.text || "按钮"}
          </ElButton>
        );
      },
      resize: { width: true, height: true },
      props: {
        text: createEditorInputProps("显示文本"),
        type: createEditorSelectProps("按钮类型", [
          { label: "基础", val: "primary" },
          { label: "成功", val: "success" },
          { label: "警告", val: "warning" },
          { label: "危险", val: "danger" },
          { label: "提示", val: "info" },
          { label: "文本", val: "text" },
        ]),
        size: createEditorSelectProps("按钮大小", [
          { label: "默认", val: "" },
          { label: "中等", val: "medium" },
          { label: "小", val: "small" },
          { label: "极小", val: "mini" },
        ]),
      },
    });
    
    • visual-editor-block.tsx中将block的属性传给render函数
        const renderProps = {
            size: props.block?.hasResize
              ? {
                  width: props.block.width,
                  height: props.block.height,
                }
              : {},
    +        props: props.block?.props || {},
          };
          const Render = component?.render(renderProps);
    

    3. 新建一个属性面板组件 visual-editor-operator.tsxvisual-editor中引用

    • 将当前选中的block和编辑器配置config传入
    • updateBlockupdateModelValue用来更新block和容器的属性
    	<VisualOperatorEditor
              block={state.selectBlock!}
              config={props.config}
              dataModel={dataModel as any}
              updateBlock={updateBlockProps}
              updateModelValue={updateModelValue}
            />
    
    visual-editor-operator.tsx代码如下:
    import deepcopy from "deepcopy";
    import {
      ElButton,
      ElColorPicker,
      ElForm,
      ElFormItem,
      ElInput,
      ElInputNumber,
      ElOption,
      ElSelect,
    } from "element-plus";
    import { defineComponent, PropType, reactive, watch } from "vue";
    import {
      VisualEditorProps,
      VisualEditorPropsType,
    } from "./visual-editor.props";
    import {
      VisualEditorBlockData,
      VisualEditorConfig,
      VisualEditorModelValue,
    } from "./visual-editor.utils";
    
    export const VisualOperatorEditor = defineComponent({
      props: {
        block: { type: Object as PropType<VisualEditorBlockData> },
        config: { type: Object as PropType<VisualEditorConfig> },
        dataModel: {
          type: Object as PropType<VisualEditorModelValue>,
          required: true,
        },
        updateBlock: {
          type: Function as PropType<
            (
              newBlock: VisualEditorBlockData,
              oldBlock: VisualEditorBlockData
            ) => void
          >,
          required: true,
        },
        updateModelValue: {
          type: Function as PropType<(...args: any[]) => void>,
          required: true,
        },
      },
    
      setup(props) {
        const state = reactive({
          editData: {} as any,
        });
    
        const methods = {
          apply: () => {
            if (!props.block) {
              // 当前编辑容器属性
              props.updateModelValue({
                ...(props.dataModel as any).value,
                container: state.editData,
              });
            } else {
              // 当前编辑block数据属性
              const newBlock = state.editData;
              debugger;
              props.updateBlock(newBlock, props.block);
            }
          },
          reset: () => {
            if (!props.block) {
              state.editData = deepcopy((props.dataModel as any).value.container);
            } else {
              state.editData = deepcopy(props.block);
            }
          },
        };
    
        watch(
          () => props.block,
          () => {
            methods.reset();
          },
          {
            immediate: true,
          }
        );
    
        const renderEditor = (propName: string, propConfig: VisualEditorProps) => {
          return {
            [VisualEditorPropsType.input]: () => (
              <ElInput v-model={state.editData.props[propName]} />
            ),
            [VisualEditorPropsType.color]: () => (
              <ElColorPicker v-model={state.editData.props[propName]} />
            ),
            [VisualEditorPropsType.select]: () => (
              <ElSelect
                placeholder="请选择"
                v-model={state.editData.props[propName]}
              >
                {(() => {
                  return propConfig.options!.map((opt, i) => (
                    <ElOption key={i} label={opt.label} value={opt.val} />
                  ));
                })()}
              </ElSelect>
            ),
          }[propConfig.type]();
        };
    
        return () => {
          let content: JSX.Element[] = [];
          if (!props.block) {
            content.push(
              <>
                <ElFormItem label="容器宽度">
                  <ElInputNumber
                    v-model={state.editData.width}
                    {...{ step: 100 }}
                  />
                </ElFormItem>
                <ElFormItem label="容器高度">
                  <ElInputNumber
                    v-model={state.editData.height}
                    {...{ step: 100 }}
                  />
                </ElFormItem>
              </>
            );
          } else {
            const { componentKey } = props.block;
            const component = props.config?.componentMap[componentKey];
    
            if (component) {
              content.push(
                <ElFormItem label="组件标识">
                  <ElInput v-model={state.editData.slotName} />
                </ElFormItem>
              );
              if (component.props) {
                content.push(
                  <>
                    {Object.entries(component.props).map(
                      ([propName, propConfig]) => (
                        <ElFormItem
                          {...{ labelPosition: "top" }}
                          label={propConfig.label}
                          key={propName}
                        >
                          {renderEditor(propName, propConfig)}
                        </ElFormItem>
                      )
                    )}
                  </>
                );
              }
            }
          }
          return (
            <div class="operator">
              <ElForm>
                {content.map((el) => el)}
                <ElFormItem>
                  <ElButton type="primary" {...({ onClick: methods.apply } as any)}>
                    应用
                  </ElButton>
                  <ElButton {...({ onClick: methods.reset } as any)}>重置</ElButton>
                </ElFormItem>
              </ElForm>
            </div>
          );
        };
      },
    });
    
    • 当未选中block时,默认渲染容器的宽度和高度属性,可以修改容器的宽高
    • 选中block时, 根据block的componentKey,获取在config注册的组件属性,渲染block的属性列表(renderEditor方法)
    	const { componentKey } = props.block;
            const component = props.config?.componentMap[componentKey];
    
            if (component) {
              content.push(
                <ElFormItem label="组件标识">
                  <ElInput v-model={state.editData.slotName} />
                </ElFormItem>
              );
              if (component.props) {
                content.push(
                  <>
                    {Object.entries(component.props).map(
                      ([propName, propConfig]) => (
                        <ElFormItem
                          {...{ labelPosition: "top" }}
                          label={propConfig.label}
                          key={propName}
                        >
                          {renderEditor(propName, propConfig)}
                        </ElFormItem>
                      )
                    )}
                  </>
                );
              }
            }
    
    • 将 block对象深拷贝到 editData中,根据propName将属性值和组件进行双向数据绑定,修改的属性值会保存在 editData
    const renderEditor = (propName: string, propConfig: VisualEditorProps) => {
          return {
            [VisualEditorPropsType.input]: () => (
              <ElInput v-model={state.editData.props[propName]} />
            ),
            [VisualEditorPropsType.color]: () => (
              <ElColorPicker v-model={state.editData.props[propName]} />
            ),
            [VisualEditorPropsType.select]: () => (
              <ElSelect
                placeholder="请选择"
                v-model={state.editData.props[propName]}
              >
                {(() => {
                  return propConfig.options!.map((opt, i) => (
                    <ElOption key={i} label={opt.label} value={opt.val} />
                  ));
                })()}
              </ElSelect>
            ),
          }[propConfig.type]();
        };
    
    • 点击应用时调用 传入的updateBlock方法更新选中的block,调用updateModelValue更新容器的属性值
        // 更新block属性
        const updateBlockProps = (
          newBlock: VisualEditorBlockData,
          oldBlock: VisualEditorBlockData
        ) => {
          const blocks = [...dataModel.value!.blocks];
          const index = dataModel.value!.blocks.indexOf(state.selectBlock!);
          if (index > -1) {
            blocks.splice(index, 1, newBlock);
            dataModel.value!.blocks = deepcopy(blocks);
            state.selectBlock = dataModel.value!.blocks[index];
          }
        };
    
        // 更新容器属性值
        const updateModelValue = (newVal: VisualEditorModelValue) => {
          props.modelValue!.container = { ...newVal.container };
        };
    

    commit代码

    完整代码 GitHub


    起源地下载网 » 可视化拖拽页面编辑器 四

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元