一、案例效果
二、案例代码
- 封装左侧抽屉
DrawerSearch.vue
<template>
<div>
<mtd-form :model="formDrawerSearch" ref="formCustom" inline>
<mtd-form-item>
<mtd-input
type="text"
v-model="formDrawerSearch.hostname"
placeholder="搜索已有图表"
style="width: 130px"
/>
</mtd-form-item>
<mtd-form-item>
<mtd-select
v-model="formDrawerSearch.searchOrder"
placeholder="按修改时间排序"
style="width: 145px"
clearable
filterable
>
<mtd-option
v-for="item in searchOrderList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</mtd-select>
</mtd-form-item>
</mtd-form>
</div>
</template>
<script lang="ts" setup name="DrawerSearch">
import { ref, watch } from 'vue';
const $emit = defineEmits(['formDrawerSearch']);
const formDrawerSearch = ref({
hostname: '',
searchOrder: '',
});
const searchOrderList = ref([
{
value: 'tag1',
label: '按照修改人',
},
{
value: 'tag2',
label: '按照风险域',
},
{
value: 'tag3',
label: '按照数据集',
},
]);
watch(
() => formDrawerSearch.value,
() => {
$emit('formDrawerSearch', formDrawerSearch.value);
},
{ deep: true },
);
</script>
DrawerContent.vue
<template>
<div class="drawer-content pr-5">
<draggable
:list="cardList"
:move="onMove"
:group="{ name: 'items', pull: 'clone', put: false }"
:clone="checkAndCloneItem"
:sort="false"
item-key="id"
>
<div v-for="(item, index) in cardList" :key="index">
<mtd-card class="card-box">
<div>
<span class="text-[13px] font-semibold">{{ item.title }}</span>
<mtd-button
v-if="item.isAdd"
class="float-right"
ghost
type="primary"
size="small"
>已添加</mtd-button
>
</div>
<div class="mt-3 card-content">
<div>可视化类型:{{ item.chartType }}</div>
<div>
数据集:
<a @click="goChartDetail()">{{ item.dataSet }}</a>
</div>
<div>修改:{{ item.modify }}</div>
</div>
</mtd-card>
</div>
</draggable>
</div>
</template>
<script lang="ts" setup name="DrawerContent">
import { BoardDrawerItemType } from '@/type/aggregateAnalysis';
import { Message } from '@ss/mtd-vue';
import { ref } from 'vue';
import Draggable from 'vuedraggable';
const $emit = defineEmits(['cloneItem']);
const props = defineProps(['rightList']);
const cardList = ref<BoardDrawerItemType[]>([
{
id: 1,
title: '12133场景',
isAdd: true,
chartType: '折线图',
dataSet: '全量数据集',
modify: '2024-03-01/jinlidan',
},
{
id: 2,
title: 'asdasd场景',
isAdd: true,
chartType: '柱形图',
dataSet: '全量数据集',
modify: '2024-05-06/jinlidan',
},
{
id: 3,
title: '88999场景',
isAdd: true,
chartType: '饼图',
dataSet: '全量数据集',
modify: '2024-08-09/jinlidan',
},
{
id: 4,
title: 'dfaaaa场景',
isAdd: true,
chartType: '饼图',
dataSet: '全量数据集',
modify: '2024-08-09/jinlidan',
},
{
id: 5,
title: '333场景',
isAdd: true,
chartType: '饼图',
dataSet: '全量数据集',
modify: '2024-08-09/jinlidan',
},
{
id: 6,
title: '66666场景',
isAdd: true,
chartType: '饼图',
dataSet: '全量数据集',
modify: '2024-08-09/jinlidan',
},
{
id: 7,
title: '7777场景',
isAdd: true,
chartType: '饼图',
dataSet: '全量数据集',
modify: '2024-08-09/jinlidan',
},
]);
const checkAndCloneItem = (item: any) => {
console.log('props.rightList', props.rightList, 'item', item);
const isDuplicate = props.rightList.some(
(rightItem: any) => rightItem.id === item.id,
);
if (isDuplicate) {
Message.warning('已有重复项目!');
return false; // 返回 false 或 null 来阻止拖动
}
$emit('cloneItem', { ...item });
return { ...item }; // 返回一个新对象,避免修改原始数据
};
const onMove = (evt: any) => {
// evt.dragged: 当前被拖拽的元素
// evt.related: 当前拖拽元素下的DOM元素
const draggedEl = evt.dragged;
const relatedEl = evt.related;
// 给被拖拽的元素添加样式
draggedEl.classList.add('dragging-item');
};
const goChartDetail = () => {};
</script>
<style lang="less" scoped>
.drawer-content {
overflow: auto;
height: calc(100vh - 272px);
}
.card-box {
margin-bottom: 5px;
margin-left: 0px;
/deep/.mtd-card-body {
padding: 10px;
}
.card-content {
font-size: 12px;
color: #6b7280c4;
}
}
.dragging-item {
width: 100%;
padding: 8px;
margin: 5px;
border: 2px dashed #409eff; /* 示例样式:蓝色虚线边框 */
opacity: 0.7; /* 透明度降低,增加拖拽感 */
}
</style>
- 封装右侧区域
RightContent.vue
<template>
<div class="right-content">
<draggable
class="drag-content"
:list="rightList"
:group="{ name: 'items', put: true }"
@add="onAdd"
>
<div
v-for="(item, index) in filteredRightList"
:key="index"
class="drag-content-item"
>
<mtd-card class="card-box">
<div>
<span class="text-[13px] font-semibold">{{ item.title }}</span>
</div>
</mtd-card>
</div>
</draggable>
</div>
</template>
<script lang="ts" setup name="RightContent">
import { BoardDrawerItemType } from '@/type/aggregateAnalysis';
import { getCloneItemHandle } from '@/utils/aggregateAnalysis';
import { ref, computed } from 'vue';
import Draggable from 'vuedraggable';
const $emit = defineEmits(['rightList']);
const rightList = ref<BoardDrawerItemType[]>([]);
const filteredRightList = computed(() => {
return rightList.value.filter((item) => item.title);
});
const onAdd = (evt: any) => {
console.log('onAdd triggered', evt);
let newItem = evt.item;
// 检查是否存在 _underlying_vm_ 属性
if (newItem && newItem._underlying_vm_) {
newItem = newItem._underlying_vm_;
}
console.log('New item:', newItem);
const arr = JSON.parse(JSON.stringify(rightList.value));
rightList.value = arr.filter((item: any) => item !== false && item.id);
console.log('==rightList.value', rightList.value);
$emit('rightList', rightList.value);
if (newItem) {
const clonedItem = getCloneItemHandle(newItem);
// 检查是否已存在相同的项
const existingIndex = rightList.value.findIndex(
(item) => item.id === clonedItem.id,
);
if (existingIndex === -1) {
// 如果不存在,则添加新项
rightList.value.splice(evt.newIndex, 0, clonedItem);
}
} else {
console.error('无法从事件中获取新项');
}
};
</script>
<style lang="less" scoped>
.right-content {
height: 100%;
.drag-content {
display: flex;
height: 100%;
flex-wrap: wrap;
white-space: nowrap;
.drag-content-item {
margin: 10px;
width: calc(33.33% - 20px);
height: 350px;
background: #fff;
.card-box {
/deep/.mtd-card-body {
padding: 10px;
}
}
}
}
}
</style>
- 组件组合
<template>
<div>
<AggregateAnalysisTab :activeTabType="'board'">
<div slot="tabBoardContent">
<BackTitle href="aggregateAnalysis">
新建看板
<div slot="buttonHandle">
<mtd-button class="mr-3">取消</mtd-button>
<mtd-button type="primary">保存</mtd-button>
</div>
</BackTitle>
<div>
<mtd-form :model="formData" ref="formData" :rules="ruleData" inline>
<LabelModule :title="'定义看板'" />
<mtd-form-item label="看板名称" prop="boardName">
<mtd-input
type="text"
v-model="formData.boardName"
placeholder="请输入看板名称,最多128字符"
:maxlength="128"
style="width: 260px"
/>
</mtd-form-item>
<mtd-form-item label="归属风险域" prop="riskDomain">
<mtd-select
v-model="formData.riskDomain"
placeholder="请选择"
style="width: 160px"
>
<mtd-option
v-for="item in riskDomainList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</mtd-select>
</mtd-form-item>
<mtd-form-item label="看板说明" prop="boardRemark">
<mtd-input
type="text"
v-model="formData.boardRemark"
placeholder="请对看板的数据的业务含义说明,最多1024字符"
:maxlength="1024"
style="width: 560px"
/>
</mtd-form-item>
</mtd-form>
</div>
<LabelModule :title="'配置看板'">
<template slot="buttonHandle">
<mtd-button>高级配置</mtd-button>
</template>
</LabelModule>
<div class="config-box flex clear-both">
<div class="left-drawer flex">
<DragDrawer ref="drawer" mode="left">
<template slot="content">
<DrawerSearch @formDrawerSearch="getFormDrawerSearch" />
<DrawerContent :rightList="rightList" />
</template>
</DragDrawer>
</div>
<div class="right-content-box">
<RightContent @rightList="getRightList" />
</div>
</div>
</div>
</AggregateAnalysisTab>
</div>
</template>
<script lang="ts">
import { debounce } from '@/common/index';
import AggregateAnalysisTab from '@/components/aggregateAnalysis/AggregateAnalysisTab.vue';
import DragDrawer from '@/components/aggregateAnalysis/widgets/DragDrawer.vue';
import BackTitle from '@/components/base/BackTitle.vue';
import LabelModule from '@/components/base/LabelModule.vue';
import { DrawerBoardType } from '@/type/aggregateAnalysis';
import { Component, Vue } from 'vue-property-decorator';
import DrawerContent from './components/DrawerContent.vue';
import DrawerSearch from './components/DrawerSearch.vue';
import RightContent from './components/RightContent.vue';
@Component({
name: 'AggregateBoardInfo',
components: {
AggregateAnalysisTab,
BackTitle,
LabelModule,
DragDrawer,
DrawerSearch,
DrawerContent,
RightContent,
},
})
export default class AggregateBoardInfo extends Vue {
private formData = {
boardName: '',
riskDomain: '',
boardRemark: '',
};
private ruleData = {
boardName: [
{
required: true,
message: '请输入',
},
],
riskDomain: [
{
required: true,
message: '请选择',
},
],
};
private riskDomainList = [
{
value: 'tag1',
label: '标签1',
},
{
value: 'tag2',
label: '标签2',
},
{
value: 'tag3',
label: '标签3',
},
];
private rightList: any = [];
/**
* 查询图表列表
*/
private getChartList() {}
/**
* 获取图表查询参数
*/
private getFormDrawerSearch(params: DrawerBoardType) {
console.log('128--params', params);
debounce(this.getChartList);
}
private getRightList(params: any) {
this.rightList = [...params];
}
}
</script>
<style lang="less" scoped>
.back-title {
/deep/span {
line-height: 33px;
}
}
.config-box {
height: 100%;
.right-content-box {
background: #0c3fa6de;
flex: 1;
margin-left: 30px;
overflow: auto;
height: calc(100vh - 272px);
}
}
</style>
- utils/index
export const getCloneItemHandle = (params: any) => {
return { ...params };
};
三、文件目录
- views
- components
四、总结
- 采用 vuedraggable 插件进行功能实现
- 保留左侧数据 主要是
:group="{ name: 'items', pull: 'clone', put: false }"
中 ‘clone’ - 保持左侧数据不能互相拖拽改变顺序 主要是配置
:sort="false" item-key="id"
- 左侧拖拽到右侧数据去重主要采用
:clone="checkAndCloneItem"
实现 - 拖动到右侧区域之前可以改变的样式采用
:move="onMove"
添加 'dragging-item’设置样式