Files
xk-doctor-wx/subPackages/sub_salesperson/components/RegionAddressPicker.vue

245 lines
8.1 KiB
Vue
Raw Normal View History

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>