fix(user): 修复地址和购物车相关的多个问题

- 解决鸿蒙手机首次打开地图选择不显示 POI 列表,先获取当前位置传入地图
- 修正收货地址编辑接口路径,改为无 id 的 PUT /shop/shop-user-address
- 防御性合并旧地址数据,避免 null 字段覆盖初始值
- 新用户注册和登录后同步更新 UserContext 状态,确保页面刷新购物车数据
- 各页面 useDidShow 内增加从 storage 同步用户状态,解决状态不同步问题
- 优化地图选择初始化逻辑,提升用户体验
- 保证购物车、商品详情和首页登录状态及时同步更新
This commit is contained in:
2026-07-17 01:07:54 +08:00
parent 22fed3b163
commit 1077b16f58
9 changed files with 99 additions and 18 deletions

View File

@@ -29,3 +29,26 @@
### 验证
- `npx taro build --type weapp` 构建成功
## 地址/购物车问题修复
### 5. 地图选择首次打开列表不显示src/pages/user/address-edit.tsx
- **问题**:鸿蒙手机首次打开 `Taro.chooseLocation` 时地图 POI 列表不显示,需拖动才出现
- **根因**:未传入经纬度时地图默认定位到北京,鸿蒙系统不会自动获取用户位置
- **修复**:调 `chooseLocation` 前先 `Taro.getLocation({ type: 'gcj02' })` 获取当前位置,将 latitude/longitude 传给地图确保首次打开就在用户位置附近POI 列表立即加载
### 6. 收货地址编辑不加载旧数据
- **Bug 1API 路径不匹配)**:前端 `updateShopUserAddress``PUT /shop/shop-user-address/{id}`,但后端 `@PutMapping()``/{id}` 路径 → 404
- 修复:改为 `PUT /shop/shop-user-address`(去掉 `/{id}`id 通过 body 传递)
- **Bug 2加载防御**`setFormData(addr)` 直接替换 state若后端返回 null 字段会覆盖初始 `''`
- 修复:改为 `setFormData(prev => ({...prev, ...addr, name: addr.name || '', ...}))` 保证字段非 null
### 7. 新用户注册后首次加购购物车不刷新
- **根因**:注册页 `register.tsx` 和登录页 `login.tsx` 只调 `saveStorageByLoginUser()` 存 storage未调 `UserContext.loginUser()` 更新 React 状态 → `isLoggedIn` 仍为 `false` → 购物车页 `useDidShow``if (isLoggedIn) refresh()` 不执行
- **修复**
1. `register.tsx``login.tsx``saveStorageByLoginUser` 后加 `loginUser(token, user)` 同步 UserContext
2. `sms-login.tsx``loginBySms` 后加 `syncFromStorage()` 同步状态
3. 防御性措施:`cart.tsx``product-detail.tsx``index/index.tsx``useDidShow``syncFromStorage()` 确保页面显示时从 storage 同步用户状态
### 验证
- `npx taro build --type weapp` 构建成功

View File

