修改首页获取地理权限逻辑和添加用户信息修改接口

This commit is contained in:
wangxiaowei
2025-12-02 16:09:57 +08:00
parent 9247f5bff4
commit 6707bfd876
13 changed files with 269 additions and 129 deletions

6
env/.env vendored
View File

@ -8,7 +8,7 @@ VITE_WX_APPID = 'wxa2abb91f64032a2b'
VITE_APP_PUBLIC_BASE = '/h5'
# 登录页面
VITE_LOGIN_URL = '/pages/login/mobile'
VITE_LOGIN_URL = '/pages/my/my'
# 第一个请求地址
VITE_SERVER_BASEURL = 'https://cz.stnav.com'
@ -25,6 +25,6 @@ VITE_API_SECONDARY_URL = 'https://cz.stnav.com'
VITE_WX_SERVICE_ACCOUNT_APPID = 'wx0224f558e3b3f499'
# 默认地址
VITE_DEFAULT_LONGITUDE = 113.665412
VITE_DEFAULT_LATITUDE = 34.757975
VITE_DEFAULT_LONGITUDE = 121.473629
VITE_DEFAULT_LATITUDE = 31.230393
VITE_DEFAULT_ADDRESS = '上海市'

View File

@ -46,3 +46,34 @@ export function getTeaSpecialist(data: ITeaSpecialistParams) {
}
)
}
/**
* 经纬度转换为城市
*/
export interface ILocationToCityParams {
latitude: number
longitude: number
}
export function getLocationToCity(data: ILocationToCityParams) {
return http.Post<{
message: string
result: {
ad_info: {
province: string
city: string
district: string
adcode: string
},
location: {
lat: number
lng: number
}
}
}>('/api/common/cityAddress',
data,
{
meta: { ignoreAuth: true }
}
)
}

View File

@ -48,3 +48,25 @@ export interface IUserCouponListResult {
no_use: Array<any>
use: Array<any>
}
/**
* 用户信息接口返回
*/
export interface IUserResult {
id: number
sn: number
sex: "未知" | "男" | "女"
account: string
nickname: string
real_name: string
avatar: string
collect_count: number
coupon_count: number
create_time: string
has_auth: boolean
has_password: boolean
member: number
mobile: string
user_money: string
version: string
}

View File

@ -1,13 +1,13 @@
import { http } from '@/http/alova'
import type { IUserAddressListResult, IUserAddressDetailsResult, IUserCouponListResult } from '@/api/types/user'
import type { IOrderListResult } from '@/api/types/order'
import type { IUserResult } from '@/api/types/user'
/**
* 获取用户个人信息
*/
export function getUserInfo() {
return http.Post('/api/user/info')
return http.Post<IUserResult>('/api/user/info')
}
/**
@ -131,3 +131,15 @@ export interface IGetUserMoneyLogParams {
export function getUserMoneyLog(data: IGetUserMoneyLogParams) {
return http.Post<IOrderListResult>('/api/user/moneyLogList', data)
}
/**
* 修改用户信息
*/
export interface IUpdateUserInfoParams {
field: string,
value: string | number
}
export function updateUserInfo(data: IUpdateUserInfoParams) {
return http.Post('/api/user/setInfo', data)
}

View File

