Files
xk-doctor-wx/subPackages/sub_salesperson/components/RegionAddressPicker.vue
2026-06-05 12:28:36 +08:00

197 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">
<view class="search-box">
<u-input v-model="keyword" placeholder="搜索省/市/区" clearable />
</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">
<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 },
}
},
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,
}))
},
},
methods: {
rowHasNextLevel,
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; }
.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; }
.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>