245 lines
8.1 KiB
Vue
245 lines
8.1 KiB
Vue
<template>
|
||
|
||
<view class="picker-wrap">
|
||
|
||
<view class="region-trigger" @click="openDrawer">
|
||
|
||
<text :class="displayText ? 'region-text' : 'region-placeholder'">{{ displayText || placeholder }}</text>
|
||
|
||
<u-icon name="arrow-right" color="#c0c4cc" size="28"></u-icon>
|
||
|
||
</view>
|
||
|
||
|
||
|
||
<drawer-page-container v-model="show" title="选择省市区">
|
||
|
||
<view class="drawer-body" :style="drawerBodyStyle">
|
||
|
||
<view class="search-box">
|
||
|
||
<u-input v-model="keyword" placeholder="搜索省/市/区" clearable :adjust-position="false" />
|
||
|
||
</view>
|
||
|
||
<scroll-view scroll-x class="crumb-scroll">
|
||
|
||
<view class="crumbs">
|
||
|
||
<text
|
||
|
||
v-for="(item, idx) in breadcrumbs"
|
||
|
||
:key="item.rowKey"
|
||
|
||
class="crumb"
|
||
|
||
@click="onCrumb(item)"
|
||
|
||
>{{ item.label }}<text v-if="idx < breadcrumbs.length - 1" class="crumb-sep"> / </text></text>
|
||
|
||
<text v-if="!breadcrumbs.length" class="crumb muted">请选择</text>
|
||
|
||
</view>
|
||
|
||
</scroll-view>
|
||
|
||
<scroll-view scroll-y class="list-scroll" :style="listScrollStyle">
|
||
|
||
<view
|
||
|
||
v-for="row in listRows"
|
||
|
||
:key="row.rowKey"
|
||
|
||
class="list-row"
|
||
|
||
@click="onPickRow(row)"
|
||
|
||
>
|
||
|
||
<text class="row-label">{{ isSearching ? row.label : row.node.label }}</text>
|
||
|
||
<text v-if="!isSearching && rowHasNextLevel(row, ctx)" class="row-arrow">›</text>
|
||
|
||
</view>
|
||
|
||
<view v-if="!listRows.length" class="empty">无匹配结果</view>
|
||
|
||
</scroll-view>
|
||
|
||
</view>
|
||
|
||
</drawer-page-container>
|
||
|
||
</view>
|
||
|
||
</template>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
import DrawerPageContainer from '@/subPackages/sub_salesperson/components/DrawerPageContainer.vue'
|
||
|
||
import {
|
||
|
||
formatAddressDisplay,
|
||
|
||
resolveAddressValue,
|
||
|
||
buildAddressOutput,
|
||
|
||
buildBreadcrumbFromContext,
|
||
|
||
getInitialPickerLevel,
|
||
|
||
levelOptionsToRows,
|
||
|
||
searchAddressHits,
|
||
|
||
searchHitsToRows,
|
||
|
||
applySearchHit,
|
||
|
||
rowHasNextLevel,
|
||
|
||
truncateContextToBreadcrumb,
|
||
|
||
getNextLevel,
|
||
|
||
canCompleteAfterSelect,
|
||
|
||
} from '@/subPackages/sub_clinic_salesperson/utils/address-index.js'
|
||
|
||
|
||
|
||
export default {
|
||
|
||
name: 'RegionAddressPicker',
|
||
|
||
components: { DrawerPageContainer },
|
||
|
||
props: {
|
||
|
||
value: { type: Array, default: () => [] },
|
||
|
||
placeholder: { type: String, default: '请选择省市区' },
|
||
|
||
},
|
||
|
||
data() {
|
||
|
||
return {
|
||
|
||
show: false,
|
||
|
||
keyword: '',
|
||
|
||
currentLevel: 'province',
|
||
|
||
ctx: { provinceId: undefined, cityId: undefined, districtId: undefined },
|
||
|
||
keyboardHeight: 0,
|
||
|
||
}
|
||
|
||
},
|
||
|
||
computed: {
|
||
|
||
displayText() {
|
||
|
||
return formatAddressDisplay(this.value)
|
||
|
||
},
|
||
|
||
isSearching() {
|
||
|
||
return !!(this.keyword || '').trim()
|
||
|
||
},
|
||
|
||
breadcrumbs() {
|
||
|
||
return buildBreadcrumbFromContext(this.ctx).map((item) => ({
|
||
|
||
...item,
|
||
|
||
rowKey: item.level + '-' + item.id,
|
||
|
||
}))
|
||
|
||
},
|
||
|
||
listRows() {
|
||
|
||
const rows = this.isSearching
|
||
|
||
? searchHitsToRows(searchAddressHits(this.keyword, this.ctx))
|
||
|
||
: levelOptionsToRows(this.currentLevel, this.ctx)
|
||
|
||
return rows.map((row) => ({
|
||
|
||
...row,
|
||
|
||
rowKey: row.level + '-' + row.node.value,
|
||
|
||
}))
|
||
|
||
},
|
||
|
||
drawerBodyStyle() {
|
||
|
||
if (!this.keyboardHeight) {
|
||
|
||
return {}
|
||
|
||
}
|
||
|
||
return {
|
||
|
||
paddingBottom: this.keyboardHeight + 'px',
|
||
|
||
maxHeight: `calc(70vh - ${this.keyboardHeight}px)`,
|
||
|
||
}
|
||
|
||
},
|
||
|
||
listScrollStyle() {
|
||
|
||
if (!this.keyboardHeight) {
|
||
|
||
return {}
|
||
|
||
}
|
||
|
||
return {
|
||
|
||
maxHeight: `calc(52vh - ${this.keyboardHeight}px)`,
|
||
|
||
minHeight: '240rpx',
|
||
|
||
}
|
||
|
||
},
|
||
|
||
},
|
||
|
||
watch: {
|
||
|
||
show(val) {
|
||
|
||
if (val) {
|
||
|
||
this.bindKeyboardListener()
|
||
|
||
return
|
||
|
||
}
|
||
|
||
this.keyboardHeight = 0
|
||
|
||
this.unbindKeyboardListener()
|
||
|
||
},
|
||
|
||
},
|
||
|
||
beforeDestroy() {
|
||
|
||
this.unbindKeyboardListener()
|
||
|
||
},
|
||
|
||
methods: {
|
||
|
||
rowHasNextLevel,
|
||
|
||
bindKeyboardListener() {
|
||
|
||
if (this._keyboardBound) return
|
||
|
||
this._keyboardBound = true
|
||
|
||
this._keyboardHandler = (res) => {
|
||
|
||
this.keyboardHeight = res.height || 0
|
||
|
||
}
|
||
|
||
uni.onKeyboardHeightChange(this._keyboardHandler)
|
||
|
||
},
|
||
|
||
unbindKeyboardListener() {
|
||
|
||
if (!this._keyboardBound || !this._keyboardHandler) return
|
||
|
||
if (typeof uni.offKeyboardHeightChange === 'function') {
|
||
|
||
uni.offKeyboardHeightChange(this._keyboardHandler)
|
||
|
||
}
|
||
|
||
this._keyboardBound = false
|
||
|
||
this._keyboardHandler = null
|
||
|
||
},
|
||
|
||
syncFromValue() {
|
||
|
||
const resolved = resolveAddressValue(this.value)
|
||
|
||
this.ctx = {
|
||
|
||
provinceId: resolved.provinceId,
|
||
|
||
cityId: resolved.cityId,
|
||
|
||
districtId: resolved.districtId,
|
||
|
||
}
|
||
|
||
this.currentLevel = getInitialPickerLevel(resolved)
|
||
|
||
this.keyword = ''
|
||
|
||
},
|
||
|
||
openDrawer() {
|
||
|
||
this.syncFromValue()
|
||
|
||
this.show = true
|
||
|
||
},
|
||
|
||
emitValue() {
|
||
|
||
const out = buildAddressOutput(this.ctx.provinceId, this.ctx.cityId, this.ctx.districtId)
|
||
|
||
if (out) this.$emit('change', out)
|
||
|
||
},
|
||
|
||
onCrumb(item) {
|
||
|
||
this.ctx = truncateContextToBreadcrumb(this.ctx, item)
|
||
|
||
this.currentLevel = item.level
|
||
|
||
this.keyword = ''
|
||
|
||
},
|
||
|
||
onPickRow(row) {
|
||
|
||
if (this.isSearching) {
|
||
|
||
const hit = searchAddressHits(this.keyword, this.ctx).find(
|
||
|
||
(h) => h.level === row.level && h.node.value === row.node.value
|
||
|
||
)
|
||
|
||
if (!hit) return
|
||
|
||
this.ctx = applySearchHit(hit, this.ctx)
|
||
|
||
if (hit.complete) {
|
||
|
||
this.emitValue()
|
||
|
||
this.show = false
|
||
|
||
return
|
||
|
||
}
|
||
|
||
const next = getNextLevel(hit.level, this.ctx)
|
||
|
||
this.currentLevel = next || hit.level
|
||
|
||
this.keyword = ''
|
||
|
||
return
|
||
|
||
}
|
||
|
||
|
||
|
||
const pickCtx = row.level === 'city'
|
||
|
||
? { provinceId: this.ctx.provinceId, cityId: row.node.value }
|
||
|
||
: row.level === 'district'
|
||
|
||
? { provinceId: this.ctx.provinceId, cityId: this.ctx.cityId, districtId: row.node.value }
|
||
|
||
: { provinceId: row.node.value, cityId: undefined, districtId: undefined }
|
||
|
||
|
||
|
||
if (row.level === 'province') {
|
||
|
||
this.ctx = { provinceId: row.node.value, cityId: undefined, districtId: undefined }
|
||
|
||
this.currentLevel = getNextLevel('province', this.ctx) || 'province'
|
||
|
||
} else if (row.level === 'city') {
|
||
|
||
this.ctx = { ...this.ctx, cityId: row.node.value, districtId: undefined }
|
||
|
||
if (canCompleteAfterSelect('city', pickCtx, row.node)) {
|
||
|
||
this.emitValue()
|
||
|
||
this.show = false
|
||
|
||
return
|
||
|
||
}
|
||
|
||
this.currentLevel = getNextLevel('city', this.ctx) || 'city'
|
||
|
||
} else if (row.level === 'district') {
|
||
|
||
this.ctx = { ...this.ctx, districtId: row.node.value }
|
||
|
||
this.emitValue()
|
||
|
||
this.show = false
|
||
|
||
}
|
||
|
||
},
|
||
|
||
},
|
||
|
||
}
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<style scoped>
|
||
|
||
.picker-wrap { width: 100%; }
|
||
|
||
.region-trigger {
|
||
|
||
display: flex;
|
||
|
||
align-items: center;
|
||
|
||
justify-content: space-between;
|
||
|
||
width: 100%;
|
||
|
||
min-height: 72rpx;
|
||
|
||
}
|
||
|
||
.region-text { font-size: 28rpx; color: #303133; flex: 1; }
|
||
|
||
.region-placeholder { font-size: 28rpx; color: #c0c4cc; flex: 1; }
|
||
|
||
.drawer-body { display: flex; flex-direction: column; max-height: 70vh; min-height: 50vh; box-sizing: border-box; }
|
||
|
||
.search-box { padding: 16rpx 24rpx; flex-shrink: 0; }
|
||
|
||
.crumb-scroll { white-space: nowrap; padding: 0 24rpx 16rpx; flex-shrink: 0; }
|
||
|
||
.crumbs { display: inline-flex; flex-wrap: nowrap; }
|
||
|
||
.crumb { font-size: 26rpx; color: #6acdbb; }
|
||
|
||
.crumb.muted { color: #86909c; }
|
||
|
||
.crumb-sep { color: #c0c4cc; }
|
||
|
||
.list-scroll { flex: 1; max-height: 52vh; min-height: 240rpx; box-sizing: border-box; }
|
||
|
||
.list-row {
|
||
|
||
display: flex;
|
||
|
||
align-items: center;
|
||
|
||
justify-content: space-between;
|
||
|
||
padding: 28rpx 32rpx;
|
||
|
||
border-bottom: 1rpx solid #f5f5f5;
|
||
|
||
}
|
||
|
||
.row-label { font-size: 28rpx; color: #1d2129; flex: 1; }
|
||
|
||
.row-arrow { font-size: 36rpx; color: #c0c4cc; }
|
||
|
||
.empty { text-align: center; color: #86909c; padding: 80rpx 0; font-size: 28rpx; }
|
||
|
||
</style>
|
||
|