Files
yxe-mp/packageUser/address/index.vue
T
2026-09-29 15:17:13 +08:00

540 lines
14 KiB
Vue

<template>
<view class="address-page">
<view class="page-hero">
<AppNavbar title="收货地址管理" show-back/>
<view class="intro-card">
<view class="card-title">
<view class="title-bar"></view>
<text>收货地址管理</text>
</view>
<text class="intro-text">请填写准确的收货信息</text>
</view>
</view>
<view class="card block">
<view class="section-title-row">
<view class="section-title">地址列表</view>
<view class="primary-btn add-btn" @tap="openFormPopup">新增地址</view>
</view>
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="addresses.length === 0" class="empty">暂无地址</view>
<view v-else class="address-list">
<view class="addr-item" v-for="item in addresses" :key="item.id">
<view class="addr-head">
<view class="addr-name">{{ item.name }} {{ maskPhone(item.phone) }}</view>
<view v-if="Number(item.isDefault) === 1 || item.isDefault === true" class="chip">默认</view>
</view>
<view class="addr-detail">{{ item.fullAddress || buildFullAddress(item) }}</view>
</view>
</view>
</view>
<view v-if="showFormPopup" class="form-popup-mask" @tap="closeFormPopup"></view>
<view v-if="showFormPopup" class="form-popup card" @tap.stop>
<view class="popup-head">
<view class="section-title">新增/编辑地址</view>
<view class="close-btn" @tap="closeFormPopup">关闭</view>
</view>
<view class="form-row">
<view class="label">姓名</view>
<input v-model="form.name" class="input" placeholder="收货人姓名" />
</view>
<view class="form-row">
<view class="label">手机号</view>
<input v-model="form.phone" class="input" placeholder="11位手机号" type="number" maxlength="11" />
</view>
<view class="form-row">
<view class="label">省市区</view>
<view class="picker-value" :class="{ placeholder: !form.province }" @tap="openRegionPicker">
{{ regionText }}
</view>
</view>
<view v-if="showRegionPicker" class="region-picker">
<view class="region-picker-head">
<view class="region-picker-title">选择省市区</view>
<view class="region-picker-actions">
<view class="region-picker-btn" @tap="cancelRegionPicker">取消</view>
<view class="region-picker-btn primary" @tap="confirmRegionPicker">确定</view>
</view>
</view>
<view v-if="areaLoading" class="region-loading">加载中...</view>
<picker-view v-else class="picker-view" :value="areaIndex" @change="onAreaPickerChange">
<picker-view-column>
<view v-for="item in areaColumns.provinces" :key="item.id" class="picker-item">
{{ item.name }}
</view>
</picker-view-column>
<picker-view-column>
<view v-for="item in areaColumns.cities" :key="item.id" class="picker-item">
{{ item.name }}
</view>
</picker-view-column>
<picker-view-column>
<view v-for="item in areaColumns.regions" :key="item.id" class="picker-item">
{{ item.name }}
</view>
</picker-view-column>
</picker-view>
</view>
<view class="form-row">
<view class="label">详细地址</view>
<input v-model="form.address" class="input" placeholder="街道门牌信息" />
</view>
<view class="form-row switch-row">
<view class="label">默认地址</view>
<switch :checked="form.isDefault" color="#2563eb" @change="onDefaultChange" />
</view>
<view class="primary-btn submit" @tap="submitAddress">保存地址</view>
</view>
</view>
</template>
<script>
import {saveUserAddressReq, userAddressListReq} from "@/api/address";
import {areaListByFidReq} from "@/api/common";
import AppNavbar from "@/components/AppNavbar.vue";
export default {
components: {AppNavbar},
data() {
return {
form: {
province: '',
city: '',
region: '',
phone: '',
name: '',
address: '',
isDefault: false
},
addresses: [],
loading: false,
showFormPopup: false,
showRegionPicker: false,
areaLoading: false,
areaIndex: [0, 0, 0],
areaColumns: {
provinces: [],
cities: [],
regions: []
}
}
},
computed: {
regionValue() {
if (!this.form.province) return []
return [this.form.province, this.form.city, this.form.region]
},
regionText() {
if (!this.form.province) return '请选择省市区'
return `${this.form.province} ${this.form.city} ${this.form.region}`
}
},
onShow() {
this.loadAddressList()
},
methods: {
onRegionChange(e) {
const value = e.detail.value || []
this.form.province = value[0] || ''
this.form.city = value[1] || ''
this.form.region = value[2] || ''
},
onDefaultChange(e) {
this.form.isDefault = e.detail.value
},
normalizeAreaItem(item) {
return {
id: item.areaId || item.code || item.value || item.id || null,
name: item.name || item.title || item.areaName || item.label || ''
}
},
async fetchAreaListByFid(fid) {
const {data} = await areaListByFidReq({fid})
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
return list.map(this.normalizeAreaItem)
},
async initAreaColumns() {
this.areaLoading = true
try {
const provinces = await this.fetchAreaListByFid(0)
this.areaColumns.provinces = provinces
const provinceIndex = Math.max(0, provinces.findIndex(item => item.name === this.form.province))
const province = provinces[provinceIndex]
const cities = province ? await this.fetchAreaListByFid(province.id) : []
this.areaColumns.cities = cities
const cityIndex = Math.max(0, cities.findIndex(item => item.name === this.form.city))
const city = cities[cityIndex]
const regions = city ? await this.fetchAreaListByFid(city.id) : []
this.areaColumns.regions = regions
const regionIndex = Math.max(0, regions.findIndex(item => item.name === this.form.region))
this.areaIndex = [provinceIndex, cityIndex, regionIndex]
} catch (err) {
uni.showToast({
title: '地区加载失败',
icon: 'none'
})
} finally {
this.areaLoading = false
}
},
async updateCitiesByProvince(provinceIndex) {
const province = this.areaColumns.provinces[provinceIndex]
console.log(province)
this.areaColumns.cities = province ? await this.fetchAreaListByFid(province.id) : []
this.areaColumns.regions = []
this.areaIndex = [provinceIndex, 0, 0]
await this.updateRegionsByCity(provinceIndex, 0)
},
async updateRegionsByCity(provinceIndex, cityIndex) {
const city = this.areaColumns.cities[cityIndex]
const regions = city ? await this.fetchAreaListByFid(city.id) : []
this.areaColumns.regions = regions
this.areaIndex = [provinceIndex, cityIndex, 0]
},
async openRegionPicker() {
this.showRegionPicker = true
if (!this.areaColumns.provinces.length) {
await this.initAreaColumns()
}
},
cancelRegionPicker() {
this.showRegionPicker = false
},
confirmRegionPicker() {
const [pIndex, cIndex, rIndex] = this.areaIndex
const province = this.areaColumns.provinces[pIndex]
const city = this.areaColumns.cities[cIndex]
const region = this.areaColumns.regions[rIndex]
this.form.province = province ? province.name : ''
this.form.city = city ? city.name : ''
this.form.region = region ? region.name : ''
this.showRegionPicker = false
},
async onAreaPickerChange(e) {
const value = e.detail.value || [0, 0, 0]
console.log(value)
const [pIndex, cIndex] = value
const [prevP, prevC] = this.areaIndex
if (pIndex !== prevP) {
await this.updateCitiesByProvince(pIndex)
return
}
if (cIndex !== prevC) {
await this.updateRegionsByCity(pIndex, cIndex)
return
}
this.areaIndex = value
},
buildFullAddress(item) {
const province = item.province || ''
const city = item.city || ''
const region = item.region || ''
const address = item.address || ''
return `${province}${city}${region}${address}`
},
maskPhone(phone) {
const value = String(phone || '')
if (value.length < 7) return value
return `${value.slice(0, 3)}****${value.slice(-4)}`
},
async loadAddressList() {
this.loading = true
try {
const {data} = await userAddressListReq()
this.addresses = Array.isArray(data) ? data : (data && data.list ? data.list : [])
} catch (err) {
uni.showToast({
title: '地址加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
openFormPopup() {
this.showFormPopup = true
},
closeFormPopup() {
this.showFormPopup = false
},
async submitAddress() {
if (!this.form.name || !this.form.phone || !this.form.address || !this.form.province) {
uni.showToast({
title: '请完善收货信息',
icon: 'none'
})
return
}
const fullAddress = `${this.form.province}${this.form.city}${this.form.region}${this.form.address}`
const payload = {
province: this.form.province,
city: this.form.city,
region: this.form.region,
phone: this.form.phone,
name: this.form.name,
address: this.form.address,
fullAddress: fullAddress,
isDefault: this.form.isDefault ? 1 : 0
}
try {
await saveUserAddressReq(payload)
uni.showToast({
title: '地址已保存',
icon: 'success'
})
this.form.address = ''
this.form.name = ''
this.form.phone = ''
this.form.province = ''
this.form.city = ''
this.form.region = ''
this.form.isDefault = false
this.showFormPopup = false
await this.loadAddressList()
} catch (err) {
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
}
}
</script>
<style lang="scss" scoped>
.address-page {
min-height: 100vh;
//padding: 0 20rpx 40rpx;
background: #f5f5f5;
}
.page-hero {
//padding: 30rpx 6rpx 18rpx;
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat
top center / 100% auto;
}
.page-navbar {
padding: 0 6rpx;
}
.intro-card {
margin: 18rpx 20rpx 0;
padding: 20rpx 18rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
}
.card-title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.title-bar {
width: 6rpx;
height: 30rpx;
border-radius: 999rpx;
background: #2fc6a7;
}
.intro-text {
margin-top: 12rpx;
font-size: 26rpx;
color: #6b7280;
line-height: 1.6;
}
.block {
margin: 18rpx 20rpx 0;
}
.section-title-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.add-btn {
padding: 6rpx 16rpx;
height: 52rpx;
font-size: 24rpx;
}
.form-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx 0;
border-bottom: 1rpx solid #edf1f7;
}
.form-row:last-of-type {
border-bottom: none;
}
.label {
color: var(--text-main);
font-size: 26rpx;
min-width: 140rpx;
}
.input {
flex: 1;
text-align: right;
font-size: 26rpx;
color: var(--text-main);
}
.picker-value {
flex: 1;
text-align: right;
color: var(--text-main);
font-size: 26rpx;
}
.picker-value.placeholder {
color: var(--text-sub);
}
.switch-row {
align-items: center;
}
.submit {
margin-top: 18rpx;
padding: 14rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.address-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.addr-item {
border: 1rpx dashed #dfe8f5;
border-radius: 14rpx;
padding: 12rpx;
}
.addr-head {
display: flex;
justify-content: space-between;
align-items: center;
}
.addr-name {
color: var(--text-main);
font-weight: 700;
}
.addr-detail {
margin-top: 6rpx;
color: var(--text-sub);
font-size: 24rpx;
}
.empty {
color: var(--text-sub);
text-align: center;
padding: 16rpx 0;
}
.region-picker {
margin: 12rpx 0 8rpx;
border-radius: 12rpx;
border: 1rpx solid #e7eef8;
overflow: hidden;
}
.region-picker-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 12rpx;
background: #f6f8fc;
}
.region-picker-title {
font-size: 24rpx;
color: var(--text-main);
}
.region-picker-actions {
display: flex;
gap: 12rpx;
}
.region-picker-btn {
font-size: 24rpx;
color: var(--text-sub);
}
.region-picker-btn.primary {
color: var(--brand-primary);
}
.region-loading {
padding: 16rpx 0;
text-align: center;
color: var(--text-sub);
font-size: 24rpx;
}
.picker-view {
height: 360rpx;
}
.picker-item {
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 26rpx;
color: var(--text-main);
}
.form-popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 30;
}
.form-popup {
position: fixed;
left: 50%;
bottom: 40rpx;
transform: translateX(-50%);
width: 88%;
max-height: 70vh;
overflow: auto;
z-index: 40;
}
.popup-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.close-btn {
color: var(--text-sub);
font-size: 24rpx;
}
</style>