fix(user): 修复地址和购物车相关的多个问题
- 解决鸿蒙手机首次打开地图选择不显示 POI 列表,先获取当前位置传入地图 - 修正收货地址编辑接口路径,改为无 id 的 PUT /shop/shop-user-address - 防御性合并旧地址数据,避免 null 字段覆盖初始值 - 新用户注册和登录后同步更新 UserContext 状态,确保页面刷新购物车数据 - 各页面 useDidShow 内增加从 storage 同步用户状态,解决状态不同步问题 - 优化地图选择初始化逻辑,提升用户体验 - 保证购物车、商品详情和首页登录状态及时同步更新
This commit is contained in:
@@ -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 1(API 路径不匹配)**:前端 `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` 构建成功
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 注册/登录成功后,立即补齐 openid(JSAPI 支付必需)
|
||||
try {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user