feat: 地址选择改走地区选择器接口

编辑地址用 page-container 抽屉 + regionPicker children/search,提交仍传省市区中文名。
This commit is contained in:
李琦
2026-10-09 10:03:18 +08:00
parent 84be1acf65
commit 5370dcec7f
2 changed files with 431 additions and 96 deletions

View File

@@ -0,0 +1,26 @@
import { get } from './http'
/**
* 省市区选择器(xk-api type=3)
* 取值:unwrapXkApi → code / result / message
* children/search 仅未软删;resolve 含软删以便回显
*/
/** 按 pid 拉一层子节点 xk-api GET /region-picker/children */
export async function getRegionChildren(pid = 0) {
return await get('/region-picker/children', { pid }, 3)
}
/** 地名/首拼/全拼合一搜索 xk-api GET /region-picker/search */
export async function searchRegions(keyword, limit = 30) {
return await get('/region-picker/search', { keyword, limit }, 3)
}
/** 按省+叶回显 xk-api GET /region-picker/resolve */
export async function resolveRegionPair(provinceId, cityId) {
return await get(
'/region-picker/resolve',
{ province_id: provinceId, city_id: cityId },
3,
)
}

View File

@@ -7,22 +7,18 @@
height:' 68rpx',borderRadio:'8rpx'}" placeholder-style="#999999" :border-bottom="false">
</u-field>
</view>
<view class="title">
<u-field v-model="mobile" label="手机号码" placeholder="请填写收货人手机号" placeholder-style="#999999"
:border-bottom="false" :field-style="{ background: '#F6F6F6',fontSize: '28rpx',paddingLeft:'30rpx',
height:' 68rpx',borderRadio:'8rpx'}">
</u-field>
</view>
<view class="title">
<u-field @click="showAction" v-model="region" :disabled="true" label="地区" placeholder="点击选择"
<u-field @click="openRegionPicker" v-model="region" :disabled="true" label="地区" placeholder="点击选择"
placeholder-style="#999999" :border-bottom="false" :field-style="{ background: '#F6F6F6',fontSize: '28rpx',paddingLeft:'30rpx',
height:' 68rpx',borderRadio:'8rpx'}">
</u-field>
<u-picker mode="region" v-model="show" :safe-area-inset-bottom="true" @confirm="confirm"></u-picker>
</view>
<view class="title">
<u-field v-model="location" label="地址" placeholder="如街道、门牌号、小区" :border-bottom="false"
placeholder-style="#999999" icon="map-fill" icon-color="#1777FF" :field-style="{ background: '#F6F6F6',fontSize: '28rpx',paddingLeft:'30rpx',
@@ -30,31 +26,24 @@ height:' 68rpx',borderRadio:'8rpx'}">
</u-field>
</view>
</view>
<view class="card" v-if="editShow">
<view class="title">
<u-field v-model="from[0].name" label="收货人" placeholder="请填写收货人名字" :field-style="{ background: '#F6F6F6',fontSize: '28rpx',paddingLeft:'30rpx',
height:' 68rpx',borderRadio:'8rpx'}" placeholder-style="#999999" :border-bottom="false">
</u-field>
</view>
<view class="title">
<u-field v-model="from[0].mobile" label="手机号码" placeholder="请填写收货人手机号" placeholder-style="#999999"
:border-bottom="false" :field-style="{ background: '#F6F6F6',fontSize: '28rpx',paddingLeft:'30rpx',
height:' 68rpx',borderRadio:'8rpx'}">
</u-field>
</view>
<view class="title">
<u-field @click="showAction" v-model="from[0].region" :disabled="true" label="地区" placeholder="点击选择"
<u-field @click="openRegionPicker" v-model="from[0].region" :disabled="true" label="地区" placeholder="点击选择"
placeholder-style="#999999" :border-bottom="false" :field-style="{ background: '#F6F6F6',fontSize: '28rpx',paddingLeft:'30rpx',
height:' 68rpx',borderRadio:'8rpx'}">
</u-field>
<u-picker mode="region" v-model="show" :safe-area-inset-bottom="true" @confirm="confirms"></u-picker>
</view>
<view class="title">
<u-field v-model="from[0].detail_address" label="地址" placeholder="如街道、门牌号、小区" :border-bottom="false"
placeholder-style="#999999" icon="map-fill" icon-color="#1777FF" :field-style="{ background: '#F6F6F6',fontSize: '28rpx',paddingLeft:'30rpx',
@@ -62,28 +51,86 @@ height:' 68rpx',borderRadio:'8rpx'}">
</u-field>
</view>
</view>
<view class="btn">
<view class="add_btn" v-if="editShow==false" @click="getAdd">
确定
</view>
<view class="eidt_btn" v-if="editShow" @click="getEdit">
保存
</view>
</view>
<!-- 省市区抽屉:page-container + v-if 防误退;直连 yii_region -->
<page-container
v-if="regionGuard"
:show="regionShow"
position="bottom"
round
overlay
custom-style="height:70%;"
@clickoverlay="closeRegionPicker"
@afterleave="onRegionAfterLeave"
>
<view class="region-drawer">
<view class="region-header">
<text class="region-title">选择省市区</text>
<view class="region-close" @click="closeRegionPicker">×</view>
</view>
<view class="search-box">
<input
class="search-input"
:value="keyword"
placeholder="地名/首拼/全拼"
confirm-type="search"
@input="onKeywordInput"
/>
</view>
<scroll-view scroll-x class="crumb-scroll">
<view class="crumbs">
<text
v-for="(item, idx) in breadcrumbs"
:key="item.key"
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-if="loading" class="empty">加载中...</view>
<view
v-for="row in listRows"
:key="row.key"
class="list-row"
@click="onPickRow(row)"
>
<text class="row-label">{{ row.label }}</text>
<text v-if="row.showArrow" class="row-arrow">›</text>
</view>
<view v-if="!loading && !listRows.length" class="empty">无匹配结果</view>
</scroll-view>
</view>
</page-container>
<u-toast ref="uToast" />
</view>
</template>
<script>
/**
* 收货地址编辑页:地区选择改为直连 xk-api region-picker(不再用 uView 国标包)
* 提交仍写中文 province/city/area/region,Yii 地址接口不变
*/
import {
getaddressAdd,
getaddressEdit,
getaddressList
} from '../../request/api/api'
import {
getRegionChildren,
searchRegions,
} from '@/request/api/regionPicker.js'
import { unwrapXkApi } from '@/utils/api-response.js'
import { assertPhone } from '@/utils/phone.js'
export default {
data() {
return {
@@ -91,26 +138,23 @@ height:' 68rpx',borderRadio:'8rpx'}">
mobile: "",
name: "",
region: "",
areaList: [{
text: '北京',
},
{
text: '杭州'
},
{
text: '福建'
}
],
show: false,
editShow: false,
id: "",
infoList: [],
from: {
},
province: "", // 省
city: "", // 市
area:"", // 区
from: {},
province: "",
city: "",
area: "",
// 选择器状态
regionGuard: false,
regionShow: false,
loading: false,
keyword: "",
currentLevel: "province",
ctx: { provinceId: 0, cityId: 0, districtId: 0 },
crumbLabels: { province: "", city: "", district: "" },
listRows: [],
_searchTimer: null,
}
},
onLoad(e) {
@@ -123,29 +167,246 @@ height:' 68rpx',borderRadio:'8rpx'}">
}
},
computed: {
isSearching() {
return !!(this.keyword || '').trim()
},
breadcrumbs() {
const items = []
if (this.ctx.provinceId) {
items.push({
level: 'province',
id: this.ctx.provinceId,
label: this.crumbLabels.province || String(this.ctx.provinceId),
key: 'p-' + this.ctx.provinceId,
})
}
if (this.ctx.cityId) {
items.push({
level: 'city',
id: this.ctx.cityId,
label: this.crumbLabels.city || String(this.ctx.cityId),
key: 'c-' + this.ctx.cityId,
})
}
if (this.ctx.districtId) {
items.push({
level: 'district',
id: this.ctx.districtId,
label: this.crumbLabels.district || String(this.ctx.districtId),
key: 'd-' + this.ctx.districtId,
})
}
return items
},
},
beforeDestroy() {
clearTimeout(this._searchTimer)
},
methods: {
showAction() {
this.show = true;
/** 打开省市区抽屉并重置到省级列表 */
openRegionPicker() {
this.ctx = { provinceId: 0, cityId: 0, districtId: 0 }
this.crumbLabels = { province: '', city: '', district: '' }
this.currentLevel = 'province'
this.keyword = ''
this.regionGuard = true
this.$nextTick(() => {
this.regionShow = true
this.reloadList()
})
},
confirm(e) {
// console.log(e, 'area');
// 省市区拼接
this.region = e.province.label + e.city.label + e.area.label
this.province = e.province.label
this.city = e.city.label
this.area = e.area.label
closeRegionPicker() {
this.regionShow = false
},
confirms(e) {
this.from[0].region = e.province.label + e.city.label + e.area.label
this.province = e.province.label
this.city = e.city.label
this.area = e.area.label
onRegionAfterLeave() {
this.regionGuard = false
this.listRows = []
this.keyword = ''
},
onKeywordInput(e) {
this.keyword = (e && e.detail && e.detail.value) || ''
clearTimeout(this._searchTimer)
this._searchTimer = setTimeout(() => {
this.reloadList()
}, 250)
},
/** 按当前层或搜索关键字刷新列表 */
async reloadList() {
this.loading = true
try {
if (this.isSearching) {
const res = await searchRegions(this.keyword, 30)
const { ok, payload } = unwrapXkApi(res)
const hits = ok && Array.isArray(payload) ? payload : []
this.listRows = hits.map((h) => ({
key: 's-' + h.id,
id: h.id,
name: h.name,
level: h.level,
pid: h.pid,
hasChildren: !h.complete,
complete: !!h.complete,
path: h.path || [],
pathLabel: h.path_label || h.name,
label: h.path_label || h.name,
showArrow: false,
isSearch: true,
}))
} else {
let pid = 0
if (this.currentLevel === 'city') pid = this.ctx.provinceId || 0
else if (this.currentLevel === 'district') pid = this.ctx.cityId || 0
const res = await getRegionChildren(pid)
const { ok, payload } = unwrapXkApi(res)
const rows = ok && Array.isArray(payload) ? payload : []
this.listRows = rows.map((r) => ({
key: 'n-' + r.id,
id: r.id,
name: r.name,
level: r.level,
pid: r.pid,
hasChildren: !!r.has_children,
complete: !r.has_children,
path: [],
pathLabel: r.name,
label: r.name,
showArrow: !!r.has_children,
isSearch: false,
}))
}
} finally {
this.loading = false
}
},
async onCrumb(item) {
if (item.level === 'province') {
this.ctx = { provinceId: item.id, cityId: 0, districtId: 0 }
this.crumbLabels = { province: item.label, city: '', district: '' }
this.currentLevel = 'city'
} else if (item.level === 'city') {
this.ctx = {
provinceId: this.ctx.provinceId,
cityId: item.id,
districtId: 0,
}
this.crumbLabels = {
province: this.crumbLabels.province,
city: item.label,
district: '',
}
this.currentLevel = 'district'
}
this.keyword = ''
await this.reloadList()
},
/** 选中一行:搜索命中或下钻/完成 */
async onPickRow(row) {
if (row.isSearch) {
const path = row.path || []
let provinceId = 0
let cityId = 0
let districtId = 0
let pName = ''
let cName = ''
let dName = ''
for (let i = 0; i < path.length; i++) {
const n = path[i]
if (n.level === 1) {
provinceId = n.id
pName = n.name
} else if (n.level === 2) {
cityId = n.id
cName = n.name
} else if (n.level === 3) {
districtId = n.id
dName = n.name
}
}
if (row.level === 1) {
provinceId = row.id
pName = row.name
} else if (row.level === 2) {
cityId = row.id
cName = row.name
if (!provinceId) provinceId = row.pid
} else {
districtId = row.id
dName = row.name
if (!cityId) cityId = row.pid
}
this.ctx = { provinceId, cityId, districtId }
this.crumbLabels = { province: pName, city: cName, district: dName }
if (row.complete) {
this.applyRegionNames(pName, cName, dName)
this.closeRegionPicker()
return
}
if (row.level === 1) this.currentLevel = 'city'
else if (row.level === 2) this.currentLevel = 'district'
this.keyword = ''
await this.reloadList()
return
}
if (this.currentLevel === 'province') {
this.ctx = { provinceId: row.id, cityId: 0, districtId: 0 }
this.crumbLabels = { province: row.name, city: '', district: '' }
if (row.complete) {
this.applyRegionNames(row.name, '', '')
this.closeRegionPicker()
return
}
this.currentLevel = 'city'
} else if (this.currentLevel === 'city') {
this.ctx = {
provinceId: this.ctx.provinceId,
cityId: row.id,
districtId: 0,
}
this.crumbLabels = {
province: this.crumbLabels.province,
city: row.name,
district: '',
}
if (row.complete) {
this.applyRegionNames(
this.crumbLabels.province,
row.name,
'',
)
this.closeRegionPicker()
return
}
this.currentLevel = 'district'
} else {
this.ctx = { ...this.ctx, districtId: row.id }
this.crumbLabels = {
...this.crumbLabels,
district: row.name,
}
this.applyRegionNames(
this.crumbLabels.province,
this.crumbLabels.city,
row.name,
)
this.closeRegionPicker()
return
}
this.keyword = ''
await this.reloadList()
},
/** 把中文省市区写回表单(Yii 地址接口仍要中文) */
applyRegionNames(province, city, area) {
this.province = province || ''
this.city = city || ''
this.area = area || ''
const text = [province, city, area].filter(Boolean).join('')
if (this.editShow && this.from[0]) {
this.from[0].region = text
} else {
this.region = text
}
},
// 新增地址
getAdd() {
// 收货手机号与后端一致:大陆 11 位号段
if (!assertPhone(this.mobile, '请填写收货人手机号', '收货人手机号格式不正确')) {
return
}
@@ -156,14 +417,13 @@ height:' 68rpx',borderRadio:'8rpx'}">
detail_address: this.location,
province: this.province,
city: this.city,
area:this.area,
area: this.area,
store_id: uni.getStorageSync('store_id') || '11001',
}
getaddressAdd({
method: "post",
data: list
}).then((res) => {
// console.log(res, 'add');
if (res.data.errcode == 0) {
uni.showToast({
title: '添加成功',
@@ -171,20 +431,12 @@ height:' 68rpx',borderRadio:'8rpx'}">
mask: false,
icon: "success"
})
setTimeout(() => {
uni.navigateBack({
delta: 1
});
}, 700)
} else if (res.data.errcode != 0) {
// uni.showToast({
// title: res.data.msg,
// icon: "error",
// duration: 1500,
// mask: false
// })
this.$refs.uToast.show({
title: res.data.msg,
type: 'default',
@@ -193,8 +445,6 @@ height:' 68rpx',borderRadio:'8rpx'}">
}
})
},
// 地址列表
getList() {
getaddressList({
method: "post",
@@ -202,19 +452,19 @@ height:' 68rpx',borderRadio:'8rpx'}">
store_id: uni.getStorageSync('store_id') || '11001',
}
}).then((res) => {
// console.log(res, 'list');
if (res.data.errcode == 0) {
this.infoList = (res.data.data).filter((item) => item.id == this.id)
this.from = {
...this.infoList
}
// console.log(this.from, 'edit');
if (this.from[0]) {
this.province = this.from[0].province || ''
this.city = this.from[0].city || ''
this.area = this.from[0].area || ''
}
}
})
},
// 修改地址
getEdit() {
const editMobile = this.from[0] && this.from[0].mobile
if (!assertPhone(editMobile, '请填写收货人手机号', '收货人手机号格式不正确')) {
@@ -228,14 +478,13 @@ height:' 68rpx',borderRadio:'8rpx'}">
detail_address: this.from[0].detail_address,
province: this.province,
city: this.city,
area:this.area,
area: this.area,
store_id: uni.getStorageSync('store_id') || '11001',
}
getaddressEdit({
method: "post",
data: list
}).then((res) => {
// console.log(res, 'add');
if (res.data.errcode == 0) {
uni.showToast({
title: '修改成功',
@@ -243,20 +492,12 @@ height:' 68rpx',borderRadio:'8rpx'}">
mask: false,
icon: "success"
})
setTimeout(() => {
uni.navigateBack({
delta: 1
});
}, 700)
} else if (res.data.errcode != 0) {
// uni.showToast({
// title: res.data.msg,
// icon: "error",
// duration: 1500,
// mask: false
// })
this.$refs.uToast.show({
title: res.data.msg,
type: 'default',
@@ -265,11 +506,7 @@ height:' 68rpx',borderRadio:'8rpx'}">
}
})
}
},
mounted() {
}
}
</script>
@@ -296,20 +533,7 @@ height:' 68rpx',borderRadio:'8rpx'}">
.btn {
margin: 80rpx auto;
.add_btn {
width: 686rpx;
height: 86rpx;
line-height: 86rpx;
background: #4175EE;
text-align: center;
border-radius: 44rpx 44rpx 44rpx 44rpx;
font-size: 32rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #FFFFFF;
margin: 0rpx auto;
}
.add_btn,
.eidt_btn {
width: 686rpx;
height: 86rpx;
@@ -325,4 +549,89 @@ height:' 68rpx',borderRadio:'8rpx'}">
}
}
}
</style>
.region-drawer {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
box-sizing: border-box;
}
.region-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.region-title {
font-size: 32rpx;
font-weight: 600;
color: #1d2129;
}
.region-close {
font-size: 44rpx;
color: #86909c;
line-height: 1;
padding: 0 8rpx;
}
.search-box {
padding: 16rpx 24rpx;
flex-shrink: 0;
}
.search-input {
height: 68rpx;
background: #f6f6f6;
border-radius: 8rpx;
padding: 0 24rpx;
font-size: 28rpx;
}
.crumb-scroll {
white-space: nowrap;
padding: 0 24rpx 16rpx;
flex-shrink: 0;
}
.crumbs {
display: inline-flex;
flex-wrap: nowrap;
}
.crumb {
font-size: 26rpx;
color: #4175EE;
}
.crumb.muted {
color: #86909c;
}
.crumb-sep {
color: #c0c4cc;
}
.list-scroll {
flex: 1;
height: 0;
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>