2026-06-18 10:52:42 +08:00
|
|
|
|
<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>
|