文章
问答
冒泡
vue3 中 使用vue.draggable.next 实现拖拽生成页面

前言

vue.draggable.next 是一款vue3的拖拽插件,是vue.draggable升级版本,同样是基于Sortable.js实现的,你可以用它来拖拽列表、菜单、工作台、选项卡等常见的工作场景。

包安装方式

yarn add vuedraggable@next
npm i -S vuedraggable@next

UMD浏览器引用JS方式

<script src="https://www.itxst.com/package/vue3/vue.global.js"></script>
<script src="https://www.itxst.com/package/sortable/Sortable.min.js"></script>
<script src="https://www.itxst.com/package/vuedraggablenext/vuedraggable.umd.min.js"></script>

左侧icon 区

<draggable
          :list="item.componentList"
          class="icon-list-box"
          animation="300"
          item-key="id"
          @start="onStart"
          @end="onEnd"
          @filter="onFilter"
          :clone="cloneDog"
          :group="{ name: props.dragGroup, pull: 'clone', put: false }"
          :touch-start-threshold="50"
          :fallback-tolerance="50"
          :sort="false"
        >
          <template #item="{ element }">
            <div :class="{ 'list-item': true, draggableToolItem: true }">
              <div class="icon-box">
                <Icon :size="40" :icon="element.icon" />
                <p class="icon-title">{{ element.title }}</p>
              </div>
            </div>
          </template>
        </draggable>
<script setup name="toolBar">
  import { Tabs } from 'ant-design-vue';
  import draggable from 'vuedraggable';
  import { Icon } from '/@/components/Icon';
  import { generateEditorItem } from '../../utils';
  const $emits = defineEmits(['onFilter', 'onDragStart', 'onDragEnd']);

  const TabPane = Tabs.TabPane;
  const props = defineProps({
    tools: {
      type: Array,
      default: () => [],
    },
    dragGroup: {
      type: String,
      default: '',
    },
  });

  console.log(props.dragGroup, '-----');
  //拖拽开始的事件
  const onStart = () => {
    console.log('--onStart---');
    $emits('onDragStart');
  };

  const onFilter = () => {
    console.log('--onFilter---');
    $emits('onFilter');
  };
  //拖拽结束的事件
  const onEnd = () => {
    console.log('--onEnd---');
    $emits('onDragEnd');
  };

  const cloneDog = (toolItem) => {
    console.log('--cloneDog---', toolItem);
    return generateEditorItem(toolItem);
  };
</script>

中间显示区

<div ref="domScrollWrap" class="drag-area-wrap">
              <draggable
                ref="editDraggable"
                :list="editComponentList"
                @start="onStart"
                @change="onDragChange"
                :disabled="false"
                item-key="id"
                :component-data="{ name: 'fade' }"
                draggable=".draggableItem"
                handle=".draggableItem"
                :group="{ name: 'componentsGroup', pull: true }"
                :ghostClass="$style.ghost"
                class="drag-area"
                :animation="300"
                :sort="true"
              >
                <template #item="{ element, index }">
                  <div class="draggableItem">
                    <div className="widget">{{ element.title }}</div>
                    <div @click="showPopover(element)" :class="{ active: state.id == element.id }">
                      <Popover
                        placement="right"
                        overlayClassName="draggable-popover"
                        :get-popup-container="getPopupContainer"
                        trigger="click"
                        v-model="visible"
                      >
                        <template #content>
                          <a-button
                            preIcon="ant-design:caret-up-outlined"
                            title="上移"
                            :disabled="index == 0"
                            @click="handleItemOperate({ item: element, command: 'moveUp' })"
                          />
                          <a-button
                            preIcon="ant-design:caret-down-outlined"
                            title="下移"
                            :disabled="index == editComponentList.length - 1"
                            @click="handleItemOperate({ item: element, command: 'moveDown' })"
                          />
                          <a-button
                            preIcon="ant-design:copy-outlined"
                            title="复制"
                            @click="handleItemOperate({ item: element, command: 'copy' })"
                          />
                          <a-button
                            preIcon="ant-design:delete-outlined"
                            title="移除"
                            @click="handleItemOperate({ item: element, command: 'remove' })"
                          />
                        </template>
                        <component
                          :is="group[element.name]"
                          :ref="element.name"
                          :form-data="element.componentValue"
                        />
                      </Popover>
                    </div>
                  </div>
                </template>
              </draggable>
              <div class="tip-box" v-if="editComponentList.length === 0">
                <p>左边选择模块拖入该区域</p>
              </div>
