实现效果:
左侧往右侧拖动,右侧列表可以进行拖拽排序。
安装引用:
npm install vuedraggable
import draggable from 'vuedraggable'
使用:
data数据:
componentList: [
{
groupName: '考试题型',
children: [
{
componentType: 'danxuan',
componentName: '单选题',
componentIcon: 'icon-danxuan'
},
{
componentType: 'duoxuan',
componentName: '多选题',
componentIcon: 'icon-duoxuan'
},
{
componentType: 'panduan',
componentName: '判断题',
componentIcon: 'icon-panduan'
}
]
},
{
groupName: '信息题',
children: [
{
componentType: 'message',
componentName: '姓名',
componentIcon: 'icon-xingming'
},
{
componentType: 'message',
componentName: '手机',
componentIcon: 'icon-shouji'
},
{
componentType: 'message',
componentName: '邮箱',
componentIcon: 'icon-youxiang'
}
]
}
],
questionList:[],
html代码:
左侧代码:
<el-tabs type="border-card" class="tabs">
<el-tab-pane label="题型">
<div v-for="(item, index) in componentList" :key="index">
<b class="fs-14">{{item.groupName}}</b>
<draggable
@end="end"
:clone="cloneElement"
class="group"
v-model="item.children"
:sort="false" //禁止排序
:group="{
name: 'component',
pull: 'clone',
put: false //不允许其他元素拖拽进此空间
}">
<div @click="pushComponent(_item)" class="component" v-for="(_item, _index) in item.children" :key="_index">
<i class="iconfont mr-8" :class="_item.componentIcon"></i>
<span>{{_item.componentName}}</span>
</div>
</draggable>
</div>
</el-tab-pane>
<el-tab-pane label="题库">
<el-tree
ref="tree"
highlight-current
:data="treeList"
node-key="id"
:current-node-key="currentNodekey"
@node-click="handleNodeClick"
:load="loadNode"
:props="props"
lazy>
<span slot-scope="{node}">
<el-tooltip v-if="node.label.length>=8" class="item" effect="dark" :content="node.label" placement="top">
<div class="text-ellipsis width-150">{{ node.label }}</div>
</el-tooltip>
<div v-else>{{ node.label }}</div>
</span>
</el-tree>
</el-tab-pane>
</el-tabs>
右侧代码:
<div class="content">
<el-scrollbar ref="scrollbar" style="height: calc(100vh - 220px)">
{{questionList}}
<draggable
class="list"
forceFallback
:animation="200"
ghostClass="ghost"
handle=".el-icon-rank"
v-model="questionList"
:group="{
name: 'component'
}">
<transition-group class="height-percent-100 display-block">
<div
class="item"
:class="{active: item.active, error: item.error}"
v-for="(item, index) in questionList"
:key="item.uid">
<div
class="display-flex ai-flex-start padding-20 pt-14"
@click="clickQuestion(item)"
:id="item.uid">
<div class="pt-6 width-40">
<b>{{index + 1}}</b>
</div>
<div class="flex-1">
<div class="display-flex ai-flex-start jc-space-between">
<b @click="editTitle(item)" class="width-percent-80 pt-6" style="min-height: 26px" v-if="!item.editTitle">{{item.title}}</b>
<el-input
type="textarea"
autosize
:ref="item.uid"
v-else
size="small"
class="width-percent-80"
@blur="item.editTitle = false"
v-model="item.title"></el-input>
<span v-if="item.componentType !== 'message'" class="color-info pt-6">( {{item.score}}分 )</span>
</div>
<div class="mt-12">
<el-input
v-if="item.componentType === 'message'"
readonly
placeholder="请输入"
type="textarea"
autosize
v-model="item.answer"
size="small"
class="width-percent-80"></el-input>
<draggable v-model="item.options" handle=".el-icon-d-caret">
<transition-group>
<div v-for="i in item.options" :key="i.value" class="display-flex ai-center jc-space-between pt-4 pb-4">
<div class="flex-1 display-flex ai-center">
<el-checkbox
v-if="item.componentType === 'duoxuan'"
v-model="item.answer" :label="i.value">
{{ }}
</el-checkbox>
<el-radio
v-else
v-model="item.answer"
:label="i.value" class="mr-0">{{ }}</el-radio>
<p @click="editOption(i)" v-if="!i.edit" class="margin-0 fs-14 width-percent-80 display-flex ai-center" style="min-height: 32px">{{i.label}}</p>
<el-input
type="textarea"
autosize
@blur="i.edit = false"
:ref="i.value"
v-else
v-model="i.label"
size="small"
class="width-percent-80"></el-input>
</div>
<div class="display-flex ai-center fd-row-reverse color-info width-130">
<i class="el-icon-d-caret ml-8 cursor-move"></i>
<i @click="delOption(item, i.value)" class="ml-10 el-icon-remove-outline cursor-pointer"></i>
<span class="color-success fs-14" v-if="item.answer.includes(i.value)">( 正确答案 )</span>
</div>
</div>
</transition-group>
</draggable>
<div v-if="['danxuan', 'duoxuan'].includes(item.componentType)">
<el-button class="pb-0" @click="addOption(item)" type="text" icon="el-icon-plus">添加选项</el-button>
</div>
</div>
</div>
<div class="display-flex ai-center color-info mt-8">
<i class="ml-14 el-icon-rank cursor-move"></i>
<i @click.stop="copyQuestion(item, index)" class="ml-14 el-icon-document-copy cursor-pointer"></i>
<i @click.stop="delQuestion(item)" class="ml-14 el-icon-delete cursor-pointer"></i>
</div>
</div>
<div class="errorMessage" v-if="item.error">
{{item.errorMessage}}
</div>
</div>
<div key="empty" v-if="!questionList.length" class="height-percent-100 fd-column display-flex ai-center jc-center">
<el-empty description="请点击右侧或拖入题型进行添加题目"></el-empty>
</div>
</transition-group>
</draggable>
</el-scrollbar>
</div>
方法:
/**
* 点击组件进行push
* @param data
* @param type
*/
pushComponent (data, type = 0) {
console.log(data)
//type=1:后端给的题库项导入 0:题型项导入
this.questionList.push(type ? data : this.cloneElement(data))
const newDraggableIndex = this.questionList.length - 1
const e = {
to: {
className: 'pushComponent'
},
newDraggableIndex
}
this.end(e)
},
/**
* 拖拽结束
* @param e
*/
end (e) {
console.log(e)
if (e.to.className !== 'group') {
for (const item of this.questionList) {
item.active = false
}
this.questionList[e.newDraggableIndex].active = true
this.$nextTick(() => {
document.getElementById(this.questionList[e.newDraggableIndex].uid).scrollIntoView();
})
}
},
/**
* 拖拽clone
* @param item
* @returns {any}
*/
cloneElement (item) {
const data = JSON.parse(JSON.stringify(item));
console.log(data)
data.uid = `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
data.title = data.componentName
data.answer = ''
data.active = false
data.editTitle = false
data.error = false
data.errorMessage = ''
switch (data.componentType) {
case 'danxuan':
data.scoreMethod = '1' // 得分方式
data.options = [
{
edit: false,
label: '选项1',
value: `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
},
{
edit: false,
label: '选项2',
value: `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
}
] // 选项
data.answer = data.options[0].value // 答案
data.score = 10 // 分数
data.description = '' // 解析
break
case 'duoxuan':
data.scoreMethod = '1'
data.options = [
{
edit: false,
label: '选项1',
value: `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
},
{
edit: false,
label: '选项2',
value: `${data.componentType}-${Math.floor(Math.random() * 1000000)}`
}
]
data.answer = [data.options[0].value]
data.score = 10
data.description = ''
break
case 'panduan':
data.scoreMethod = '1'
data.options = [
{
edit: false,
label: '是',
value: `${data.componentType}-true`
},
{
edit: false,
label: '否',
value: `${data.componentType}-false`
}
]
data.answer = data.options[0].value
data.score = 10
data.description = ''
break
}
return data
},
css:
.tabs {
width: 240px;
box-shadow: none;
border: none;
height: 100%;
.group {
display: grid;
grid-gap: 12px;
grid-template-columns: repeat(2, 1fr);
font-size: 14px;
padding: 12px 0;
}
.component {
color: #666666;
border-radius: 4px;
border: 1px solid #D8D8D8;
display: flex;
align-items: center;
justify-content: center;
padding: 8px 0;
cursor: pointer;
&:hover {
color: #1774FF;
border-color: #1774FF;
}
}
}
.content {
flex: 1;
background-color: #EFF2F4;
border-left: 1px solid #DCDFE6;
border-right: 1px solid #DCDFE6;
padding: 20px 4px 0 20px;
.list {
height: 100%;
margin-right: 16px;
.item {
padding: 0;
border: 1px solid transparent;
border-radius: 4px;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
background-color: #fff;
margin-bottom: 20px;
.el-icon-delete,.el-icon-remove-outline {
&:hover {
color: #F56C6C;
}
}
.el-icon-document-copy {
&:hover {
color: #3377FF;
}
}
}
.errorMessage {
color: #FFFFFF;
background-color: #F56C6C;
padding: 10px 20px;
font-size: 14px;
border-radius: 0 0 4px 4px;
}
.active {
border-color: #2A5EFF;
}
.error {
border-color: #F56C6C;
}
}
.ghost {
background-color: #499BFF;
border-radius: 4px;
padding: 20px;
margin-bottom: 20px;
.iconfont {
display: none;
}
span {
color: #FFFFFF;
}
}
}
扩展:
点击题库中的题进行导入:
代码:
<el-tab-pane label="题库"> <el-tree ref="tree" highlight-current :data="treeList" node-key="id" :current-node-key="currentNodekey" @node-click="handleNodeClick" :load="loadNode" :props=" { label: 'name', value: 'id', isLeaf: 'isLeaf' }," lazy> <span slot-scope="{node}"> <el-tooltip v-if="node.label.length>=8" class="item" effect="dark" :content="node.label" placement="top"> <div class="text-ellipsis width-150">{{ node.label }}</div> </el-tooltip> <div v-else>{{ node.label }}</div> </span> </el-tree> </el-tab-pane>方法:
handleNodeClick (node) { if (node.level === 2) { //点击子节点(叶子节点) this.$nextTick(() => { this.$refs.tree.setCurrentKey(node.id) }) const data = JSON.parse(JSON.stringify(node)) data.answer = JSON.parse(node.answer) data.uid = `${data.componentType}-${Math.floor(Math.random() * 1000000)}` this.pushComponent(data, 1) } else { this.$nextTick(() => { this.$refs.tree.setCurrentKey() }) } },loadNode (node, resolve) { if (node.level === 0) { this.$api.pxExam.getExamSetList({ name: this.fuzzy }).then(res => { this.treeList = res.map(res => { return { name: res.name, id: res.id, isLeaf: false, level: 1 } }) return resolve(this.treeList) }) }pushComponent方法通用的(传参不同),上面写的有。