feat(wechat): 集成微信原生地址导入、头像选择与昵称填写能力

- 新增微信收货地址导入功能(chooseAddress),含地区匹配与错误处理
- 个人资料页接入微信头像选择(chooseAvatar)与昵称填写(type="nickname")
- 重构收货地址添加页与管理页的UI布局,抽取公共样式 address-page.scss
- 地址页与个人页增加加载失败提示、请求防抖与保存状态反馈
- 下拉刷新改用 Promise.allSettled 避免单接口失败阻断其他数据更新
- manifest 新增 chooseAddress 隐私权限声明,版本号更新至 5.03
This commit is contained in:
pikachu1995@126.com
2026-11-04 11:09:34 +08:00
parent 7b2b045fd4
commit d72f02f7cf
9 changed files with 507 additions and 247 deletions

View File

@@ -1,55 +1,64 @@
<template>
<view class="address">
<u-empty class="empty" v-if="addressList.length === 0" text="暂无收货地址" mode="address"></u-empty>
<view class="list" >
<view class="item c-content" v-for="(item, index) in addressList" :key="index">
<view class="basic">
<text>{{ item.name }}</text>
<text>{{ item.mobile }}</text>
<text class="default" v-show="item.isDefault">默认</text>
<view>
<div class="region">
<span v-if="item.consigneeAddressPath[0]">{{item.consigneeAddressPath[0]}}</span>
<span v-if="item.consigneeAddressPath[1]">{{item.consigneeAddressPath[1]}}</span>
<span v-if="item.consigneeAddressPath[2]">{{item.consigneeAddressPath[2]}}</span>
<span v-if="item.consigneeAddressPath[3]">{{item.consigneeAddressPath[3]}}</span>
<span>{{ item.detail }}</span>
</div>
</view>
<view class="address-page address-manage" :style="themeStyle">
<view v-if="!loading && addressList.length === 0" class="empty-state">
<view class="empty-icon"><u-icon name="map" size="42" color="#c4c8d1" /></view>
<text class="empty-title">还没有收货地址</text>
<text class="empty-description">添加常用地址,下次购物更方便</text>
</view>
<view v-if="addressList.length" class="list-heading">
<text>我的收货地址</text>
<text class="address-count">共 {{ addressList.length }} 个</text>
</view>
<view class="address-list">
<view class="address-card address-item" v-for="item in addressList" :key="item.id">
<view class="recipient-line">
<text class="recipient-name">{{ item.name }}</text>
<text class="recipient-mobile">{{ item.mobile }}</text>
</view>
<view class="edit">
<view class="relative" @click="setDefault(item)">
<view v-if="item.isDefault" class="alifont icon-xuanzhong"></view>
<text v-else class="unchecked"></text>
<text>{{ item.isDefault ? "默认地址" : "设为默认" }}</text>
<view class="address-region">{{ item.consigneeAddressPath.join(' ') }}</view>
<text class="address-detail">{{ item.detail }}</text>
<view class="address-tags" v-if="item.isDefault || item.alias">
<text v-if="item.isDefault" class="address-tag default-tag">默认地址</text>
<text v-if="item.alias" class="address-tag">{{ item.alias }}</text>
</view>
<view class="card-actions">
<view class="default-action" @click="setDefault(item)">
<view class="default-check" :class="{ 'is-checked': item.isDefault }">
<u-icon v-if="item.isDefault" name="checkmark" size="11" color="#fff" />
</view>
<text>{{ item.isDefault ? '已设为默认' : '设为默认' }}</text>
</view>
<view class="relative">
<view class="alifont icon-bianji-copy"></view>
<text class="mr-40" @click="addAddress(item.id)">编辑</text>
<view class="alifont icon-lajitong" @click.stop="removeAddress(item.id)"></view>
<text @click.stop="removeAddress(item.id)">删除</text>
<view class="edit-actions">
<view class="text-action" @click="addAddress(item.id)">
<u-icon name="edit-pen" size="16" color="#6c7280" /><text>编辑</text>
</view>
<view class="action-divider"></view>
<view class="text-action" @click="removeAddress(item.id)">
<u-icon name="trash" size="16" color="#9297a1" /><text>删除</text>
</view>
</view>
</view>
</view>
</view>
<view class="btn-bar">
<view class="btn" @click="addAddress('')">
<u-icon name="plus-circle" color="#ffffff" size="18"></u-icon>
<text class="btn-text">添加新收货人</text>
</view>
<view class="address-footer">
<button class="address-primary-button" @click="addAddress('')">
<u-icon name="plus" color="#fff" size="17" /><text>新增收货地址</text>
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { onLoad, onShow, onPullDownRefresh, onBackPress } from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { tipsToLogin } from '@/utils/filters.js'
import * as API_Address from '@/api/address.js'
import { getThemeStyle } from '@/utils/theme'
const store = useStore()
const themeStyle = computed(() => getThemeStyle(store.state.theme))
const loading = ref(true)
const addressList = ref<any[]>([])
const routerVal = ref<Record<string, string>>({})
@@ -65,7 +74,6 @@ onLoad((val) => {
})
onPullDownRefresh(() => {
addressList.value = []
getAddressList()
})
@@ -75,19 +83,23 @@ onShow(() => {
}
})
function hideLoadingIfNeeded() {
if (store.state.isShowToast) uni.hideLoading()
}
function getAddressList() {
uni.showLoading()
API_Address.getAddressList(params.pageNumber, params.pageSize).then((res) => {
res.data.result.records.forEach((item: any) => {
item.consigneeAddressPath = item.consigneeAddressPath.split(',')
})
addressList.value = res.data.result.records
hideLoadingIfNeeded()
})
async function getAddressList() {
loading.value = true
try {
const res = await API_Address.getAddressList(params.pageNumber, params.pageSize)
if (!res.data?.success || !Array.isArray(res.data.result?.records)) throw new Error('地址加载失败')
addressList.value = res.data.result.records.map((item: any) => ({
...item,
consigneeAddressPath: Array.isArray(item.consigneeAddressPath)
? item.consigneeAddressPath
: String(item.consigneeAddressPath || '').split(',').filter(Boolean),
}))
} catch {
uni.showToast({ title: '地址加载失败,请下拉重试', icon: 'none' })
} finally {
loading.value = false
uni.stopPullDownRefresh()
}
}
function removeAddress(id: string) {
@@ -97,7 +109,7 @@ function removeAddress(id: string) {
success: (res) => {
if (!res.confirm) return
API_Address.deleteAddress(id).then((result) => {
if (result.statusCode == 200) {
if (result.data?.success) {
uni.showToast({ icon: 'none', title: '删除成功' })
getAddressList()
} else {
@@ -115,17 +127,45 @@ function addAddress(id?: string) {
}
function setDefault(item: any) {
delete item.updateBy
delete item.updateTime
delete item.deleteFlag
if (!item.isDefault) item.isDefault = true
API_Address.editAddress(item).then(() => {
uni.showToast({ title: '设置默认地址成功', icon: 'none' })
getAddressList()
})
if (item.isDefault) return
const data = { ...item, isDefault: true, consigneeAddressPath: item.consigneeAddressPath.join(',') }
delete data.updateBy
delete data.updateTime
delete data.deleteFlag
API_Address.editAddress(data).then((res) => {
if (res.data?.success) {
uni.showToast({ title: '已设为默认地址', icon: 'none' })
getAddressList()
} else {
uni.showToast({ title: res.data?.message || '设置失败,请重试', icon: 'none' })
}
}).catch(() => uni.showToast({ title: '设置失败,请重试', icon: 'none' }))
}
</script>
<style lang="scss" scoped>
@import "./address.scss";
@import "./address-page.scss";
.list-heading { display: flex; justify-content: space-between; padding: 8rpx 8rpx 24rpx; font-size: 26rpx; color: #6c7280; }
.address-count { color: #a0a5af; font-size: 24rpx; }
.address-item { padding: 32rpx 32rpx 0; margin-bottom: 24rpx; }
.recipient-line { display: flex; align-items: baseline; flex-wrap: wrap; gap: 20rpx; margin-bottom: 18rpx; }
.recipient-name { font-size: 34rpx; font-weight: 600; color: #222732; overflow-wrap: anywhere; }
.recipient-mobile { color: #636a77; font-size: 28rpx; }
.address-region { color: #9399a4; font-size: 25rpx; line-height: 1.7; }
.address-detail { display: block; color: #343b48; font-size: 29rpx; line-height: 1.7; overflow-wrap: anywhere; }
.address-tags { display: flex; flex-wrap: wrap; gap: 12rpx; padding-top: 20rpx; }
.address-tag { max-width: 100%; overflow-wrap: anywhere; padding: 4rpx 14rpx; background: #f3f4f6; border-radius: 8rpx; color: #7f8591; font-size: 22rpx; }
.default-tag { color: var(--theme-primary); background: var(--theme-primary-10); }
.card-actions { margin-top: 26rpx; border-top: 1rpx solid #f0f1f4; min-height: 92rpx; display: flex; justify-content: space-between; align-items: center; }
.default-action, .text-action, .edit-actions { display: flex; align-items: center; }
.default-action { gap: 12rpx; min-height: 80rpx; color: #858b96; font-size: 24rpx; }
.default-check { width: 30rpx; height: 30rpx; box-sizing: border-box; border: 2rpx solid #dce0e6; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.is-checked { background: var(--theme-primary); border-color: var(--theme-primary); }
.text-action { gap: 8rpx; min-height: 80rpx; color: #6c7280; font-size: 24rpx; padding: 0 8rpx; }
.action-divider { width: 1rpx; height: 22rpx; background: #e9ebef; margin: 0 16rpx; }
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 160rpx 0 80rpx; }
.empty-icon { width: 148rpx; height: 148rpx; display: flex; align-items: center; justify-content: center; background: #eaecef; border-radius: 50%; margin-bottom: 36rpx; }
.empty-title { color: #454c59; font-size: 32rpx; font-weight: 500; }
.empty-description { color: #a0a5af; font-size: 26rpx; margin-top: 16rpx; }
</style>