parent
5794f76fe0
commit
8954fe89c0
@ -1,217 +0,0 @@ |
||||
<template> |
||||
<div> |
||||
<slot name="reference"/> |
||||
<transition :name="transition" @after-enter="handleAfterEnter" @after-leave="handleAfterLeave"> |
||||
<div |
||||
ref="popper" |
||||
v-show="!disabled && showPopper" |
||||
class="el-popover el-popper" |
||||
:class="[popperClass, content && 'el-popover--plain']" |
||||
:style="{ width: width + 'px' }" |
||||
:id="tooltipId" |
||||
> |
||||
<div class="el-popover__title" v-if="title"> |
||||
<slot name="title">{{title}}</slot> |
||||
</div> |
||||
<slot>{{ content }}</slot> |
||||
</div> |
||||
</transition> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import Popper from '@ele/mixins/vue-popper' |
||||
import {on, off} from 'element-ui/lib/utils/dom' |
||||
import {addClass, removeClass} from 'element-ui/lib/utils/dom' |
||||
import {generateId} from 'element-ui/lib/utils/util' |
||||
|
||||
export default { |
||||
name: "ElPopover", |
||||
|
||||
mixins: [Popper], |
||||
|
||||
props: { |
||||
trigger: { |
||||
type: String, |
||||
default: 'click', |
||||
validator: value => ['click', 'focus', 'hover', 'manual'].indexOf(value) > -1 |
||||
}, |
||||
openDelay: {type: Number, default: 0}, |
||||
closeDelay: {type: Number, default: 200}, |
||||
transition: {type: String, default: 'fade-in-liner'}, |
||||
popperClass: String, |
||||
title: String, |
||||
disabled: Boolean, |
||||
content: String, |
||||
width: {}, |
||||
arrowOffset: {type: Number, default: 0} |
||||
}, |
||||
|
||||
data() { |
||||
return {} |
||||
}, |
||||
|
||||
computed: { |
||||
tooltipId() { |
||||
return `el-popover-${generateId()}` |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
showPopper(val) { |
||||
if (this.disabled) return |
||||
this.$emit(val ? 'show' : 'hide') |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
doToggle() { |
||||
this.showPopper = !this.showPopper |
||||
}, |
||||
|
||||
doShow() { |
||||
this.showPopper = true |
||||
}, |
||||
|
||||
doClose() { |
||||
this.showPopper = false |
||||
}, |
||||
|
||||
handleFocus() { |
||||
addClass(this.referenceElm, 'focusing') |
||||
if (this.trigger === 'click' || this.trigger === 'focus') this.showPopper = true |
||||
}, |
||||
|
||||
handleClick() { |
||||
removeClass(this.referenceElm, 'focusing') |
||||
}, |
||||
|
||||
handleBlur() { |
||||
removeClass(this.referenceElm, 'focusing') |
||||
if (this.trigger === 'click' || this.trigger === 'focus') this.showPopper = false |
||||
}, |
||||
|
||||
handleMouseEnter() { |
||||
window.clearTimeout(this._timer) |
||||
if (this.openDelay) { |
||||
this._timer = setTimeout(() => this.showPopper = true, this.openDelay) |
||||
} |
||||
else this.showPopper = true |
||||
}, |
||||
|
||||
handleKeydown(ev) { |
||||
if (ev.keyCode === 27 && this.trigger !== 'manual') { // esc |
||||
this.doClose() |
||||
} |
||||
}, |
||||
|
||||
handleMouseLeave() { |
||||
window.clearTimeout(this._timer) |
||||
if (this.closeDelay) { |
||||
this._timer = setTimeout(() => this.showPopper = false, this.closeDelay) |
||||
} |
||||
else this.showPopper = false |
||||
}, |
||||
|
||||
handleDocumentClick(e) { |
||||
let reference = this.reference || this.$refs.reference |
||||
const popper = this.popper || this.$refs.popper |
||||
|
||||
if (!reference && this.$slots.reference && this.$slots.reference[0]) { |
||||
reference = this.referenceElm = this.$slots.reference[0].elm |
||||
} |
||||
if (!this.$el || |
||||
!reference || |
||||
this.$el.contains(e.target) || |
||||
reference.contains(e.target) || |
||||
!popper || |
||||
popper.contains(e.target)) return |
||||
this.showPopper = false |
||||
}, |
||||
|
||||
handleAfterEnter() { |
||||
this.$emit('after-enter') |
||||
}, |
||||
|
||||
handleAfterLeave() { |
||||
this.$emit('after-leave') |
||||
this.doDestroy() |
||||
}, |
||||
|
||||
cleanup() { |
||||
if (this.openDelay || this.closeDelay) { |
||||
window.clearTimeout(this._timer) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
mounted() { |
||||
let reference = this.referenceElm = this.reference || this.$refs.reference |
||||
const popper = this.popper || this.$refs.popper |
||||
|
||||
if (!reference && this.$slots.reference && this.$slots.reference[0]) { |
||||
reference = this.referenceElm = this.$slots.reference[0].elm |
||||
} |
||||
|
||||
if (reference) { |
||||
addClass(reference, 'el-popover__reference') |
||||
|
||||
if (this.trigger !== 'click') { |
||||
on(reference, 'focusin', () => { |
||||
this.handleFocus() |
||||
const instance = reference.__vue__ |
||||
if (instance && typeof instance.focus === 'function') { |
||||
instance.focus() |
||||
} |
||||
}) |
||||
on(popper, 'focusin', this.handleFocus) |
||||
on(reference, 'focusout', this.handleBlur) |
||||
on(popper, 'focusout', this.handleBlur) |
||||
} |
||||
on(reference, 'keydown', this.handleKeydown) |
||||
on(reference, 'click', this.handleClick) |
||||
} |
||||
if (this.trigger === 'click') { |
||||
on(reference, 'click', this.doToggle) |
||||
on(document, 'click', this.handleDocumentClick) |
||||
} |
||||
else if (this.trigger === 'hover') { |
||||
on(reference, 'mouseenter', this.handleMouseEnter) |
||||
on(popper, 'mouseenter', this.handleMouseEnter) |
||||
on(reference, 'mouseleave', this.handleMouseLeave) |
||||
on(popper, 'mouseleave', this.handleMouseLeave) |
||||
} |
||||
else if (this.trigger === 'focus') { |
||||
if (reference.querySelector('input, textarea')) { |
||||
on(reference, 'focusin', this.doShow) |
||||
on(reference, 'focusout', this.doClose) |
||||
} |
||||
else { |
||||
on(reference, 'mousedown', this.doShow) |
||||
on(reference, 'mouseup', this.doClose) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
beforeDestroy() { |
||||
this.cleanup() |
||||
|
||||
const reference = this.reference |
||||
|
||||
off(reference, 'click', this.doToggle) |
||||
off(reference, 'mouseup', this.doClose) |
||||
off(reference, 'mousedown', this.doShow) |
||||
off(reference, 'focusin', this.doShow) |
||||
off(reference, 'focusout', this.doClose) |
||||
off(reference, 'mousedown', this.doShow) |
||||
off(reference, 'mouseup', this.doClose) |
||||
off(reference, 'mouseleave', this.handleMouseLeave) |
||||
off(reference, 'mouseenter', this.handleMouseEnter) |
||||
off(document, 'click', this.handleDocumentClick) |
||||
}, |
||||
|
||||
deactivated() { |
||||
this.cleanup() |
||||
}, |
||||
} |
||||
</script> |
||||
@ -0,0 +1,263 @@ |
||||
<template> |
||||
<el-select |
||||
ref="select" |
||||
:value="value" |
||||
:disabled="readonly" |
||||
:size="size" |
||||
clearable |
||||
@clear="() => $emit('clear')" |
||||
@visible-change="visibleChange" |
||||
> |
||||
<template slot="empty"> |
||||
<div class="rg-header"> |
||||
<span>行政区划选择器</span> |
||||
<button class="rg-removeall-button" type="button" title="清除已选" @click="removeAll"> |
||||
<i class="el-icon-delete"/> |
||||
</button> |
||||
<button class="rg-done-button" type="button" title="完成" @click="done"> |
||||
<i class="el-icon-check"/> |
||||
</button> |
||||
</div> |
||||
<div class="rg-search"> |
||||
<input |
||||
ref="searchInput" |
||||
v-model="searchText" |
||||
class="rg-input" |
||||
type="text" |
||||
placeholder="输入地区id或名称,回车搜索" |
||||
@keyup.enter="search" |
||||
> |
||||
</div> |
||||
<div class="rg-level-tabs"> |
||||
<ul> |
||||
<li v-for="(tab,index) in selected" :key="tab.name" :class="{active:index+1 === currentLevel}"> |
||||
<a href="javascript:" @click="()=>clickTab(index,tab)">{{tab.name}}</a> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
<div v-loading="loading" class="rg-results-container"> |
||||
<ul class="rg-results"> |
||||
<li |
||||
v-for="item in items" |
||||
:key="item.id" |
||||
:class="{'rg-item': true, 'active': isItemActive(item.id)}" |
||||
@click="()=>clickItem(item)" |
||||
> |
||||
{{limit ? item.name + `(${item.value})` : item.name}} |
||||
</li> |
||||
<li v-if="items.length === 0" class="rg-message-box">无匹配项目</li> |
||||
</ul> |
||||
</div> |
||||
</template> |
||||
</el-select> |
||||
</template> |
||||
|
||||
<script> |
||||
import {createLimitTree, getNodeId} from "@/utils/tree" |
||||
import common from '../mixin' |
||||
|
||||
const PROVINCE_CITIES = ['北京市', '天津市', '上海市', '重庆市'] |
||||
const DEFAULT_TABS = [{name: '省/直辖市'}, {name: '市'}, {name: '区/县'}, {name: '乡/镇/街道'}] |
||||
const DEFAULT_MAX_LEVEL = 4 |
||||
|
||||
export default { |
||||
name: "RegionTabSelector", |
||||
|
||||
mixins: [common], |
||||
|
||||
props: { |
||||
value: [String, Array], |
||||
separation: {type: String, default: ','} |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
loading: false, |
||||
searchText: '', |
||||
currentLevel: 1, |
||||
maxLevel: DEFAULT_MAX_LEVEL, |
||||
selected: [], |
||||
treeData: [], |
||||
items: [] |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
regionTree() { |
||||
return this.$store.state.dataCache.regionTree |
||||
}, |
||||
}, |
||||
|
||||
watch: { |
||||
currentLevel(v) { |
||||
this.setItems(v) |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
//select的下拉框展开收起 |
||||
visibleChange(v) { |
||||
if (v) { |
||||
this.transformValueToSelected() |
||||
this.$nextTick(() => this.$refs.searchInput.focus()) |
||||
} |
||||
else this.removeAll() |
||||
}, |
||||
|
||||
search() { |
||||
const parent = this.selected[this.currentLevel - 2] || {children: this.treeData} |
||||
|
||||
if (!this.searchText) return this.items = parent.children |
||||
|
||||
const predicate = item => item.id.startsWith(this.searchText) || item.name.startsWith(this.searchText) |
||||
|
||||
this.items = parent.children.filter(predicate) |
||||
}, |
||||
|
||||
clickTab(index, tab) { |
||||
//点击当前激活的tab时不处理 |
||||
if (index + 1 === this.currentLevel) return |
||||
|
||||
this.currentLevel = index + 1 |
||||
}, |
||||
|
||||
clickItem(item) { |
||||
this.setTabs(item, this.currentLevel) |
||||
|
||||
this.nextTab() |
||||
}, |
||||
|
||||
setTabs(item, level) { |
||||
const prev = this.selected.slice(0, level - 1) |
||||
prev.push(item) |
||||
const next = JSON.parse(JSON.stringify(DEFAULT_TABS)) |
||||
|
||||
if (level === 1) { |
||||
//选择直辖市的时候,最大深度-1,移除'市'tab |
||||
if (PROVINCE_CITIES.includes(item.name)) { |
||||
next.splice(1, 1) |
||||
this.maxLevel = DEFAULT_MAX_LEVEL - 1 |
||||
} |
||||
else this.maxLevel = DEFAULT_MAX_LEVEL |
||||
} |
||||
|
||||
this.selected = prev.concat(next.slice(level, this.maxLevel)) |
||||
}, |
||||
|
||||
nextTab() { |
||||
this.searchText = '' |
||||
//若选择的是最后一级节点,直接完成 |
||||
if (this.currentLevel >= this.maxLevel) { |
||||
this.done() |
||||
} |
||||
else this.currentLevel++ |
||||
}, |
||||
|
||||
//判断是否是已选项 |
||||
isItemActive(id) { |
||||
const selected = this.selected[this.currentLevel - 1] |
||||
return selected && selected.id === id |
||||
}, |
||||
|
||||
//根据激活的tab改变显示的行政区划 |
||||
setItems(level) { |
||||
//顶级节点返回完整的树 |
||||
if (level === 1) this.items = this.treeData |
||||
|
||||
//否则返回上一级节点的children |
||||
else { |
||||
const parent = this.selected[level - 2] |
||||
this.items = parent ? parent.children || [] : [] |
||||
} |
||||
}, |
||||
|
||||
//将传入的value转换为已选项 |
||||
transformValueToSelected() { |
||||
if (!this.value) return |
||||
|
||||
const result = [], |
||||
loopArray = Array.isArray(this.value) |
||||
? this.value |
||||
: this.value.split(this.separation).filter(Boolean) |
||||
|
||||
for (let i = 0; i < loopArray.length; i++) { |
||||
const str = loopArray[i], |
||||
parent = result[i - 1] || {children: this.treeData} |
||||
|
||||
const node = parent.children.find(predicate(str)) |
||||
if (!node) break |
||||
result.push(node) |
||||
} |
||||
|
||||
result.forEach((item, index) => this.setTabs(item, index + 1)) |
||||
}, |
||||
|
||||
removeAll() { |
||||
this.maxLevel = DEFAULT_MAX_LEVEL |
||||
this.selected = JSON.parse(JSON.stringify(DEFAULT_TABS)) |
||||
this.currentLevel = 1 |
||||
}, |
||||
|
||||
done() { |
||||
const result = this.selected.filter(item => item.id) |
||||
this.$emit('input', result.map(item => item.name).join(this.separation)) |
||||
|
||||
if (result.length === 0) { |
||||
this.$emit('select', {}, []) |
||||
return this.$refs.select.blur() |
||||
} |
||||
|
||||
const node = result[result.length - 1] |
||||
const payload = [node] |
||||
|
||||
if (this.getChildrenOnSelect) { |
||||
const ids = getNodeId(node.children) |
||||
ids.unshift(node.id) |
||||
payload.push(ids) |
||||
} |
||||
|
||||
this.$emit('select', ...payload) |
||||
|
||||
this.$refs.select.blur() |
||||
}, |
||||
|
||||
init() { |
||||
this.loading = true |
||||
const hasInit = this.regionTree.length > 0 |
||||
const promise = () => hasInit ? Promise.resolve() : this.$store.dispatch('dataCache/initRegion') |
||||
return promise() |
||||
.then(() => this.limit ? this.limitApi() : Promise.resolve()) |
||||
.then(data => { |
||||
this.treeData = data ? createLimitTree(this.regionTree, data) : this.regionTree |
||||
this.setItems(this.currentLevel) |
||||
}) |
||||
.finally(() => this.loading = false) |
||||
} |
||||
}, |
||||
|
||||
created() { |
||||
this.selected = JSON.parse(JSON.stringify(DEFAULT_TABS)) |
||||
}, |
||||
|
||||
mounted() { |
||||
this.init() |
||||
} |
||||
} |
||||
|
||||
//根据node的类型生成查找断言,支持{id:'10',...}、{name:'北京市',...}、'10'、'北京市' 等四种类型 |
||||
function predicate(node) { |
||||
if (typeof node === 'object') { |
||||
const key = node.hasOwnProperty('id') ? 'id' : 'name' |
||||
return item => item[key] === node[key] |
||||
} |
||||
else { |
||||
const firstCharCode = node.charCodeAt(0) |
||||
const key = 48 <= firstCharCode && firstCharCode <= 57 ? 'id' : 'name' |
||||
return item => item[key] === node |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
@import "./style.scss"; |
||||
</style> |
||||
@ -0,0 +1,178 @@ |
||||
.rg-header, .rg-search { |
||||
padding: 2px 10px 0; |
||||
background-color: white; |
||||
} |
||||
|
||||
.rg-header { |
||||
h3 { |
||||
line-height: normal; |
||||
padding: 6px 80px 10px 10px; |
||||
margin: 0; |
||||
text-align: left; |
||||
color: #24292e; |
||||
font-size: 16px; |
||||
white-space: nowrap; |
||||
|
||||
.rg-header-selected { |
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
overflow: hidden; |
||||
font-size: 14px; |
||||
max-width: 310px; |
||||
display: inline-block; |
||||
} |
||||
} |
||||
|
||||
button { |
||||
position: absolute; |
||||
-webkit-appearance: none; |
||||
padding: 0; |
||||
cursor: pointer; |
||||
background: 0 0; |
||||
border: 0; |
||||
outline: none; |
||||
color: #999; |
||||
top: 9px; |
||||
|
||||
i { |
||||
font-size: 16px; |
||||
} |
||||
|
||||
&:hover { |
||||
color: black; |
||||
} |
||||
|
||||
&.rg-removeall-button { |
||||
right: 32px |
||||
} |
||||
|
||||
&.rg-done-button { |
||||
right: 8px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.rg-level-tabs { |
||||
margin-top: 10px; |
||||
background-color: #fff; |
||||
|
||||
ul { |
||||
padding: 0; |
||||
margin: 0; |
||||
line-height: 1.5; |
||||
border-bottom: 1px solid #eee; |
||||
|
||||
li { |
||||
display: inline-block; |
||||
position: relative; |
||||
|
||||
a { |
||||
display: block; |
||||
padding: 0.2rem 1rem 0.6rem; |
||||
font-size: 14px; |
||||
color: #bbb; |
||||
text-decoration: none; |
||||
cursor: pointer; |
||||
line-height: 1.43; |
||||
} |
||||
|
||||
&.active { |
||||
a { |
||||
color: #333; |
||||
background-color: #fff; |
||||
font-weight: 600; |
||||
} |
||||
|
||||
&::after { |
||||
content: ""; |
||||
display: block; |
||||
position: absolute; |
||||
bottom: 0; |
||||
height: 0.2rem; |
||||
width: 100%; |
||||
background-color: #bbb; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.rg-results-container { |
||||
background-color: #fff; |
||||
list-style: none; |
||||
margin: 0; |
||||
padding: 0; |
||||
position: relative; |
||||
width: 100%; |
||||
overflow-y: auto; |
||||
overflow-x: hidden; |
||||
clear: both; |
||||
border-bottom-left-radius: 2px; |
||||
border-bottom-right-radius: 2px; |
||||
|
||||
.rg-results { |
||||
background-color: #fff; |
||||
list-style: none; |
||||
margin: 0; |
||||
padding: 5px; |
||||
width: 364px; |
||||
line-height: 1.5; |
||||
|
||||
li { |
||||
margin: 0; |
||||
overflow: hidden; |
||||
padding: 3px 10px; |
||||
position: relative; |
||||
text-align: left; |
||||
white-space: nowrap; |
||||
font-size: 14px; |
||||
color: black; |
||||
cursor: pointer; |
||||
|
||||
&.rg-item { |
||||
display: inline-block; |
||||
border-radius: 2px; |
||||
margin-right: 5px; |
||||
color: #777; |
||||
|
||||
&:hover { |
||||
color: black; |
||||
background-color: #F5F5F5; |
||||
} |
||||
|
||||
&.active { |
||||
background-color: #E4EAEE; |
||||
color: black; |
||||
} |
||||
} |
||||
|
||||
&.rg-message-box { |
||||
height: 30px; |
||||
line-height: 30px; |
||||
text-align: center; |
||||
box-sizing: content-box; |
||||
font-size: 14px; |
||||
cursor: default; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.rg-input { |
||||
display: block; |
||||
background-color: #f5f5f5; |
||||
margin: 0 !important; |
||||
border: 0; |
||||
width: 100%; |
||||
font-size: 14px; |
||||
line-height: 1.42; |
||||
padding: 4px 6px; |
||||
vertical-align: middle; |
||||
box-sizing: border-box; |
||||
outline: none !important; |
||||
border-radius: 2px; |
||||
|
||||
&::-webkit-input-placeholder { |
||||
color: #999; |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@ |
||||
<script> |
||||
import Tree from './Tree' |
||||
import Tab from './Tab' |
||||
|
||||
export default { |
||||
name: "RegionSelector", |
||||
|
||||
functional: true, |
||||
|
||||
props: { |
||||
type: {type: String, default: 'tab', validator: v => ['tree', 'tab'].includes(v)} |
||||
}, |
||||
|
||||
render(h, context) { |
||||
return h(context.props.type === 'tree' ? Tree : Tab, context.data) |
||||
} |
||||
} |
||||
</script> |
||||
@ -0,0 +1,10 @@ |
||||
export default { |
||||
props: { |
||||
value: String, |
||||
readonly: Boolean, |
||||
size: String, |
||||
getChildrenOnSelect: Boolean, |
||||
limit: Boolean, |
||||
limitApi: Function |
||||
} |
||||
} |
||||
@ -0,0 +1,41 @@ |
||||
<template> |
||||
<div style="width: 500px"> |
||||
<div class="tip-row"> |
||||
行政区划选择,<a href="https://github.com/modood/Administrative-divisions-of-China" target="_blank">数据由此获取</a> |
||||
</div> |
||||
<div class="tip-row"> |
||||
<p>树形</p> |
||||
<div> |
||||
<region-selector :value="value" type="tree" @clear="clear" @select="select"/> |
||||
</div> |
||||
</div> |
||||
<div class="tip-row"> |
||||
<p>选项卡型,支持v-model,<a href="https://github.com/TerryZ/v-region" target="_blank">样式搬的这个</a></p> |
||||
<div> |
||||
<region-selector v-model="value" @clear="clear"/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import RegionSelector from '@/components/RegionSelector' |
||||
|
||||
export default { |
||||
name: "regionSelectorExample", |
||||
components: {RegionSelector}, |
||||
data() { |
||||
return { |
||||
value: '北京市' |
||||
} |
||||
}, |
||||
methods: { |
||||
clear() { |
||||
this.value = null |
||||
}, |
||||
select(obj) { |
||||
this.value = obj.fullname |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
Loading…
Reference in new issue