@@ -46,10 +46,11 @@ export async function addShopUserAddress(data: ShopUserAddress) {
/**
* 修改收货地址
* 后端 @PutMapping() 无 /{id} 路径id 通过 body 传递
*/
export async function updateShopUserAddress(data: ShopUserAddress) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-user-address/' + data.id,
'/shop/shop-user-address',
data
);
if (res.code === 0) {

View File

@@ -25,7 +25,7 @@ definePageConfig({
})
const IndexPage: React.FC = () => {
const { user, isLoggedIn } = useUser()
const { user, isLoggedIn, syncFromStorage } = useUser()
const [banners, setBanners] = useState<CmsAd[]>([])
const [bannerWrapWidth, setBannerWrapWidth] = useState(0)
const [announcements, setAnnouncements] = useState<CmsArticle[]>([])
@@ -68,6 +68,8 @@ const IndexPage: React.FC = () => {
// 页面重新显示时也刷新店员身份(可能从其他页面回来时状态变了)
useDidShow(() => {
// 先从 storage 同步用户状态(解决注册/登录后 UserContext 状态未更新的问题)
syncFromStorage()
if (isLoggedIn) {
getMyClerk()
.then(data => {

View File

@@ -18,7 +18,7 @@ definePageConfig({
const CartPage: React.FC = () => {
const { items, selectedCount, selectedPrice, updateQuantity, toggleSelect, selectAll, removeItem, refresh, loading, removeSelected, addItem } = useCartContext()
const { isLoggedIn } = useUserContext()
const { isLoggedIn, syncFromStorage } = useUserContext()
// VIP 状态异步校验并更新缓存isVip 变化时触发重渲染
const { isVip } = useVipStatus()
const [recommendGoods, setRecommendGoods] = useState<ShopGoods[]>([])
@@ -34,7 +34,9 @@ const CartPage: React.FC = () => {
// 页面再次显示时刷新购物车数据(从其他页面加购后回到购物车)
useDidShow(() => {
if (isLoggedIn) {
// 先从 storage 同步用户状态(解决注册/登录后 UserContext 状态未更新的问题)
const loggedIn = syncFromStorage()
if (loggedIn) {
refresh()
}
})

View File

@@ -37,7 +37,7 @@ const ProductDetailPage: React.FC = () => {
const [skuMode, setSkuMode] = useState<'cart' | 'buy'>('cart')
const [isFavorite, setIsFavorite] = useState(false)
const { addItem, totalCount, refresh } = useCartContext()
const { isLoggedIn } = useUserContext()
const { isLoggedIn, syncFromStorage } = useUserContext()
// VIP 状态异步校验并更新缓存isVip 变化时触发重渲染
const { isVip } = useVipStatus()
// 底部操作栏高度约 90px含按钮 + 安全区),动态计算 ScrollView 可用高度
@@ -64,6 +64,8 @@ const ProductDetailPage: React.FC = () => {
// 页面显示时刷新购物车数量,确保底部购物车角标准确
useDidShow(() => {
// 先从 storage 同步用户状态(解决注册/登录后 UserContext 状态未更新的问题)
syncFromStorage()
if (isLoggedIn) refresh()
})

View File

@@ -313,11 +313,35 @@ const AddressEditPage: React.FC = () => {
return
}
// 3. 仅在有有效经纬度时传入初始化参数,避免 undefined 触发部分机型失败
const initLat = selectedLocation?.lat ? Number(selectedLocation.lat) : undefined
const initLng = selectedLocation?.lng ? Number(selectedLocation.lng) : undefined
const latitude = typeof initLat === 'number' && Number.isFinite(initLat) ? initLat : undefined
const longitude = typeof initLng === 'number' && Number.isFinite(initLng) ? initLng : undefined
// 3. 优先使用已有经纬度;没有则先调 getLocation 获取当前位置
// 解决鸿蒙等机型首次打开地图时 POI 列表不显示的问题
let latitude: number | undefined
let longitude: number | undefined
if (selectedLocation?.lat && selectedLocation?.lng) {
const initLat = Number(selectedLocation.lat)
const initLng = Number(selectedLocation.lng)
if (Number.isFinite(initLat) && Number.isFinite(initLng)) {
latitude = initLat
longitude = initLng
}
}
// 没有已有定位时,主动获取当前位置传给地图,确保地图打开就在用户位置附近
// 这样地图的 POI 列表能立即加载,不需要用户手动拖动
if (latitude === undefined || longitude === undefined) {
try {
const loc: any = await Taro.getLocation({ type: 'gcj02' })
if (loc && Number.isFinite(loc.latitude) && Number.isFinite(loc.longitude)) {
latitude = loc.latitude
longitude = loc.longitude
}
} catch (locErr) {
// getLocation 失败不阻断流程chooseLocation 仍可打开(只是默认定位到北京)
console.warn('getLocation 失败,地图将以默认位置打开:', locErr)
}
}
const params: any = {}
if (typeof latitude === 'number') params.latitude = latitude
if (typeof longitude === 'number') params.longitude = longitude
@@ -470,14 +494,28 @@ const AddressEditPage: React.FC = () => {
if (isEditMode && addressId) {
try {
const addr = (await getShopUserAddress(addressId)) as ShopUserAddress
setFormData(addr)
const p = String((addr as any)?.province || '').trim()
const c = String((addr as any)?.city || '').trim()
const r = String((addr as any)?.region || '').trim()
setRegionText([p, c, r].filter(Boolean).join(' '))
if (hasValidLngLat(addr)) {
setSelectedLocation({lng: String((addr as any).lng), lat: String((addr as any).lat)})
setRegionLocked(true)
if (addr) {
// 合并到 formData保证初始字段不被 null 覆盖为 undefined
setFormData(prev => ({
...prev,
...addr,
name: addr.name || prev.name || '',
phone: addr.phone || prev.phone || '',
address: addr.address || prev.address || '',
province: addr.province || prev.province || '',
city: addr.city || prev.city || '',
region: addr.region || prev.region || '',
country: addr.country || prev.country || '中国',
isDefault: addr.isDefault ?? false,
}))
const p = String(addr?.province || '').trim()
const c = String(addr?.city || '').trim()
const r = String(addr?.region || '').trim()
setRegionText([p, c, r].filter(Boolean).join(' '))
if (hasValidLngLat(addr)) {
setSelectedLocation({lng: String((addr as any).lng), lat: String((addr as any).lat)})
setRegionLocked(true)
}
}
} catch (error) {
console.error('加载地址失败:', error)

View File

@@ -6,6 +6,7 @@ import { getWxOpenId } from '@/api/layout'
import { getUserInfo } from '@/api/layout'
import { saveStorageByLoginUser, SERVER_API_URL } from '@/utils/server'
import { isUserDisabled } from '@/utils/auth'
import { useUserContext } from '@/contexts/UserContext'
import request from '@/utils/request'
import {
getPhoneAuthCooldownRemaining,
@@ -108,6 +109,7 @@ const Login = () => {
const [phoneAuthPending, setPhoneAuthPending] = useState(false)
const [phoneAuthCooling, setPhoneAuthCooling] = useState(isPhoneAuthCoolingDown())
const [showContent, setShowContent] = useState(false)
const { loginUser } = useUserContext()
const phoneAuthInFlightRef = useRef(false)
const phoneAuthPendingRef = useRef(false)
const phoneAuthTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -346,6 +348,8 @@ const Login = () => {
}
saveStorageByLoginUser(token, user)
// 同步更新 UserContext 状态,确保后续页面 isLoggedIn 为 true
loginUser(token, user)
// 绑定 openid + 处理邀请关系
await ensureWxOpenIdSaved()

View File

@@ -6,6 +6,7 @@ import { TenantId } from '@/config/app'
import { getUserInfo, getWxOpenId } from '@/api/layout'
import { saveStorageByLoginUser } from '@/utils/server'
import { isUserDisabled } from '@/utils/auth'
import { useUserContext } from '@/contexts/UserContext'
import {
getStoredInviteParams,
parseInviteParams,
@@ -98,6 +99,7 @@ function isTabBarUrl(url: string) {
const Register = () => {
const [isAgree, setIsAgree] = useState(false)
const [loading, setLoading] = useState(false)
const { loginUser } = useUserContext()
// 短信验证码登录仅在非微信小程序端展示
const isWeapp = useMemo(() => {
@@ -234,6 +236,8 @@ const Register = () => {
}
saveStorageByLoginUser(token, user)
// 同步更新 UserContext 状态,确保后续页面 isLoggedIn 为 true
loginUser(token, user)
// 注册/登录成功后,立即补齐 openidJSAPI 支付必需)
try {

View File

@@ -4,12 +4,14 @@ import { View, Text, Input } from '@tarojs/components'
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
import {LoginParam} from "@/api/passport/login/model";
import {checkAndHandleInviteRelation, hasPendingInvite, parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
import { useUserContext } from '@/contexts/UserContext'
import './sms-login.scss'
const SmsLogin = () => {
const [loading, setLoading] = useState<boolean>(false)
const [sendingCode, setSendingCode] = useState<boolean>(false)
const [countdown, setCountdown] = useState<number>(0)
const { syncFromStorage } = useUserContext()
const [formData, setFormData] = useState<LoginParam>({
phone: '',
code: ''
@@ -182,6 +184,9 @@ const SmsLogin = () => {
code: formData.code
})
// loginBySms 内部已调 saveStorageByLoginUser这里同步 UserContext 状态
syncFromStorage()
// 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
if (hasPendingInvite()) {
try {