@ -442,6 +442,10 @@
OrderDetail.handleInit()
})
onUnload(() => {
uni.$off('refreshOrderDetail')
})
const OrderDetail = {
// 获取订单详情
handleInit: async () => {

View File

@ -21,11 +21,13 @@
<view class="flex items-center">
<view class="mr-10rpx">
<wd-upload
:header="{'token': tk}"
:file-list="fileList"
:limit="1"
image-mode="scaleToFill"
:action="action">
<wd-img width="64rpx" height="64rpx" :src="`${OSS}icon/icon_avatar.png`" mode="aspectFill" round />
:action="action"
@success="Profile.handleUploadSuccess">
<wd-img width="64rpx" height="64rpx" :src="user.avatar" mode="aspectFill" round />
</wd-upload>
</view>
<wd-icon name="arrow-right" size="32rpx" color="#C0C4CC" />
@ -39,7 +41,7 @@
<view class="flex justify-end">
<view class="flex items-center">
<view class="font-400 text-[#303133] text-30rpx leading-42rpx">
王小伟
{{ user.nickname }}
</view>
<view>
<wd-icon name="arrow-right" size="32rpx" color="#C0C4CC" />
@ -54,7 +56,7 @@
<view class="flex justify-end">
<view class="flex items-center">
<view class="font-400 text-[#303133] text-30rpx leading-42rpx">
+86 155****5456
+86 {{ maskedMobile }}
</view>
<view>
<wd-icon name="arrow-right" size="32rpx" color="#C0C4CC" />
@ -124,34 +126,92 @@
import {toast} from '@/utils/toast'
import { useUserStore } from '@/store'
import { router } from '@/utils/tools'
import { getUserInfo, updateUserInfo } from '@/api/user'
import type { IUserResult } from '@/api/types/user'
const OSS = inject('OSS')
const showLogoutPopup = ref<boolean>(false) // 是否显示退出登录弹出框
// 上传文件
const fileList = ref<any[]>([])
const action = 'https://www.mocky.io/v2/5cc8019d300000980a055e76' // 仅做测试使用,实际请换成真实上传接口
const action = 'https://cz.stnav.com/api/upload/image' // 仅做测试使用,实际请换成真实上传接口
// 修改昵称
const showEditNicknamePopup = ref<boolean>(false) // 是否显示退款详情弹出框
const nickname = ref<string>('') // 昵称
// 用户信息相关
const user = ref<IUserResult>({
id: 0,
sn: 0,
sex: "未知",
account: "",
nickname: "",
real_name: "",
avatar: "",
collect_count: 0,
coupon_count: 0,
create_time: "",
has_auth: false,
has_password: false,
member: 0,
mobile: "",
user_money: "0.00",
version: ""
})
const tk = ref<string>('') // 用户token
onLoad(() => {
const userStore = useUserStore()
const { token } = userStore.userInfo
tk.value = token
Profile.handleInit()
})
const Profile = {
// 图片选择/删除
handleChange: (e: any) => {
console.log("🚀 ~ e:", e)
/**
* 初始化用户信息
*/
handleInit: () => {
getUserInfo().then(res => {
user.value = res
})
},
// 保存昵称
handleSaveNickname: () => {
handleUploadSuccess: async (e: any) => {
try {
const response = JSON.parse(e.file.response)
if (response.code) {
const avatarUrl = response.data.uri
await updateUserInfo({ field: 'avatar', value: avatarUrl })
user.value.avatar = avatarUrl
toast.info('头像上传成功')
} else {
throw new Error('上传失败')
}
} catch (error) {
toast.info('上传失败')
}
},
/**
* 保存昵称
*/
handleSaveNickname: async () => {
if (!nickname.value) {
toast.info('请输入昵称')
return
}
await updateUserInfo({ field: 'nickname', value: nickname.value })
showEditNicknamePopup.value = false
user.value.nickname = nickname.value
toast.info('昵称修改成功')
},
// 修改手机号
handleToEditMobile: () => {
uni.navigateTo({
@ -169,6 +229,15 @@
}
}
}
/**
* 掩码处理手机号
*/
const maskedMobile = computed(() => {
if (!user.value.mobile) return ''
// 只处理11位手机号
return user.value.mobile.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')
})
</script>
<style lang="scss">

View File

@ -27,19 +27,19 @@ export const getUrlCode = (): { [key: string]: string | undefined } => {
*/
export async function snsapiBaseAuthorize() {
// TODO 测试代码
// wxSnsapiBaseLogin({code: '011ganGa10NGEK0reKGa1l3rpS2ganGX'}).then((res: IUserInfoVo) => {
// console.log("登录成功 ~ snsapiBaseAuthorize ~ res:", res)
// // 映射 IUserLogin 到 IUserInfoVo
// useUserStore().setUserInfo(res)
// uni.$emit('loginSuccess')
wxSnsapiBaseLogin({code: '081fXl0w3YeN563rel1w37QK751fXl0l'}).then((res: IUserInfoVo) => {
console.log("登录成功 ~ snsapiBaseAuthorize ~ res:", res)
// 映射 IUserLogin 到 IUserInfoVo
useUserStore().setUserInfo(res)
uni.$emit('loginSuccess')
// }).catch(err => {
// // 失败就重新授权
// uni.setStorageSync('wechatCode', 0)
// console.log('请求失败', err)
// })
}).catch(err => {
// 失败就重新授权
uni.setStorageSync('wechatCode', 0)
console.log('请求失败', err)
})
// return
return
let local = window.location.href // 获取页面url
let appid = import.meta.env.VITE_WX_SERVICE_ACCOUNT_APPID // 公众号的APPID
@ -62,22 +62,22 @@ export async function snsapiBaseAuthorize() {
uni.setStorageSync('wechatCode',code)
// 使用code换取用户信息同步写法
uni.showLoading({title: '登录中...'})
try {
const res: IUserInfoVo = await wxSnsapiBaseLogin({code})
console.log("登录成功 ~ snsapiBaseAuthorize ~ res:", res)
useUserStore().setUserInfo(res)
// if (!res.mobile) {
// // 如果没有绑定手机号的话需要去绑定手机号
// toast.info('请先绑定手机号')
// router.navigateTo('/pages/login/mobile', 500)
// } else {
uni.$emit('loginSuccess')
// try {
// const res: IUserInfoVo = await wxSnsapiBaseLogin({code})
// console.log("登录成功 ~ snsapiBaseAuthorize ~ res:", res)
// useUserStore().setUserInfo(res)
// // if (!res.mobile) {
// // // 如果没有绑定手机号的话需要去绑定手机号
// // toast.info('请先绑定手机号')
// // router.navigateTo('/pages/login/mobile', 500)
// // } else {
// uni.$emit('loginSuccess')
// // }
// uni.hideLoading()
// } catch (err) {
// uni.setStorageSync('wechatCode', 0)
// uni.hideLoading()
// console.log('请求失败', err)
// }
uni.hideLoading()
} catch (err) {
uni.setStorageSync('wechatCode', 0)
uni.hideLoading()
console.log('请求失败', err)
}
}
}

View File

@ -118,6 +118,7 @@
{
"path": "pages/index/detail",
"type": "page",
"needLogin": "true",
"layout": "default",
"style": {
"navigationStyle": "custom"

View File

@ -1,5 +1,6 @@
<route lang="jsonc" type="page">
{
"needLogin": "true",
"layout": "default",
"style": {
"navigationStyle": "custom"

View File

@ -157,7 +157,7 @@
import TeaSpecialistLevel from '@/components/TeaSpecialistLevel.vue'
import {wxGetLocation} from '@/utils/jwexin'
import {TeaSpecialistLevelValue} from '@/utils/teaSpecialist'
import {getDecorate, getTeaSpecialistLevels, getTeaSpecialist} from '@/api/home'
import {getDecorate, getTeaSpecialistLevels, getTeaSpecialist, getLocationToCity} from '@/api/home'
import {getCity} from '@/api/city'
import type { IIndexListResult } from '@/api/types/home'
import { router } from '@/utils/tools'
@ -177,14 +177,10 @@
auto: true,
textNoMore: '~ 已经到底啦 ~', //无更多数据的提示
}
const latitude = ref<number>(import.meta.env.VITE_DEFAULT_LATITUDE) // 纬度
const longitude = ref<number>(import.meta.env.VITE_DEFAULT_LONGITUDE) // 经度
const latitude = ref<number>(0) // 纬度
const longitude = ref<number>(0) // 经度
// 经纬度缓存过期处理1小时
const LOCATION_EXPIRE_KEY = 'location_expire_time'
const LOCATION_EXPIRE_MS = 30 * 24 * 60 * 60 * 1000 // 1个月
const LOCATION_DENY_KEY = 'location_deny_time'
const LOCATION_DENY_INTERVAL = 60 * 1000 // 1分钟
const defaultCity = ref<string>(import.meta.env.VITE_DEFAULT_ADDRESS) // 默认城市
const list = ref<Array<any>>([]) // 茶艺师列表
const teaSpecialistName = ref<string>('') // 茶艺师名称
@ -197,8 +193,69 @@
TeaSpecialistLevels.push(...res)
})
// 检查缓存是否过期,超时则重新授权
await Index.handleEnsureLocationAuth()
// 1. 检查经纬度缓存是否有效
const expire = uni.getStorageSync('location_expire_time')
const cacheLat = uni.getStorageSync('latitude')
const cacheLng = uni.getStorageSync('longitude')
const cacheCity= uni.getStorageSync('city')
if (expire && Date.now() < expire && cacheLat && cacheLng) {
// 缓存有效,直接用缓存
latitude.value = cacheLat
longitude.value = cacheLng
defaultCity.value = cacheCity || import.meta.env.VITE_DEFAULT_ADDRESS
} else {
// 2. 检查拒绝授权时间30秒内不再弹窗
const denyTime = uni.getStorageSync('location_deny_time')
if (denyTime && Date.now() - denyTime < 5 * 1000) {
// 拒绝后30秒内使用默认经纬度和城市
latitude.value = import.meta.env.VITE_DEFAULT_LATITUDE
longitude.value = import.meta.env.VITE_DEFAULT_LONGITUDE
defaultCity.value = import.meta.env.VITE_DEFAULT_ADDRESS
} else {
// 3. 请求地理位置授权
await new Promise((resolve) => {
wxGetLocation(async (res) => {
if (res === 'end') {
// 不处理
resolve(true)
return
}
if (res && res.latitude && res.longitude) {
latitude.value = res.latitude
longitude.value = res.longitude
// 如果授权成功则获取城市名称
const locRes = await getLocationToCity({latitude: res.latitude, longitude: res.longitude})
if (locRes && locRes.message === "Success") {
uni.setStorageSync('latitude', locRes.result.location.lat)
uni.setStorageSync('longitude', locRes.result.location.lng)
uni.setStorageSync('city', locRes.result.ad_info.city)
defaultCity.value = locRes.result.ad_info.city || import.meta.env.VITE_DEFAULT_ADDRESS
} else {
uni.setStorageSync('latitude', res.latitude)
uni.setStorageSync('longitude', res.longitude)
}
uni.setStorageSync('location_expire_time', Date.now() + 30 * 24 * 60 * 60 * 1000)
Index.handleGetCityList()
Index.handleSearch()
} else {
// 授权失败或拒绝缓存拒绝时间30秒
uni.setStorageSync('location_deny_time', Date.now())
// 使用默认经纬度和城市
latitude.value = import.meta.env.VITE_DEFAULT_LATITUDE
longitude.value = import.meta.env.VITE_DEFAULT_LONGITUDE
defaultCity.value = import.meta.env.VITE_DEFAULT_ADDRESS
Index.handleGetCityList()
Index.handleSearch()
}
resolve(true)
})
})
}
}
// getDecorate({id: 1}).then((res: any) => {
// const data = JSON.parse(res.data)
@ -228,8 +285,11 @@
})
},
// 获取城市列表
handleGetCityList: () => {
console.log("🚀 ~ latitude.value2:", latitude.value, longitude.value)
getCity({latitude: latitude.value, longitude: longitude.value}).then((res: any) => {
cityColumns.value = res.area_list.map(item => {
return {
@ -240,74 +300,6 @@
})
},
// 设置经纬度缓存
handleSetLocationCache: (lat: number, lng: number) => {
uni.setStorageSync('latitude', lat)
uni.setStorageSync('longitude', lng)
uni.setStorageSync(LOCATION_EXPIRE_KEY, Date.now() + LOCATION_EXPIRE_MS)
},
// 检查经纬度缓存是否过期
handleCheckLocationCache: () => {
const expire = uni.getStorageSync(LOCATION_EXPIRE_KEY)
if (expire && Date.now() > expire) {
uni.removeStorageSync('latitude')
uni.removeStorageSync('longitude')
uni.removeStorageSync(LOCATION_EXPIRE_KEY)
return false
}
return true
},
// 初始化经纬度
handleEnsureLocationAuth: async () => {
// 检查缓存是否过期
if (!Index.handleCheckLocationCache()) {
// 超时,重新获取授权
await Index.handleRequestLocationAuth()
} else {
const lat = uni.getStorageSync('latitude')
const lng = uni.getStorageSync('longitude')
if (lat && lng) {
latitude.value = lat
longitude.value = lng
} else {
await Index.handleRequestLocationAuth()
}
}
},
// 定位授权弹窗逻辑
handleRequestLocationAuth: async () => {
// 检查上次弹窗时间1分钟内不再弹
const lastDeny = uni.getStorageSync(LOCATION_DENY_KEY)
if (lastDeny && Date.now() - lastDeny < LOCATION_DENY_INTERVAL) {
return
}
try {
await wxGetLocation((res) => {
console.log("🚀 ~ res:", res)
if (res) {
latitude.value = res.latitude
longitude.value = res.longitude
}
Index.handleSetLocationCache(latitude.value, longitude.value)
Index.handleSearch()
Index.handleGetCityList()
})
} catch (e) {
// 用户拒绝授权,记录弹窗时间
uni.setStorageSync(LOCATION_DENY_KEY, Date.now())
// 可选:弹窗提示
uni.showModal({
title: '提示',
content: '需要获取您的地理位置以提供更好的服务,请在授权弹窗中允许定位。',
showCancel: false
})
}
},
// 选择城市
handleSelectCity: (e: any) => {
cityValue.value = e.value

View File

@ -25,16 +25,16 @@
<!-- 账号昵称显示 -->
<view class="ml-60rpx flex items-center">
<view>
<wd-img width="120rpx" height="120rpx" :src="`${OSS}icon/icon_avatar.png`" mode="aspectFill" round />
<wd-img width="120rpx" height="120rpx" :src="isLogin ? user.avatar : `${OSS}icon/icon_avatar.png`" mode="aspectFill" round />
</view>
<view class="flex-1 ml-22rpx flex justify-between items-center" @click="My.handleToProfile">
<!-- <view class="flex-1 ml-22rpx flex justify-between items-center"> -->
<view>
<view class="text-[#303133] text-36rpx leading-50rpx ml-8rpx">{{ isLogin ? userInfo.nickname : '立即登录' }}</view>
<view class="text-[#303133] text-36rpx leading-50rpx ml-8rpx">{{ isLogin ? user.nickname : '立即登录' }}</view>
<view v-if="isLogin" class="flex justify-center items-center vip-bg mt-10rpx" >
<!-- 会员显示图标 -->
<view v-if="isVip" class="flex items-center mr-12rpx">
<wd-img width="36rpx" height="36rpx" mode="aspectFill" :src="userInfo.avatar" round></wd-img>
<wd-img width="36rpx" height="36rpx" mode="aspectFill" :src="`${OSS}icon/icon_crown.png`" round></wd-img>
</view>
<!-- 这里要根据用户身份显示不同的文字 -->
<view class="text-24rpx text-[#675649] leading-34rpx flex items-center">茶址会员</view>
@ -272,17 +272,20 @@
},
handleInitUserInfo: (code: string = '') => {
const userStore = useUserStore()
if (code && !userStore.userInfo.token) {
// 这里是微信授权之后跳转回到本页面获取到code但是没有登录状态的话需要进行登录操作
snsapiBaseAuthorize()
} else if (userStore.isLoggedIn) {
userInfo.value = userStore.userInfo
isLogin.value = userStore.isLoggedIn
// 获取用户详情信息接口
getUserInfo().then(res => {
user.value = res
console.log("🚀 ~ user.value:", user.value)
})
}
},

View File

@ -7,6 +7,8 @@
import { useUserStore } from '@/store'
import { tabbarStore } from '@/tabbar/store'
import { needLoginPages as _needLoginPages, getLastPage, getNeedLoginPages } from '@/utils'
import { router } from '@/utils/tools'
import { toast } from '@/utils/toast'
// TODO Check
const loginRoute = import.meta.env.VITE_LOGIN_URL
@ -53,7 +55,9 @@ export const navigateToInterceptor = {
}
tabbarStore.restorePrevIdx()
const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}`
uni.navigateTo({ url: redirectRoute })
// uni.navigateTo({ url: redirectRoute })
toast.info('请先登录')
router.switchTab(redirectRoute, 500)
return false
},
}

View File

@ -65,12 +65,13 @@ export async function wxGetLocation(callback: (res: any) => void ) {
console.log('授权失败:' + res)
},
complete:function(res){
console.log(res)
if (res.errMsg === 'getLocation:cancel') {
// 用户拒绝授权
uni.setStorageSync('location_deny_time', Date.now())
}
callback(false)
} else {
callback('end')
}
}
});
});