<script setup name="adornment">
  import { ref, reactive, computed, onBeforeMount } from 'vue';
  import { Input, Popover, Pagination, Empty } from 'ant-design-vue';
  import { Icon } from '/@/components/Icon';
  import draggable from 'vuedraggable';
  import { PageWrapper } from '/@/components/Page';
  import ToolBar from './components/ToolBar/index.vue';
  import tools from './data/tools';
  import group from './components/group';
  import defaultItems from './data/mock';
  import * as arrayMethods from './array';
  import { api2VmToolItem, generateEditorItem } from './utils';
  import { editDecoratePageConfig, getDecoratePageConfigDetail } from '/@/api/sys/decorate';
  import { useUserStore } from '/@/store/modules/user';
  const { userInfo } = useUserStore();
  const editDraggable = ref(null);
  const domScrollWrap = ref(null);
  const state = reactive({
    formName: '',
    id: '',
    formData: {},
  });
  const editHeaderComponentList = reactive([]);
  const editComponentList = reactive([]);
  const editFooterComponentList = reactive([]);
  const visible = ref(false);
  //拖拽开始的事件
  const onStart = (data) => {
    console.log('开始拖拽');
  };
  // 新增和查询页面配置
  editDecoratePageConfig({
    pageName: '首页1',
    remark: '111',
    settingJson: JSON.stringify([
      {
        title: '搜索框',
        description: '首页搜索框',
        name: 'SearchBar',
        orderNo: 1,
        id: '1',
        properties: {
          link: '/pages/category/category',
        },
      },
      {
        title: '标题栏',
        description: '首页标题栏',
        name: 'TitleBar',
        orderNo: 2,
        id: '111',
        properties: {
          leftText: '爆款秒杀',
          rightText: '查看更多',
        },
      },
      {
        title: '轮播图',
        description: '首页轮播',
        name: 'Swiper',
        orderNo: 3,
        id: '2',
        properties: {
          slideList: [
            {
              url: 'https://file.40017.cn/cvgfront-common/tc_travel_top_banner_new.png',
              type: 'img',
            },
            {
              url: 'https://pavo.elongstatic.com/i/ori/15sr7AutTyM.jpg',
              type: 'img',
            },
            {
              url: 'https://storage.360buyimg.com/jdc-article/welcomenutui.jpg',
              type: 'img',
            },
            {
              url: 'https://storage.360buyimg.com/jdc-article/fristfabu.jpg',
              type: 'img',
            },
          ],
        },
      },
    ]),
    shopId: 0,
    updateManager: userInfo.account,
  });
  getDecoratePageConfigDetail({ pageId: 1 });
  const getPopupContainer = (trigger) => {
    return trigger.parentElement;
  };

  const showPopover = (item) => {
    console.log(item, '=========test======');
    state.formData = item.componentValue;
    state.formName = item.formName;
    state.id = item.id;
    visible.value = !visible.value;
  };

  onBeforeMount(() => {
    const dataList = api2VmToolItem(tools, defaultItems);
    dataList.forEach((toolItemData) => {
      // 模拟拖入组件插入数据
      const editorData = generateEditorItem(toolItemData);
      editComponentList.push(editorData);
      console.log(editComponentList, '----------');
    });
  });

  //拖拽结束的事件
  const onEnd = (data) => {
    console.log('-----on----End----', data);
    console.log('结束拖拽');
  };

  const onDragChange = (data) => {
    console.log('-----onDragChange----', data);
  };

  const handleItemOperate = ({ item, command }) => {
    const strategyMap = {
      moveUp(target, arrayItem) {
        return arrayMethods.moveUp(target, arrayItem);
      },
      moveDown(target, arrayItem) {
        return arrayMethods.moveDown(target, arrayItem);
      },
      copy(target, arrayItem) {
        const { componentValue, ...emptyPack } = arrayItem;
        return target.splice(target.indexOf(arrayItem) + 1, 0, generateEditorItem(emptyPack));
      },
      remove(target, arrayItem) {
        return arrayMethods.remove(target, arrayItem);
      },
    };

    const curStrategy = strategyMap[command];

    if (curStrategy) {
      curStrategy.apply(this, [editComponentList, item]);
    } else {
      this.$message.error(`系统错误 - 未知的操作:[${command}]`);
    }
  };

  const onSave = () => {
    console.log('222222', editComponentList);
  };

  const onMove = (e) => {
    console.log('-----on----Move----', e); //不允许停靠
    // if (e.relatedContext.element.disabledPark == true) return false;
    return true;
  };
