mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-11-06 17:09:01 +08:00
feat(wechat): 集成微信原生地址导入、头像选择与昵称填写能力
- 新增微信收货地址导入功能(chooseAddress),含地区匹配与错误处理 - 个人资料页接入微信头像选择(chooseAvatar)与昵称填写(type="nickname") - 重构收货地址添加页与管理页的UI布局,抽取公共样式 address-page.scss - 地址页与个人页增加加载失败提示、请求防抖与保存状态反馈 - 下拉刷新改用 Promise.allSettled 避免单接口失败阻断其他数据更新 - manifest 新增 chooseAddress 隐私权限声明,版本号更新至 5.03
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
"name" : "lilishop",
|
||||
"appid" : "__UNI__ED807EA",
|
||||
"description" : "",
|
||||
"versionName" : "4.5.3",
|
||||
"versionCode" : 4000503,
|
||||
"versionName" : "5.03",
|
||||
"versionCode" : 503,
|
||||
"transformPx" : false,
|
||||
"app-plus" : {
|
||||
"compatible" : {
|
||||
@@ -212,7 +212,7 @@
|
||||
"plugins" : {},
|
||||
"libVersion" : "3.5.8",
|
||||
"__usePrivacyCheck__" : false,
|
||||
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ]
|
||||
"requiredPrivateInfos" : [ "chooseLocation", "getLocation", "chooseAddress" ]
|
||||
},
|
||||
"h5" : {
|
||||
"devServer" : {
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "lilishop-uniapp",
|
||||
"version": "4.5.3",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tencentcloud/chat": "^3.5.5",
|
||||
"hls.js": "^1.6.16",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"type": "git",
|
||||
"url": "https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp"
|
||||
},
|
||||
"license": "MIT",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tencentcloud/chat": "^3.5.5",
|
||||
"hls.js": "^1.6.16",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<view :style="themeStyle" class="add-address">
|
||||
<view :style="themeStyle" class="address-page add-address">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="wechat-import">
|
||||
<button class="wechat-import-button" :disabled="importing" @click="importWechatAddress">
|
||||
<view class="wechat-icon"><u-icon name="weixin-fill" color="#07b75b" size="26" /></view>
|
||||
<view class="wechat-copy">
|
||||
<text class="wechat-title">{{ importing ? '正在导入微信地址…' : '导入微信收货地址' }}</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#77a38a" size="14" />
|
||||
</button>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view v-if="importNotice" class="import-notice">{{ importNotice }}</view>
|
||||
<view class="section-heading">收货信息</view>
|
||||
<view class="address-card form-card">
|
||||
<up-form
|
||||
:model="form"
|
||||
ref="uForm"
|
||||
@@ -7,30 +21,31 @@
|
||||
:rules="rules"
|
||||
label-position="left"
|
||||
label-width="180rpx"
|
||||
:label-style="{ color: '#5b606b', fontSize: '28rpx' }"
|
||||
>
|
||||
<up-form-item label="收货人" label-width="180rpx" prop="name" :border-bottom="true">
|
||||
<up-form-item label="收货人" label-width="160rpx" prop="name" :border-bottom="true" :custom-style="fieldStyle">
|
||||
<u-input
|
||||
v-model="form.name"
|
||||
border="none"
|
||||
input-align="right"
|
||||
input-align="left"
|
||||
clearable
|
||||
placeholder="请输入收货人姓名"
|
||||
placeholder="姓名"
|
||||
/>
|
||||
</up-form-item>
|
||||
|
||||
<up-form-item label="手机号码" label-width="180rpx" prop="mobile" :border-bottom="true">
|
||||
<up-form-item label="手机号码" label-width="160rpx" prop="mobile" :border-bottom="true" :custom-style="fieldStyle">
|
||||
<u-input
|
||||
v-model="form.mobile"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
border="none"
|
||||
input-align="right"
|
||||
placeholder="请输入收货人手机号码"
|
||||
input-align="left"
|
||||
placeholder="收货人的手机号码"
|
||||
/>
|
||||
</up-form-item>
|
||||
|
||||
<up-form-item label="所在区域" label-width="180rpx" prop="___path" :border-bottom="true">
|
||||
<view class="form-value" @click="showPicker">
|
||||
<up-form-item label="所在地区" label-width="160rpx" prop="___path" :border-bottom="true" :custom-style="fieldStyle" @click="showPicker">
|
||||
<view class="form-value" :class="{ 'is-placeholder': !form.___path }">
|
||||
{{ form.___path || '请选择所在地区' }}
|
||||
</view>
|
||||
<template #right>
|
||||
@@ -38,40 +53,38 @@
|
||||
</template>
|
||||
</up-form-item>
|
||||
|
||||
<up-form-item class="detailAddress" label="详细地址" label-width="180rpx" prop="detail" :border-bottom="true">
|
||||
<u-input
|
||||
type="textarea"
|
||||
<up-form-item label="详细地址" label-width="160rpx" prop="detail" :border-bottom="true" :custom-style="{ ...fieldStyle, alignItems: 'flex-start' }">
|
||||
<textarea
|
||||
class="address-textarea"
|
||||
v-model="form.detail"
|
||||
maxlength="100"
|
||||
height="150"
|
||||
border="none"
|
||||
input-align="right"
|
||||
placeholder="街道楼牌号等"
|
||||
:auto-height="true"
|
||||
placeholder="街道、门牌号等详细信息"
|
||||
placeholder-style="color: #b1b5bd; font-size: 28rpx;"
|
||||
/>
|
||||
</up-form-item>
|
||||
|
||||
<up-form-item label="地址别名" label-width="180rpx" :border-bottom="false">
|
||||
<up-form-item label="地址标签" label-width="160rpx" :border-bottom="false" :custom-style="fieldStyle">
|
||||
<u-input
|
||||
v-model="form.alias"
|
||||
border="none"
|
||||
input-align="right"
|
||||
placeholder="请输入地址别名"
|
||||
input-align="left"
|
||||
placeholder="如:家、公司(选填)"
|
||||
/>
|
||||
</up-form-item>
|
||||
|
||||
<view class="default-row">
|
||||
<u-checkbox
|
||||
usedAlone
|
||||
shape="circle"
|
||||
size="30"
|
||||
:active-color="lightColor"
|
||||
v-model:checked="form.isDefault"
|
||||
label="设为默认地址"
|
||||
></u-checkbox>
|
||||
</view>
|
||||
|
||||
<view class="saveBtn" @click="save">保存</view>
|
||||
</up-form>
|
||||
</view>
|
||||
<view class="address-card default-row">
|
||||
<view>
|
||||
<text class="default-title">设为默认地址</text>
|
||||
<text class="default-hint">下单时优先使用这个地址</text>
|
||||
</view>
|
||||
<switch :checked="!!form.isDefault" :color="mainColor" @change="form.isDefault = $event.detail.value" />
|
||||
</view>
|
||||
<view class="address-footer">
|
||||
<button class="address-primary-button" :disabled="importing" @click="save">保存收货地址</button>
|
||||
</view>
|
||||
|
||||
<m-city
|
||||
:provinceData="list"
|
||||
@@ -93,16 +106,114 @@ import MCity from '@/components/m-city/m-city.vue'
|
||||
import uniMap from '@/components/uniMap'
|
||||
import permision from '@/js_sdk/wa-permission/permission.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getRegionsById } from '@/api/common.js'
|
||||
import { resolveWechatRegion, getWechatAddressErrorType } from '@/utils/wechatAddress.js'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const { proxy } = getCurrentInstance()!
|
||||
|
||||
const lightColor = computed(() => store.getters.lightColor)
|
||||
const mainColor = computed(() => store.getters.mainColor)
|
||||
const fieldStyle = { padding: '28rpx 0', minHeight: '100rpx', boxSizing: 'border-box', alignItems: 'center' }
|
||||
const mapFlag = ref(false)
|
||||
const routerVal = ref<Record<string, string>>({})
|
||||
const uForm = ref<any>(null)
|
||||
const cityPicker = ref<any>(null)
|
||||
const importing = ref(false)
|
||||
const importNotice = ref('')
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
function importWechatAddress() {
|
||||
if (importing.value) return
|
||||
importing.value = true
|
||||
uni.chooseAddress({
|
||||
success: async (address: any) => {
|
||||
uni.showLoading({ title: '正在匹配地区', mask: true })
|
||||
// Clear the old region and coordinates, including when editing an existing address.
|
||||
Object.assign(form, {
|
||||
name: address.userName || '',
|
||||
mobile: address.telNumber || '',
|
||||
detail: address.detailInfoNew || address.detailInfo || '',
|
||||
consigneeAddressIdPath: [],
|
||||
consigneeAddressPath: [],
|
||||
___path: '',
|
||||
lat: '',
|
||||
lon: '',
|
||||
})
|
||||
importNotice.value = ''
|
||||
try {
|
||||
const regions = await resolveWechatRegion(address, getRegionsById)
|
||||
if (regions) {
|
||||
if (regions.length < 4 && address.streetName) {
|
||||
form.detail = address.detailInfo || [address.streetName, address.detailInfoNew].filter(Boolean).join('')
|
||||
}
|
||||
const names = regions.map((item: any) => item.name || item.localName)
|
||||
form.consigneeAddressIdPath = regions.map((item: any) => item.id)
|
||||
form.consigneeAddressPath = names
|
||||
form.___path = names.join('')
|
||||
const center = String(regions[regions.length - 1].center || '').split(',')
|
||||
if (center.length === 2) [form.lon, form.lat] = center
|
||||
importNotice.value = '已导入微信地址,请核对后保存'
|
||||
} else {
|
||||
showImportedRegionHint(address)
|
||||
}
|
||||
} catch {
|
||||
showImportedRegionHint(address)
|
||||
} finally {
|
||||
importing.value = false
|
||||
uni.hideLoading()
|
||||
uForm.value?.clearValidate()
|
||||
}
|
||||
},
|
||||
fail: (error: any) => {
|
||||
importing.value = false
|
||||
const type = getWechatAddressErrorType(error)
|
||||
if (type === 'cancel') return
|
||||
let platform = ''
|
||||
try { platform = (globalThis as any).wx?.getDeviceInfo?.()?.platform || '' } catch {}
|
||||
// Log diagnostics only, never the selected address or other personal information.
|
||||
console.warn('[微信地址导入失败]', {
|
||||
errMsg: String(error?.errMsg || error?.message || ''),
|
||||
errCode: error?.errCode ?? error?.errno ?? '',
|
||||
type,
|
||||
platform,
|
||||
})
|
||||
if (type === 'auth') {
|
||||
uni.showModal({
|
||||
title: '开启收货地址授权',
|
||||
content: '需要允许使用微信收货地址才能导入,也可以继续手动填写。',
|
||||
confirmText: '去设置',
|
||||
success: (result) => { if (result.confirm) uni.openSetting({}) },
|
||||
})
|
||||
return
|
||||
}
|
||||
if (type === 'privacy') {
|
||||
console.warn('[微信地址导入] 请核对小程序后台的用户隐私保护指引是否声明收货地址,以及用户隐私授权状态。')
|
||||
}
|
||||
if (type === 'configuration') {
|
||||
console.warn('[微信地址导入] manifest.json 的 mp-weixin.requiredPrivateInfos 必须包含 chooseAddress,修改后需重新编译小程序。')
|
||||
}
|
||||
const content = type === 'configuration'
|
||||
? '收货地址导入暂未配置完成,请稍后再试,也可以手动填写。'
|
||||
: type === 'privacy'
|
||||
? '暂时无法获取微信收货地址,请稍后重试,也可以手动填写。'
|
||||
: platform === 'devtools'
|
||||
? '当前开发工具中未能打开微信地址簿,请使用手机微信扫码预览测试,也可以手动填写。'
|
||||
: type === 'unsupported'
|
||||
? '当前微信环境不支持导入收货地址,请更新微信后重试,也可以手动填写。'
|
||||
: '暂时无法打开微信地址簿,请稍后重试,也可以手动填写。'
|
||||
uni.showModal({ title: '微信地址未导入', content, showCancel: false })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function showImportedRegionHint(address: any) {
|
||||
const region = [address.provinceName, address.cityName, address.countyName, address.streetName].filter(Boolean).join('')
|
||||
// Preserve the street if the user has to select it manually and the new detail field omits it.
|
||||
form.detail = address.detailInfo || [address.streetName, address.detailInfoNew].filter(Boolean).join('')
|
||||
importNotice.value = `已导入姓名、电话和详细地址,请补选所在地区${region ? '(微信地址:' + region + ')' : ''}`
|
||||
}
|
||||
// #endif
|
||||
|
||||
const form = reactive<Record<string, any>>({
|
||||
detail: '',
|
||||
@@ -160,18 +271,24 @@ const rules = {
|
||||
|
||||
onShow(() => {})
|
||||
|
||||
onLoad((option) => {
|
||||
uni.showLoading({ title: '加载中' })
|
||||
onLoad(async (option) => {
|
||||
routerVal.value = option || {}
|
||||
if (option.id) {
|
||||
getAddressDetail(option.id).then((res) => {
|
||||
const params = res.data.result
|
||||
params.___path = params.consigneeAddressPath
|
||||
Object.assign(form, params)
|
||||
if (store.state.isShowToast) uni.hideLoading()
|
||||
})
|
||||
uni.setNavigationBarTitle({ title: option?.id ? '编辑收货地址' : '新增收货地址' })
|
||||
if (!option?.id) return
|
||||
uni.showLoading({ title: '加载中' })
|
||||
try {
|
||||
const res = await getAddressDetail(option.id)
|
||||
const params = res.data?.result
|
||||
if (!res.data?.success || !params) {
|
||||
uni.showToast({ title: '地址加载失败,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
Object.assign(form, params, { ___path: params.consigneeAddressPath || '' })
|
||||
} catch {
|
||||
uni.showToast({ title: '地址加载失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
@@ -233,6 +350,7 @@ function callBackAddress(val: any) {
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (importing.value) return
|
||||
uForm.value?.validate().then(() => {
|
||||
const params = { ...form }
|
||||
delete params.___path
|
||||
@@ -278,11 +396,12 @@ function getpickerParentValue(e: any[]) {
|
||||
form.___path = name
|
||||
}
|
||||
if (index == e.length - 1) {
|
||||
const _town = item.children.filter((_child: any) => _child.id == item.id)
|
||||
form.lat = _town[0].center.split(',')[1]
|
||||
form.lon = _town[0].center.split(',')[0]
|
||||
const center = String(item.center || item.children?.find((_child: any) => _child.id == item.id)?.center || '').split(',')
|
||||
form.lat = center.length === 2 ? center[1] : ''
|
||||
form.lon = center.length === 2 ? center[0] : ''
|
||||
}
|
||||
})
|
||||
importNotice.value = ''
|
||||
}
|
||||
|
||||
function showPicker() {
|
||||
@@ -290,99 +409,25 @@ function showPicker() {
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
page {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.add-address {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
:deep(.u-form-item) {
|
||||
padding: 0 30rpx;
|
||||
min-height: 100rpx;
|
||||
}
|
||||
|
||||
:deep(.u-form-item__body) {
|
||||
min-height: 100rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.u-form-item__body__left__content__label) {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
:deep(.u-form-item__body__right__content__slot) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.u-input) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.u-input__content__field-wrapper__field) {
|
||||
font-size: 28rpx !important;
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.form-value {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detailAddress {
|
||||
:deep(.u-form-item__body) {
|
||||
align-items: flex-start;
|
||||
padding-top: 24rpx;
|
||||
padding-bottom: 24rpx;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
:deep(.u-form-item__body__left) {
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
:deep(.u-form-item__body__right__content__slot) {
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
:deep(.u-textarea__field),
|
||||
:deep(.u-input__content__field-wrapper__field) {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
:deep(.uni-textarea-placeholder),
|
||||
:deep(.input-placeholder) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.default-row {
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
:deep(.default-row .u-checkbox__label) {
|
||||
font-size: 28rpx;
|
||||
color: $font-color-light;
|
||||
}
|
||||
|
||||
.saveBtn {
|
||||
margin: 40rpx 30rpx 0;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
background: $light-color;
|
||||
color: #fff;
|
||||
border-radius: 40rpx;
|
||||
@import "./address-page.scss";
|
||||
.wechat-import { margin-bottom: 28rpx; }
|
||||
.wechat-import-button {
|
||||
display: flex; align-items: center; padding: 28rpx; margin: 0; width: 100%; text-align: left;
|
||||
background: #eef9f2; border-radius: 24rpx; line-height: 1.5;
|
||||
&::after { border: 1rpx solid #dcefe3; border-radius: 24rpx; }
|
||||
&[disabled] { background: #eef9f2; opacity: .65; }
|
||||
}
|
||||
.wechat-icon { width: 76rpx; height: 76rpx; border-radius: 20rpx; background: #fff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: 22rpx; }
|
||||
.wechat-copy { flex: 1; min-width: 0; }
|
||||
.wechat-title { display: block; color: #253e30; font-size: 30rpx; font-weight: 600; }
|
||||
.import-notice { margin-bottom: 24rpx; padding: 20rpx 24rpx; border-radius: 16rpx; background: #fff5e5; color: #a47a3b; font-size: 24rpx; line-height: 1.6; }
|
||||
.section-heading { padding: 0 8rpx 20rpx; font-size: 26rpx; color: #858b96; }
|
||||
.form-card { padding: 0 28rpx; }
|
||||
.form-value { width: 100%; line-height: 1.6; font-size: 28rpx; color: #303744; overflow-wrap: anywhere; }
|
||||
.is-placeholder { color: #b1b5bd; }
|
||||
.address-textarea { width: 100%; min-height: 88rpx; font-size: 28rpx; color: #303744; line-height: 1.6; padding: 0; }
|
||||
.default-row { display: flex; justify-content: space-between; align-items: center; margin-top: 24rpx; padding: 28rpx; }
|
||||
.default-title { display: block; font-size: 28rpx; color: #353b47; }
|
||||
.default-hint { display: block; font-size: 23rpx; color: #a0a5af; margin-top: 8rpx; }
|
||||
.default-row switch { transform: scale(.8); transform-origin: right center; }
|
||||
</style>
|
||||
43
pages/mine/address/address-page.scss
Normal file
43
pages/mine/address/address-page.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
.address-page {
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
background: #f6f7f9;
|
||||
padding: 28rpx 24rpx calc(180rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.address-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.address-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 90;
|
||||
box-sizing: border-box;
|
||||
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #f0f1f4;
|
||||
}
|
||||
|
||||
.address-primary-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14rpx;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin: 0;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 44rpx;
|
||||
background: var(--theme-primary);
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
&::after { border: none; }
|
||||
&[disabled] { opacity: .5; background: var(--theme-primary); color: #fff; }
|
||||
}
|
||||
@@ -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 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="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-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="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="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="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 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()
|
||||
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 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()
|
||||
})
|
||||
}
|
||||
|
||||
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' })
|
||||
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>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<template>
|
||||
<view :style="themeStyle" class="person-msg">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button class="head avatar-button" open-type="chooseAvatar" :disabled="avatarUploading || saving" @chooseavatar="onChooseAvatar">
|
||||
<image :src="form.face || '/static/missing-face.png'" mode="aspectFill"></image>
|
||||
<view>{{ avatarUploading ? '头像上传中…' : '点击选择微信头像' }}</view>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="head" @click="changeFace">
|
||||
<image :src="form.face || '/static/missing-face.png'" mode="aspectFill"></image>
|
||||
<view>点击修改头像</view>
|
||||
<view>{{ avatarUploading ? '头像上传中…' : '点击修改头像' }}</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<up-form
|
||||
:model="form"
|
||||
@@ -13,12 +21,26 @@
|
||||
label-width="180rpx"
|
||||
>
|
||||
<up-form-item label="昵称" label-width="180rpx" :border-bottom="true">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<input
|
||||
class="nickname-input"
|
||||
type="nickname"
|
||||
:value="form.nickName"
|
||||
:disabled="saving"
|
||||
placeholder="点击填写微信昵称"
|
||||
@input="onNicknameChange"
|
||||
@blur="onNicknameChange"
|
||||
@nicknamereview="onNicknameReview"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<u-input
|
||||
v-model="form.nickName"
|
||||
border="none"
|
||||
input-align="right"
|
||||
placeholder="请输入昵称"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</up-form-item>
|
||||
|
||||
<up-form-item label="性别" label-width="180rpx" :border-bottom="true">
|
||||
@@ -55,7 +77,9 @@
|
||||
</up-form>
|
||||
|
||||
<view class="bottom">
|
||||
<view class="submit" @click="submit">保存</view>
|
||||
<button class="submit" :disabled="avatarUploading || saving" :loading="saving" @click="submit">
|
||||
{{ avatarUploading ? '头像上传中…' : saving ? '正在保存…' : '保存' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<u-datetime-picker
|
||||
@@ -80,7 +104,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ref, reactive, computed, nextTick } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { saveUserInfo } from '@/api/members.js'
|
||||
@@ -126,6 +150,24 @@ const maxBirthdayDate = Date.now()
|
||||
const region = ref([{ id: '', localName: '请选择', children: [] }])
|
||||
const showBirthday = ref(false)
|
||||
const cityPicker = ref<any>(null)
|
||||
const avatarUploading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
function onNicknameChange(e: any) {
|
||||
if (typeof e?.detail?.value === 'string') form.nickName = e.detail.value
|
||||
}
|
||||
|
||||
function onNicknameReview(e: any) {
|
||||
if (e?.detail?.pass === false) {
|
||||
form.nickName = ''
|
||||
uni.showToast({ title: '昵称未通过审核,请重新填写', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function onChooseAvatar(e: any) {
|
||||
const filePath = e?.detail?.avatarUrl
|
||||
if (typeof filePath === 'string' && filePath) uploadAvatar(filePath)
|
||||
}
|
||||
|
||||
function getPickerParentValue(selected: any[]) {
|
||||
form.region = []
|
||||
@@ -145,31 +187,66 @@ function clickRegion() {
|
||||
cityPicker.value?.show()
|
||||
}
|
||||
|
||||
function submit() {
|
||||
async function submit() {
|
||||
if (avatarUploading.value || saving.value) return
|
||||
// Nickname autofill can update the value in the blur event before the save tap.
|
||||
await nextTick()
|
||||
if (avatarUploading.value || saving.value) return
|
||||
const params = JSON.parse(JSON.stringify(form))
|
||||
params.nickName = String(params.nickName || '').trim()
|
||||
if (!params.nickName) {
|
||||
uni.showToast({ title: '请填写昵称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
delete params.regionPath
|
||||
saveUserInfo(params).then((res) => {
|
||||
if (res.statusCode === 200) {
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await saveUserInfo(params)
|
||||
if (res.data?.success && res.data.result) {
|
||||
storage.setUserInfo(res.data.result)
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
uni.showToast({ title: res.data?.message || '保存失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch {
|
||||
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function changeFace() {
|
||||
if (avatarUploading.value || saving.value) return
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (chooseImageRes) => {
|
||||
uni.uploadFile({
|
||||
url: upload,
|
||||
filePath: chooseImageRes.tempFilePaths[0],
|
||||
name: 'file',
|
||||
header: { accessToken: storage.getAccessToken() },
|
||||
success: (uploadFileRes) => {
|
||||
const data = JSON.parse(uploadFileRes.data)
|
||||
form.face = data.result
|
||||
const filePath = chooseImageRes.tempFilePaths?.[0]
|
||||
if (filePath) uploadAvatar(filePath)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function uploadAvatar(filePath: string) {
|
||||
if (avatarUploading.value || saving.value) return
|
||||
avatarUploading.value = true
|
||||
uni.uploadFile({
|
||||
url: upload,
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: { accessToken: storage.getAccessToken() },
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (res.statusCode !== 200 || !data.success || typeof data.result !== 'string' || !data.result) {
|
||||
throw new Error('上传失败')
|
||||
}
|
||||
form.face = data.result
|
||||
} catch {
|
||||
uni.showToast({ title: '头像上传失败,请重新选择', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => uni.showToast({ title: '头像上传失败,请重新选择', icon: 'none' }),
|
||||
complete: () => { avatarUploading.value = false },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,22 +269,11 @@ function navigateToBindMobile(username: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function syncUserInfo() {
|
||||
onShow(() => {
|
||||
const latest = storage.getUserInfo() || {}
|
||||
form.nickName = latest.nickName || ''
|
||||
form.birthday = latest.birthday || ''
|
||||
form.face = latest.face || '/static/missing-face.png'
|
||||
form.region = latest.region || []
|
||||
form.sex = latest.sex != null ? String(latest.sex) : '1'
|
||||
form.regionPath = latest.region
|
||||
// Refresh a newly bound phone without overwriting unsaved avatar/nickname edits.
|
||||
form.mobile = latest.mobile
|
||||
form.username = latest.username
|
||||
birthday.value = latest.birthday || ''
|
||||
birthdayValue.value = parseBirthdayTimestamp(latest.birthday)
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
syncUserInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -248,6 +314,23 @@ page {
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-button {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
line-height: normal;
|
||||
&::after { border: none; }
|
||||
&[disabled] { background: #fff; color: #999; }
|
||||
}
|
||||
|
||||
.nickname-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 64rpx;
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form {
|
||||
background: #fff;
|
||||
}
|
||||
@@ -341,5 +424,9 @@ page {
|
||||
font-size: 30rpx;
|
||||
background: $light-color;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
&::after { border: none; }
|
||||
&[disabled] { opacity: .5; background: $light-color; color: #fff; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -182,8 +182,15 @@ onShow(() => {
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
userInfo.value = isLogin() || {}
|
||||
if (isLogin('auth')) {
|
||||
getUserOrderNum()
|
||||
userInfo.value = isLogin()
|
||||
} else {
|
||||
walletNum.value = 0
|
||||
couponNum.value = 0
|
||||
footNum.value = 0
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
// #ifndef MP
|
||||
@@ -205,17 +212,24 @@ function userDetail() {
|
||||
}
|
||||
|
||||
async function getUserOrderNum() {
|
||||
uni.stopPullDownRefresh()
|
||||
|
||||
Promise.all([
|
||||
try {
|
||||
// Each request can fail independently; a missing wallet must not break the other counts.
|
||||
const results = await Promise.allSettled([
|
||||
getCouponsNum(),
|
||||
getFootprintNum(),
|
||||
getUserWallet(),
|
||||
]).then((res: any[]) => {
|
||||
couponNum.value = res[0].data.result
|
||||
footNum.value = res[1].data.result
|
||||
walletNum.value = res[2].data.result.memberWallet
|
||||
])
|
||||
// The user may have logged out while these requests were running.
|
||||
if (!isLogin('auth')) return
|
||||
const targets = [couponNum, footNum, walletNum]
|
||||
results.forEach((result, index) => {
|
||||
if (result.status !== 'fulfilled' || !result.value?.data?.success) return
|
||||
const data = result.value.data.result
|
||||
targets[index].value = index === 2 ? (data?.memberWallet ?? 0) : (data ?? 0)
|
||||
})
|
||||
} finally {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
31
utils/wechatAddress.js
Normal file
31
utils/wechatAddress.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export function getWechatAddressErrorType(error) {
|
||||
const message = String(error?.errMsg || error?.message || '')
|
||||
if (/cancel/i.test(message)) return 'cancel'
|
||||
if (/requiredPrivateInfos/i.test(message)) return 'configuration'
|
||||
if (/privacy|隐私/i.test(message)) return 'privacy'
|
||||
if (/auth deny|auth denied|authorize no response|auth reject/i.test(message)) return 'auth'
|
||||
if (/not support|not supported|not available|not a function/i.test(message)) return 'unsupported'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
// Match each name under its parent; never guess an ID from a similarly named region.
|
||||
export async function resolveWechatRegion(address, fetchRegions) {
|
||||
const names = [address.provinceName, address.cityName, address.countyName, address.streetName]
|
||||
const selected = []
|
||||
let parentId = 0
|
||||
for (let level = 0; level < 4; level++) {
|
||||
const response = await fetchRegions(parentId)
|
||||
if (!response?.data?.success || !Array.isArray(response.data.result)) {
|
||||
throw new Error('地区加载失败')
|
||||
}
|
||||
const children = response.data.result
|
||||
if (!children.length) return selected.length >= 3 ? selected : null
|
||||
const name = String(names[level] || '').trim()
|
||||
if (!name) return null
|
||||
const matches = children.filter(item => String(item.name || item.localName || '').trim() === name)
|
||||
if (matches.length !== 1 || !matches[0].id) return null
|
||||
selected.push(matches[0])
|
||||
parentId = matches[0].id
|
||||
}
|
||||
return selected
|
||||
}
|
||||
Reference in New Issue
Block a user