467 lines
12 KiB
Vue
467 lines
12 KiB
Vue
|
<template>
|
||
|
<div :style="{ width: width }" class="lazy-cascader">
|
||
|
<!-- 禁用状态 -->
|
||
|
<div
|
||
|
v-if="disabled"
|
||
|
class="el-input__inner lazy-cascader-input lazy-cascader-input-disabled"
|
||
|
>
|
||
|
<span v-show="placeholderVisible" class="lazy-cascader-placeholder">
|
||
|
{{ placeholder }}
|
||
|
</span>
|
||
|
<div v-if="props.multiple" class="lazy-cascader-tags">
|
||
|
<el-tag
|
||
|
v-for="(item, index) in labelArray"
|
||
|
:key="index"
|
||
|
class="lazy-cascader-tag"
|
||
|
type="info"
|
||
|
disable-transitions
|
||
|
closable
|
||
|
>
|
||
|
<span> {{ item.label.join(separator) }}</span>
|
||
|
</el-tag>
|
||
|
</div>
|
||
|
<div v-else class="lazy-cascader-label">
|
||
|
<el-tooltip
|
||
|
:content="labelObject.label.join(separator)"
|
||
|
placement="top-start"
|
||
|
>
|
||
|
<span>{{ labelObject.label.join(separator) }}</span>
|
||
|
</el-tooltip>
|
||
|
</div>
|
||
|
</div>
|
||
|
<!-- 禁用状态 -->
|
||
|
<!-- 可选状态 -->
|
||
|
<el-popover v-else ref="popover" trigger="click" placement="bottom-start">
|
||
|
<!-- 搜索 -->
|
||
|
<div class="lazy-cascader-search">
|
||
|
<el-autocomplete
|
||
|
v-if="filterable"
|
||
|
:style="{ width: searchWidth || '100%' }"
|
||
|
:popper-class="suggestionsPopperClass"
|
||
|
v-model="keyword"
|
||
|
:fetch-suggestions="querySearch"
|
||
|
:trigger-on-focus="false"
|
||
|
class="inline-input"
|
||
|
prefix-icon="el-icon-search"
|
||
|
label="name"
|
||
|
placeholder="请输入"
|
||
|
@select="handleSelect"
|
||
|
@blur="isSearchEmpty = false"
|
||
|
>
|
||
|
<template slot-scope="{ item }">
|
||
|
<div :class="isChecked(item[props.value])" class="name">
|
||
|
{{ item[props.label].join(separator) }}
|
||
|
</div>
|
||
|
</template>
|
||
|
</el-autocomplete>
|
||
|
<div v-show="isSearchEmpty" class="empty">{{ searchEmptyText }}</div>
|
||
|
</div>
|
||
|
<!-- 搜索 -->
|
||
|
<!-- 级联面板 -->
|
||
|
<div class="lazy-cascader-panel">
|
||
|
<el-cascader-panel
|
||
|
ref="panel"
|
||
|
v-model="current"
|
||
|
:options="options"
|
||
|
:props="currentProps"
|
||
|
@change="change"
|
||
|
/>
|
||
|
</div>
|
||
|
<!-- 级联面板 -->
|
||
|
<!--内容区域-->
|
||
|
<div
|
||
|
slot="reference"
|
||
|
:class="disabled ? 'lazy-cascader-input-disabled' : ''"
|
||
|
class="el-input__inner lazy-cascader-input"
|
||
|
>
|
||
|
<span v-show="placeholderVisible" class="lazy-cascader-placeholder">
|
||
|
{{ placeholder }}
|
||
|
</span>
|
||
|
<div v-if="props.multiple" class="lazy-cascader-tags">
|
||
|
<el-tag
|
||
|
v-for="(item, index) in labelArray"
|
||
|
:key="index"
|
||
|
class="lazy-cascader-tag"
|
||
|
type="info"
|
||
|
size="small"
|
||
|
disable-transitions
|
||
|
closable
|
||
|
@close="handleClose(item)"
|
||
|
>
|
||
|
<span> {{ item.label.join(separator) }}</span>
|
||
|
</el-tag>
|
||
|
</div>
|
||
|
<div v-else class="lazy-cascader-label">
|
||
|
<el-tooltip
|
||
|
:content="labelObject.label.join(separator)"
|
||
|
placement="top-start"
|
||
|
>
|
||
|
<span>{{ labelObject.label.join(separator) }}</span>
|
||
|
</el-tooltip>
|
||
|
</div>
|
||
|
<span
|
||
|
v-if="clearable && current.length > 0"
|
||
|
class="lazy-cascader-clear"
|
||
|
@click.stop="clearBtnClick"
|
||
|
>
|
||
|
<i class="el-icon-close"/>
|
||
|
</span>
|
||
|
</div>
|
||
|
<!--内容区域-->
|
||
|
</el-popover>
|
||
|
<!-- 可选状态 -->
|
||
|
</div>
|
||
|
</template>
|
||
|
<script>
|
||
|
export default {
|
||
|
props: {
|
||
|
value: {
|
||
|
type: Array,
|
||
|
default: () => {
|
||
|
return []
|
||
|
}
|
||
|
},
|
||
|
separator: {
|
||
|
type: String,
|
||
|
default: ' > '
|
||
|
},
|
||
|
placeholder: {
|
||
|
type: String,
|
||
|
default: '请选择'
|
||
|
},
|
||
|
|
||
|
width: {
|
||
|
type: String,
|
||
|
default: '400px'
|
||
|
},
|
||
|
filterable: Boolean,
|
||
|
clearable: Boolean,
|
||
|
disabled: Boolean,
|
||
|
props: {
|
||
|
type: Object,
|
||
|
default: () => {
|
||
|
return {}
|
||
|
}
|
||
|
},
|
||
|
suggestionsPopperClass: {
|
||
|
type: String,
|
||
|
default: 'suggestions-popper-class'
|
||
|
},
|
||
|
searchWidth: {
|
||
|
type: String
|
||
|
},
|
||
|
searchEmptyText: {
|
||
|
type: String,
|
||
|
default: '暂无数据'
|
||
|
}
|
||
|
},
|
||
|
data() {
|
||
|
return {
|
||
|
isSearchEmpty: false,
|
||
|
keyword: '',
|
||
|
options: [],
|
||
|
current: [],
|
||
|
labelObject: { label: [], value: [] },
|
||
|
labelArray: [],
|
||
|
currentProps: {
|
||
|
multiple: this.props.multiple,
|
||
|
checkStrictly: this.props.checkStrictly,
|
||
|
value: this.props.value,
|
||
|
label: this.props.label,
|
||
|
leaf: this.props.leaf,
|
||
|
lazy: true,
|
||
|
lazyLoad: this.lazyLoad
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
computed: {
|
||
|
placeholderVisible() {
|
||
|
if (this.current) {
|
||
|
return this.current.length == 0
|
||
|
} else {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
watch: {
|
||
|
current() {
|
||
|
this.getLabelArray()
|
||
|
},
|
||
|
value(v) {
|
||
|
this.current = v
|
||
|
},
|
||
|
keyword() {
|
||
|
this.isSearchEmpty = false
|
||
|
}
|
||
|
},
|
||
|
created() {
|
||
|
this.initOptions()
|
||
|
},
|
||
|
methods: {
|
||
|
// 搜索是否选中
|
||
|
isChecked(value) {
|
||
|
// 多选
|
||
|
if (this.props.multiple) {
|
||
|
const index = this.current.findIndex(item => {
|
||
|
return item.join() == value.join()
|
||
|
})
|
||
|
if (index > -1) {
|
||
|
return 'el-link el-link--primary'
|
||
|
} else {
|
||
|
return ''
|
||
|
}
|
||
|
} else {
|
||
|
if (value.join() == this.current.join()) {
|
||
|
return 'el-link el-link--primary'
|
||
|
} else {
|
||
|
return ''
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
// 搜索
|
||
|
querySearch(query, callback) {
|
||
|
this.props.lazySearch(query, list => {
|
||
|
callback(list)
|
||
|
if (!list || !list.length) this.isSearchEmpty = true
|
||
|
})
|
||
|
},
|
||
|
// 选中搜索下拉搜索项
|
||
|
handleSelect(item) {
|
||
|
if (this.props.multiple) {
|
||
|
const index = this.current.findIndex(obj => {
|
||
|
return obj.join() == item[this.props.value].join()
|
||
|
})
|
||
|
if (index == -1) {
|
||
|
this.$refs.panel.clearCheckedNodes()
|
||
|
this.current.push(item[this.props.value])
|
||
|
this.$emit('change', this.current)
|
||
|
}
|
||
|
} else {
|
||
|
// 选中下拉选变更值
|
||
|
if (
|
||
|
this.current == null ||
|
||
|
item[this.props.value].join() !== this.current.join()
|
||
|
) {
|
||
|
this.$refs.panel.activePath = []
|
||
|
this.current = item[this.props.value]
|
||
|
this.$emit('change', this.current)
|
||
|
}
|
||
|
}
|
||
|
this.keyword = ''
|
||
|
},
|
||
|
// 初始化数据
|
||
|
async initOptions() {
|
||
|
this.props.lazyLoad(0, list => {
|
||
|
this.$set(this, 'options', list)
|
||
|
if (this.props.multiple) {
|
||
|
this.current = [...this.value]
|
||
|
} else {
|
||
|
this.current = this.value
|
||
|
}
|
||
|
})
|
||
|
},
|
||
|
async getLabelArray() {
|
||
|
if (this.props.multiple) {
|
||
|
const array = []
|
||
|
for (let i = 0; i < this.current.length; i++) {
|
||
|
const obj = await this.getObject(this.current[i])
|
||
|
array.push(obj)
|
||
|
}
|
||
|
this.labelArray = array
|
||
|
this.$emit('input', this.current)
|
||
|
if (!this.disabled) {
|
||
|
this.$nextTick(() => {
|
||
|
this.$refs.popover.updatePopper()
|
||
|
})
|
||
|
}
|
||
|
} else {
|
||
|
this.labelObject = await this.getObject(this.current || [])
|
||
|
this.$emit('input', this.current)
|
||
|
}
|
||
|
},
|
||
|
/** 格式化id=>object */
|
||
|
async getObject(id) {
|
||
|
try {
|
||
|
let options = this.options
|
||
|
const nameArray = []
|
||
|
for (let i = 0; i < id.length; i++) {
|
||
|
const index = options.findIndex(item => {
|
||
|
return item[this.props.value] == id[i]
|
||
|
})
|
||
|
nameArray.push(options[index][this.props.label])
|
||
|
if (i < id.length - 1 && options[index].children == undefined) {
|
||
|
const list = new Promise(resolve => {
|
||
|
this.props.lazyLoad(id[i], list => {
|
||
|
resolve(list)
|
||
|
})
|
||
|
})
|
||
|
this.$set(options[index], 'children', await list)
|
||
|
options = options[index].children
|
||
|
} else {
|
||
|
options = options[index].children
|
||
|
}
|
||
|
}
|
||
|
return { value: id, label: nameArray }
|
||
|
} catch (e) {
|
||
|
this.current = []
|
||
|
return { value: [], label: [] }
|
||
|
}
|
||
|
},
|
||
|
// 懒加载数据
|
||
|
async lazyLoad(node, resolve) {
|
||
|
let current = this.current
|
||
|
if (this.props.multiple) {
|
||
|
current = [...this.current]
|
||
|
}
|
||
|
if (node.root) {
|
||
|
resolve()
|
||
|
} else if (node.data[this.props.leaf]) {
|
||
|
resolve()
|
||
|
} else if (node.data.children) {
|
||
|
if (this.props.multiple) {
|
||
|
this.current = current
|
||
|
}
|
||
|
resolve()
|
||
|
} else {
|
||
|
this.props.lazyLoad(node.value, list => {
|
||
|
node.data.children = list
|
||
|
if (this.props.multiple) {
|
||
|
this.current = current
|
||
|
}
|
||
|
resolve(list)
|
||
|
})
|
||
|
}
|
||
|
},
|
||
|
// 删除多选值
|
||
|
/** 删除**/
|
||
|
handleClose(item) {
|
||
|
const index = this.current.findIndex(obj => {
|
||
|
return obj.join() == item.value.join()
|
||
|
})
|
||
|
if (index > -1) {
|
||
|
this.$refs.panel.clearCheckedNodes()
|
||
|
this.current.splice(index, 1)
|
||
|
this.$emit('change', this.current)
|
||
|
}
|
||
|
},
|
||
|
// 点击清空按钮
|
||
|
clearBtnClick() {
|
||
|
this.$refs.panel.clearCheckedNodes()
|
||
|
this.current = []
|
||
|
this.$emit('change', this.current)
|
||
|
},
|
||
|
change() {
|
||
|
this.$emit('change', this.current)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
</script>
|
||
|
<style lang="less">
|
||
|
.lazy-cascader {
|
||
|
display: inline-block;
|
||
|
width: 300px;
|
||
|
.lazy-cascader-input {
|
||
|
position: relative;
|
||
|
width: 100%;
|
||
|
background: #fff;
|
||
|
height: auto;
|
||
|
min-height: 36px;
|
||
|
padding: 5px;
|
||
|
line-height: 1;
|
||
|
cursor: pointer;
|
||
|
> .lazy-cascader-placeholder {
|
||
|
padding: 0 2px;
|
||
|
line-height: 28px;
|
||
|
color: #999;
|
||
|
font-size: 14px;
|
||
|
}
|
||
|
> .lazy-cascader-label {
|
||
|
padding: 0 2px;
|
||
|
line-height: 28px;
|
||
|
color: #606266;
|
||
|
font-size: 14px;
|
||
|
overflow: hidden;
|
||
|
text-overflow: ellipsis;
|
||
|
white-space: nowrap;
|
||
|
}
|
||
|
> .lazy-cascader-clear {
|
||
|
position: absolute;
|
||
|
right: 0;
|
||
|
top: 0;
|
||
|
display: inline-block;
|
||
|
width: 40px;
|
||
|
height: 40px;
|
||
|
text-align: center;
|
||
|
line-height: 40px;
|
||
|
}
|
||
|
}
|
||
|
.lazy-cascader-input-disabled {
|
||
|
background-color: #f5f7fa;
|
||
|
border-color: #e4e7ed;
|
||
|
color: #c0c4cc;
|
||
|
cursor: not-allowed;
|
||
|
> .lazy-cascader-label {
|
||
|
color: #c0c4cc;
|
||
|
}
|
||
|
> .lazy-cascader-placeholder {
|
||
|
color: #c0c4cc;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
.lazy-cascader-tag {
|
||
|
display: inline-flex;
|
||
|
align-items: center;
|
||
|
max-width: 100%;
|
||
|
margin: 2px;
|
||
|
text-overflow: ellipsis;
|
||
|
background: #f0f2f5;
|
||
|
> span {
|
||
|
flex: 1;
|
||
|
overflow: hidden;
|
||
|
text-overflow: ellipsis;
|
||
|
}
|
||
|
> .el-icon-close {
|
||
|
-webkit-box-flex: 0;
|
||
|
-ms-flex: none;
|
||
|
flex: none;
|
||
|
background-color: #c0c4cc;
|
||
|
color: #fff;
|
||
|
}
|
||
|
}
|
||
|
.lazy-cascader-panel {
|
||
|
margin-top: 10px;
|
||
|
display: inline-block;
|
||
|
}
|
||
|
.suggestions-popper-class {
|
||
|
width: auto !important;
|
||
|
min-width: 200px;
|
||
|
}
|
||
|
.lazy-cascader-search {
|
||
|
.empty {
|
||
|
width: calc(100% - 24px);
|
||
|
box-sizing: border-box;
|
||
|
background-color: #fff;
|
||
|
color: #999;
|
||
|
text-align: center;
|
||
|
position: absolute;
|
||
|
z-index: 999;
|
||
|
padding: 12px 0;
|
||
|
margin-top: 12px;
|
||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||
|
&:before {
|
||
|
content: "";
|
||
|
position: absolute;
|
||
|
top: -12px;
|
||
|
left: 36px;
|
||
|
width: 0;
|
||
|
height: 0;
|
||
|
border-left: 6px solid transparent;
|
||
|
border-right: 6px solid transparent;
|
||
|
border-top: 6px solid transparent;
|
||
|
border-bottom: 6px solid #fff;
|
||
|
filter: drop-shadow(0 -1px 2px rgba(0, 0, 0, 0.02));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
</style>
|