540 lines
14 KiB
Vue
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>
|