</script>

右侧组件配置区

{
  "imgList": [
    {
      "id": "1",
      "name": "苏宁",
      "imgUrl": "https://gw.alicdn.com/imgextra/i1/1734167/O1CN01eXdllZ1geX1zdbBLa_!!1734167-0-lubanu.jpg_790x10000Q75.jpg_.webp",
      "imgLink": "https://www.jd.com"
    },
    {
      "id": "2",
      "name": "苏宁",
      "imgUrl": "https://img.alicdn.com/tps/i4///img.alicdn.com/tps/i4//TB1CDhKUvb2gK0jSZK9SuuEgFXa.jpg_790x10000Q75.jpg_.webp",
      "imgLink": "https://www.jd.com"
    }
  ]
}
<div class="card-list">
        <draggable :list="props.formData.imgList" animation="300" :sort="true" item-key="id">
          <template #item="{ element, index }">
            <Card class="card-box" :bodyStyle="{ padding: '0' }">
              <div style="overflow: hidden">
                <div class="upload-box">
                  <Upload
                    name="file"
                    list-type="picture-card"
                    class="avatar-uploader"
                    :show-upload-list="false"
                    :action="uploadUrl"
                    :before-upload="beforeUpload"
                    @change="(file) => handleUploadChange(file, index)"
                  >
                    <img v-if="element.imgUrl" :src="element.imgUrl" alt="avatar" />
                    <div v-else>
                      <loading-outlined v-if="state.uploadLoading" />
                      <plus-outlined v-else />
                      <div class="ant-upload-text">添加图片</div>
                    </div>
                  </Upload>
                </div>
                <div>
                  <div>
                    <div class="mb12">
                      <span class="form-label">标题</span>
                      <Input style="width: 172px" v-model:value="element.name" />
                    </div>
                    <div>
                      <span class="form-label">链接</span>
                      <a-button type="link" style="padding: 0">Link</a-button>
                    </div>
                  </div>
                </div>
              </div>
              <Icon
                icon="ant-design:close-circle-outlined"
                :size="20"
                class="card-close"
                style="display: none"
                @click="() => removeItem(index)"
              />
            </Card>
          </template>
        </draggable>
      </div>
<script setup name="carouselImgForm">
  import draggable from 'vuedraggable';
  import { ref, defineComponent, reactive, toRefs, computed } from 'vue';
  import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue';
  import { useGlobSetting } from '/@/hooks/setting';
  import { useUserStore } from '/@/store/modules/user';
  import { Card, Image, Upload, Input } from 'ant-design-vue';
  import { Icon } from '/@/components/Icon';
  import { genId } from '../../../utils';

  const { uploadUrl } = useGlobSetting();
  const props = defineProps({
    formData: { type: Object },
  });
  const state = reactive({
    uploadLoading: false,
  });

  const beforeUpload = (file) => {
    const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
    if (!isJpgOrPng) {
      message.error('You can only upload JPG file!');
    }
    const isLt2M = file.size / 1024 / 1024 < 2;
    if (!isLt2M) {
      message.error('Image must smaller than 2MB!');
    }
    return isJpgOrPng && isLt2M;
  };

  const handleUploadChange = (info, index) => {
    if (info.file.status === 'uploading') {
      state.uploadLoading = true;
      return;
    }
    if (info.file.status === 'done') {
      const { result } = info.file.response.data;
      // eslint-disable-next-line vue/no-mutating-props
      props.formData.imgList[index].imgUrl = result;
      state.uploadLoading = false;
    }
    if (info.file.status === 'error') {
      state.uploadLoading = false;
      message.error('upload error');
    }
  };

  const removeItem = (index) => {
    if (props.formData.imgList.length < 2) {
      message.error('最少保留一个图片');
      return;
    }
    // eslint-disable-next-line vue/no-mutating-props
    props.formData.imgList.splice(index, 1);
  };
  const addItem = () => {
    // eslint-disable-next-line vue/no-mutating-props
    props.formData.imgList.push({
      id: genId(),
      name: '',
      img: {
        imgUrl: '',
        imgLink: '',
      },
    });
  };
</script>

最终效果图


关于作者

这样
划水摸鱼专业户
获得点赞
文章被阅读