|
@@ -15,6 +15,7 @@
|
|
|
<view class="avatar-frame">
|
|
<view class="avatar-frame">
|
|
|
<image v-if="form.avatar" class="avatar-img" :src="form.avatar" mode="aspectFill" />
|
|
<image v-if="form.avatar" class="avatar-img" :src="form.avatar" mode="aspectFill" />
|
|
|
<text v-else class="avatar-placeholder">点击选择头像</text>
|
|
<text v-else class="avatar-placeholder">点击选择头像</text>
|
|
|
|
|
+ <text v-if="avatarUploading" class="avatar-uploading">上传中...</text>
|
|
|
</view>
|
|
</view>
|
|
|
</button>
|
|
</button>
|
|
|
</view>
|
|
</view>
|
|
@@ -25,6 +26,11 @@
|
|
|
<input class="input" type="nickname" v-model="form.nickname" :disabled="loading" placeholder="请输入姓名" />
|
|
<input class="input" type="nickname" v-model="form.nickname" :disabled="loading" placeholder="请输入姓名" />
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <text class="label">手机号</text>
|
|
|
|
|
+ <input class="input" v-model="form.phone" :disabled="loading" placeholder="请输入手机号" type="number" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
<view class="form-item">
|
|
<view class="form-item">
|
|
|
<text class="label">年龄</text>
|
|
<text class="label">年龄</text>
|
|
|
<input class="input" v-model="form.age" :disabled="loading" placeholder="请输入年龄" type="number" />
|
|
<input class="input" v-model="form.age" :disabled="loading" placeholder="请输入年龄" type="number" />
|
|
@@ -44,8 +50,22 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <text class="label">省/市</text>
|
|
|
|
|
+ <view class="location-section">
|
|
|
|
|
+ <picker mode="region" :value="region" :disabled="loading" @change="onRegionChange">
|
|
|
|
|
+ <view class="picker">{{ region.join(' ') || '请选择省/市' }}</view>
|
|
|
|
|
+ </picker>
|
|
|
|
|
+ <button class="get-location-btn" @click="getCurrentLocation" :disabled="loading || gettingLocation">
|
|
|
|
|
+ {{ gettingLocation ? '获取中...' : '使用当前定位' }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
<view class="submit-section">
|
|
<view class="submit-section">
|
|
|
- <button class="submit-btn" @click="onSubmit" :disabled="loading || submitting || avatarUploading">{{ submitting ? '提交中...' : '提交' }}</button>
|
|
|
|
|
|
|
+ <button class="submit-btn" @click="onSubmit" :disabled="loading || submitting || avatarUploading">{{ submitting ? '提交中...' : avatarUploading ? '头像上传中...' : '提交' }}</button>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -53,7 +73,6 @@
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref } from 'vue'
|
|
import { ref } from 'vue'
|
|
|
-const isHttpUrl = (url: string) => /^https?:\/\//i.test(url)
|
|
|
|
|
const loading = ref(false)
|
|
const loading = ref(false)
|
|
|
import { onShow } from '@dcloudio/uni-app'
|
|
import { onShow } from '@dcloudio/uni-app'
|
|
|
import CustomNav from '@/components/custom-nav.vue'
|
|
import CustomNav from '@/components/custom-nav.vue'
|
|
@@ -61,15 +80,20 @@ import CustomNav from '@/components/custom-nav.vue'
|
|
|
const form = ref({
|
|
const form = ref({
|
|
|
avatar: '',
|
|
avatar: '',
|
|
|
nickname: '',
|
|
nickname: '',
|
|
|
|
|
+ phone: '',
|
|
|
age: '',
|
|
age: '',
|
|
|
- sex: 0
|
|
|
|
|
|
|
+ sex: 0,
|
|
|
|
|
+ address: ''
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+const region = ref<string[]>([])
|
|
|
const submitting = ref(false)
|
|
const submitting = ref(false)
|
|
|
const isChoosing = ref(false)
|
|
const isChoosing = ref(false)
|
|
|
-const avatarNeedsUpload = ref(false)
|
|
|
|
|
const avatarUploading = ref(false)
|
|
const avatarUploading = ref(false)
|
|
|
|
|
+const avatarNeedsUpload = ref(false)
|
|
|
|
|
+// 标记用户是否主动选择/更改头像(用来控制是否上传)
|
|
|
const avatarEditedByUser = ref(false)
|
|
const avatarEditedByUser = ref(false)
|
|
|
|
|
+const gettingLocation = ref(false)
|
|
|
|
|
|
|
|
// 从后端拉取已有用户信息并填充表单
|
|
// 从后端拉取已有用户信息并填充表单
|
|
|
const fetchUserInfo = async () => {
|
|
const fetchUserInfo = async () => {
|
|
@@ -77,6 +101,7 @@ const fetchUserInfo = async () => {
|
|
|
const token = uni.getStorageSync('token')
|
|
const token = uni.getStorageSync('token')
|
|
|
if (!token) return
|
|
if (!token) return
|
|
|
loading.value = true
|
|
loading.value = true
|
|
|
|
|
+ // 使用 mask: true 阻止用户在加载/下载时继续交互
|
|
|
uni.showLoading({ title: '加载中...', mask: true })
|
|
uni.showLoading({ title: '加载中...', mask: true })
|
|
|
const response = await uni.request({
|
|
const response = await uni.request({
|
|
|
url: 'https://wx.baiyun.work/user_info',
|
|
url: 'https://wx.baiyun.work/user_info',
|
|
@@ -87,7 +112,7 @@ const fetchUserInfo = async () => {
|
|
|
},
|
|
},
|
|
|
data: {}
|
|
data: {}
|
|
|
})
|
|
})
|
|
|
- uni.hideLoading()
|
|
|
|
|
|
|
+
|
|
|
console.log('fetchUserInfo response:', response)
|
|
console.log('fetchUserInfo response:', response)
|
|
|
if (response.statusCode === 401) {
|
|
if (response.statusCode === 401) {
|
|
|
// 未授权,跳转登录
|
|
// 未授权,跳转登录
|
|
@@ -106,7 +131,9 @@ const fetchUserInfo = async () => {
|
|
|
try {
|
|
try {
|
|
|
const downloadRes = await uni.downloadFile({
|
|
const downloadRes = await uni.downloadFile({
|
|
|
url: `https://wx.baiyun.work/user/avatar/${userId}`,
|
|
url: `https://wx.baiyun.work/user/avatar/${userId}`,
|
|
|
- header: { Authorization: `Bearer ${token}` }
|
|
|
|
|
|
|
+ header: {
|
|
|
|
|
+ Authorization: `Bearer ${token}`
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) {
|
|
if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) {
|
|
|
d.avatar = downloadRes.tempFilePath
|
|
d.avatar = downloadRes.tempFilePath
|
|
@@ -117,8 +144,12 @@ const fetchUserInfo = async () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
// 填充基本字段
|
|
// 填充基本字段
|
|
|
- form.value.avatar = d.avatar || form.value.avatar
|
|
|
|
|
|
|
+ form.value.avatar = d.avatar || form.value.avatar
|
|
|
|
|
+ // 完成从服务器获取资料后默认为不上传,等待用户主动修改头像
|
|
|
|
|
+ avatarNeedsUpload.value = false
|
|
|
|
|
+ avatarEditedByUser.value = false
|
|
|
form.value.nickname = d.nickname || form.value.nickname
|
|
form.value.nickname = d.nickname || form.value.nickname
|
|
|
|
|
+ form.value.phone = d.phone || form.value.phone
|
|
|
form.value.age = d.age || form.value.age
|
|
form.value.age = d.age || form.value.age
|
|
|
// sex 可能为 'MALE'/'FEMALE' 或数字/1/2
|
|
// sex 可能为 'MALE'/'FEMALE' 或数字/1/2
|
|
|
if (d.sex) {
|
|
if (d.sex) {
|
|
@@ -129,13 +160,18 @@ const fetchUserInfo = async () => {
|
|
|
form.value.sex = d.sex
|
|
form.value.sex = d.sex
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ // address 可能为 '重庆市 重庆市' 或包含空格,用空格或中文空格分割
|
|
|
|
|
+ if (d.address) {
|
|
|
|
|
+ const parts = String(d.address).split(/\s+/).filter(Boolean)
|
|
|
|
|
+ region.value = parts
|
|
|
|
|
+ form.value.address = parts.join(' ')
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- } catch (err) {
|
|
|
|
|
|
|
+ } catch (err) {
|
|
|
console.error('fetchUserInfo error:', err)
|
|
console.error('fetchUserInfo error:', err)
|
|
|
} finally {
|
|
} finally {
|
|
|
uni.hideLoading()
|
|
uni.hideLoading()
|
|
|
loading.value = false
|
|
loading.value = false
|
|
|
-
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -144,8 +180,108 @@ onShow(() => {
|
|
|
fetchUserInfo()
|
|
fetchUserInfo()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const onChooseAvatar = (e: any) => {
|
|
|
|
|
|
|
+
|
|
|
|
|
+const isHttpUrl = (url: string) => /^https?:\/\//i.test(url)
|
|
|
|
|
+
|
|
|
|
|
+// 安全设置性别:在 loading 时禁止更改
|
|
|
|
|
+const setSex = (s: number) => {
|
|
|
if (loading.value) return
|
|
if (loading.value) return
|
|
|
|
|
+ form.value.sex = s
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 上传头像到 `user/avatar/upload`,返回上传后的 URL(如果服务器返回)
|
|
|
|
|
+ */
|
|
|
|
|
+const uploadAvatar = (filePath: string) => {
|
|
|
|
|
+ return new Promise<string>((resolve, reject) => {
|
|
|
|
|
+ if (!filePath) {
|
|
|
|
|
+ reject(new Error('no filePath'))
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ avatarUploading.value = true
|
|
|
|
|
+ uni.showLoading({ title: '上传头像中...' })
|
|
|
|
|
+ const token = uni.getStorageSync('token')
|
|
|
|
|
+ uni.uploadFile({
|
|
|
|
|
+ url: 'https://wx.baiyun.work/user/avatar/upload',
|
|
|
|
|
+ filePath,
|
|
|
|
|
+ name: 'file',
|
|
|
|
|
+ header: {
|
|
|
|
|
+ Authorization: `Bearer ${token}`
|
|
|
|
|
+ },
|
|
|
|
|
+ success: (uploadRes: any) => {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ try {
|
|
|
|
|
+ const resp = JSON.parse(uploadRes.data || '{}') as any
|
|
|
|
|
+ if (resp && resp.code === 200) {
|
|
|
|
|
+ // 尝试从响应中取出 URL 字段(后端可能返回 data.url 或 data.path 等)
|
|
|
|
|
+ const uploadedUrl = resp.data?.url || resp.data?.path || resp.data?.fileUrl || ''
|
|
|
|
|
+ if (uploadedUrl) {
|
|
|
|
|
+ form.value.avatar = uploadedUrl
|
|
|
|
|
+ }
|
|
|
|
|
+ // 标记为已上传
|
|
|
|
|
+ avatarNeedsUpload.value = false
|
|
|
|
|
+ uni.showToast({ title: '头像上传成功', icon: 'success' })
|
|
|
|
|
+ resolve(uploadedUrl || '')
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({ title: resp?.message || '上传失败', icon: 'none' })
|
|
|
|
|
+ reject(new Error(resp?.message || 'upload failed'))
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('upload parse error', err)
|
|
|
|
|
+ uni.showToast({ title: '头像上传失败', icon: 'none' })
|
|
|
|
|
+ reject(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err: any) => {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ console.error('upload fail', err)
|
|
|
|
|
+ uni.showToast({ title: '头像上传失败', icon: 'none' })
|
|
|
|
|
+ reject(err)
|
|
|
|
|
+ },
|
|
|
|
|
+ complete: () => {
|
|
|
|
|
+ avatarUploading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 确保头像已上传到服务器;如果不是本域,则先下载再上传
|
|
|
|
|
+ */
|
|
|
|
|
+const ensureAvatarUploaded = async (): Promise<void> => {
|
|
|
|
|
+ if (!form.value.avatar) {
|
|
|
|
|
+ throw new Error('no avatar')
|
|
|
|
|
+ }
|
|
|
|
|
+ // 不再根据域名跳过上传:只要用户标记为编辑(avatarNeedsUpload),就上传
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是远程 URL,需要先下载(包括本站域名的 URL)
|
|
|
|
|
+ if (isHttpUrl(form.value.avatar)) {
|
|
|
|
|
+ uni.showLoading({ title: '准备上传头像...' })
|
|
|
|
|
+ try {
|
|
|
|
|
+ const d = await uni.downloadFile({ url: form.value.avatar })
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ if (d.statusCode === 200 && d.tempFilePath) {
|
|
|
|
|
+ await uploadAvatar(d.tempFilePath)
|
|
|
|
|
+ avatarNeedsUpload.value = false
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new Error('download failed')
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ throw err
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 否则可能是平台返回的本地临时路径,直接上传
|
|
|
|
|
+ await uploadAvatar(form.value.avatar)
|
|
|
|
|
+ avatarNeedsUpload.value = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onChooseAvatar = (e: any) => {
|
|
|
|
|
+ if (loading.value) {
|
|
|
|
|
+ console.log('onChooseAvatar ignored because loading')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
console.log('onChooseAvatar called with event:', e)
|
|
console.log('onChooseAvatar called with event:', e)
|
|
|
try {
|
|
try {
|
|
|
const detail = e?.detail
|
|
const detail = e?.detail
|
|
@@ -166,9 +302,10 @@ const onChooseAvatar = (e: any) => {
|
|
|
}
|
|
}
|
|
|
if (url) {
|
|
if (url) {
|
|
|
form.value.avatar = url
|
|
form.value.avatar = url
|
|
|
|
|
+ // 用户主动选择头像 -> 标记需要上传
|
|
|
avatarEditedByUser.value = true
|
|
avatarEditedByUser.value = true
|
|
|
- avatarNeedsUpload.value = true // 标记需要上传
|
|
|
|
|
- console.log('Avatar URL updated to:', form.value.avatar)
|
|
|
|
|
|
|
+ avatarNeedsUpload.value = true
|
|
|
|
|
+ console.log('Avatar URL updated to:', form.value.avatar, 'needsUpload:', avatarNeedsUpload.value)
|
|
|
}
|
|
}
|
|
|
if (detail && detail.nickName && !form.value.nickname) {
|
|
if (detail && detail.nickName && !form.value.nickname) {
|
|
|
form.value.nickname = detail.nickName
|
|
form.value.nickname = detail.nickName
|
|
@@ -182,82 +319,19 @@ const onChooseAvatar = (e: any) => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 安全设置性别:在 loading 时禁止更改
|
|
|
|
|
-const setSex = (s: number) => {
|
|
|
|
|
- if (loading.value) return
|
|
|
|
|
- form.value.sex = s
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const uploadAvatar = async (avatarUrl: string): Promise<string | null> => {
|
|
|
|
|
- try {
|
|
|
|
|
- avatarUploading.value = true
|
|
|
|
|
- const token = uni.getStorageSync('token')
|
|
|
|
|
- const uploadRes = await uni.uploadFile({
|
|
|
|
|
- url: 'https://wx.baiyun.work/user/avatar/upload',
|
|
|
|
|
- filePath: avatarUrl,
|
|
|
|
|
- name: 'file',
|
|
|
|
|
- header: {
|
|
|
|
|
- Authorization: `Bearer ${token}`
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- if (uploadRes.statusCode === 200) {
|
|
|
|
|
- const resp = JSON.parse(uploadRes.data)
|
|
|
|
|
- if (resp && resp.code === 200 && resp.data) {
|
|
|
|
|
- return resp.data // 返回上传后的头像 URL
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- throw new Error('Upload failed')
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('Upload avatar error:', e)
|
|
|
|
|
- avatarUploading.value = false
|
|
|
|
|
- return null
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const ensureAvatarUploaded = async (): Promise<boolean> => {
|
|
|
|
|
- if (!avatarNeedsUpload.value) return true
|
|
|
|
|
- const avatarUrl = form.value.avatar
|
|
|
|
|
- if (!avatarUrl) return false
|
|
|
|
|
- // 不再根据 avatar 地址判断跳过上传:只要 avatarNeedsUpload 为 true,就执行上传流程
|
|
|
|
|
- // 如果是远端 URL,先下载成临时文件再上传;若是本地临时文件则直接上传。
|
|
|
|
|
- if (avatarUrl.startsWith('http')) {
|
|
|
|
|
- try {
|
|
|
|
|
- uni.showLoading({ title: '准备上传头像...' })
|
|
|
|
|
- const d = await uni.downloadFile({ url: avatarUrl })
|
|
|
|
|
- uni.hideLoading()
|
|
|
|
|
- if (d.statusCode === 200 && d.tempFilePath) {
|
|
|
|
|
- const uploadedUrl = await uploadAvatar(d.tempFilePath)
|
|
|
|
|
- if (uploadedUrl) {
|
|
|
|
|
- form.value.avatar = uploadedUrl
|
|
|
|
|
- avatarNeedsUpload.value = false
|
|
|
|
|
- return true
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- uni.hideLoading()
|
|
|
|
|
- throw err
|
|
|
|
|
- }
|
|
|
|
|
- return false
|
|
|
|
|
- }
|
|
|
|
|
- // 否则,上传头像
|
|
|
|
|
- const uploadedUrl = await uploadAvatar(avatarUrl)
|
|
|
|
|
- if (uploadedUrl) {
|
|
|
|
|
- form.value.avatar = uploadedUrl
|
|
|
|
|
- avatarNeedsUpload.value = false
|
|
|
|
|
- return true
|
|
|
|
|
- }
|
|
|
|
|
- return false
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
const startChooseAvatar = () => {
|
|
const startChooseAvatar = () => {
|
|
|
console.log('startChooseAvatar called, current isChoosing:', isChoosing.value)
|
|
console.log('startChooseAvatar called, current isChoosing:', isChoosing.value)
|
|
|
- if (isChoosing.value) {
|
|
|
|
|
- console.log('Already choosing, ignoring')
|
|
|
|
|
|
|
+ if (loading.value) {
|
|
|
|
|
+ console.log('Loading in progress, avatar choose ignored')
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- if (loading.value) return
|
|
|
|
|
|
|
+ // 用户点击头像组件表示想要更改头像:设置为需要上传(即使最终取消也将保留此标志)
|
|
|
avatarEditedByUser.value = true
|
|
avatarEditedByUser.value = true
|
|
|
avatarNeedsUpload.value = true
|
|
avatarNeedsUpload.value = true
|
|
|
|
|
+ if (isChoosing.value) {
|
|
|
|
|
+ console.log('Already choosing, ignoring')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
isChoosing.value = true
|
|
isChoosing.value = true
|
|
|
console.log('isChoosing set to true')
|
|
console.log('isChoosing set to true')
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
@@ -266,6 +340,54 @@ const startChooseAvatar = () => {
|
|
|
}, 3000)
|
|
}, 3000)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+const onRegionChange = (e: any) => {
|
|
|
|
|
+ region.value = e.detail.value
|
|
|
|
|
+ form.value.address = region.value.join(' ')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getCurrentLocation = async () => {
|
|
|
|
|
+ if (gettingLocation.value) return
|
|
|
|
|
+ gettingLocation.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 获取经纬度
|
|
|
|
|
+ const locationRes = await uni.getLocation({ type: 'wgs84' })
|
|
|
|
|
+ const { latitude, longitude } = locationRes
|
|
|
|
|
+ console.log('getLocation success:', locationRes)
|
|
|
|
|
+
|
|
|
|
|
+ // 调用用户自己的地理编码接口
|
|
|
|
|
+ const token = uni.getStorageSync('token')
|
|
|
|
|
+ const geocodeRes = await uni.request({
|
|
|
|
|
+ url: `https://wx.baiyun.work/geo/nearest?latitude=${latitude}&longitude=${longitude}`,
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ header: {
|
|
|
|
|
+ 'Authorization': `Bearer ${token}`
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const data = geocodeRes.data as any
|
|
|
|
|
+ if (data && data.code === 200 && data.data) {
|
|
|
|
|
+ // 解析 data 字段中的 JSON 字符串
|
|
|
|
|
+ const addressData = JSON.parse(data.data)
|
|
|
|
|
+ if (addressData && addressData.province && addressData.city) {
|
|
|
|
|
+ region.value = [addressData.province, addressData.city, addressData.district].filter(Boolean)
|
|
|
|
|
+ form.value.address = region.value.join(' ')
|
|
|
|
|
+ uni.showToast({ title: '位置获取成功', icon: 'success' })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new Error('Invalid address data')
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new Error('Geocode failed')
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Get location error:', err)
|
|
|
|
|
+ uni.showToast({ title: '获取位置失败,请检查定位权限和网络', icon: 'none' })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ gettingLocation.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const onSubmit = async () => {
|
|
const onSubmit = async () => {
|
|
|
if (submitting.value) return
|
|
if (submitting.value) return
|
|
|
|
|
|
|
@@ -284,6 +406,16 @@ const onSubmit = async () => {
|
|
|
uni.showToast({ title: '姓名格式不正确,请输入2-10位中文或字母', icon: 'none' })
|
|
uni.showToast({ title: '姓名格式不正确,请输入2-10位中文或字母', icon: 'none' })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+ if (!form.value.phone) {
|
|
|
|
|
+ uni.showToast({ title: '请输入手机号', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ // 手机号格式(中国)校验
|
|
|
|
|
+ const phoneRegex = /^1[3-9]\d{9}$/
|
|
|
|
|
+ if (!phoneRegex.test(String(form.value.phone))) {
|
|
|
|
|
+ uni.showToast({ title: '手机号格式不正确', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
if (!form.value.age) {
|
|
if (!form.value.age) {
|
|
|
uni.showToast({ title: '请输入年龄', icon: 'none' })
|
|
uni.showToast({ title: '请输入年龄', icon: 'none' })
|
|
|
return
|
|
return
|
|
@@ -298,19 +430,35 @@ const onSubmit = async () => {
|
|
|
uni.showToast({ title: '请选择性别', icon: 'none' })
|
|
uni.showToast({ title: '请选择性别', icon: 'none' })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+ if (!form.value.address) {
|
|
|
|
|
+ uni.showToast({ title: '请选择或自动获取省/市', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- submitting.value = true
|
|
|
|
|
|
|
+ // 如果头像还没有上传到服务端(非本域或本地临时文件),先上传
|
|
|
try {
|
|
try {
|
|
|
- // 确保头像已上传
|
|
|
|
|
- const avatarUploaded = await ensureAvatarUploaded()
|
|
|
|
|
- if (!avatarUploaded) {
|
|
|
|
|
- uni.showToast({ title: '头像上传失败', icon: 'none' })
|
|
|
|
|
|
|
+ if (avatarUploading.value) {
|
|
|
|
|
+ uni.showToast({ title: '头像上传中,请稍候', icon: 'none' })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+ // 只有当 avatarNeedsUpload 为 true,才会触发上传,避免重复上传
|
|
|
|
|
+ if (avatarNeedsUpload.value) {
|
|
|
|
|
+ await ensureAvatarUploaded()
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Ensure avatar uploaded error:', err)
|
|
|
|
|
+ uni.showToast({ title: '头像上传失败,无法提交', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ submitting.value = true
|
|
|
|
|
+ try {
|
|
|
const token = uni.getStorageSync('token')
|
|
const token = uni.getStorageSync('token')
|
|
|
- // 排除 avatar 字段,因为后端不允许在 update_user_info 中包含 avatar
|
|
|
|
|
- const { avatar, ...updateData } = form.value
|
|
|
|
|
|
|
+ // 因为头像使用专属接口上传,后端不允许在 update_user_info 中包含 avatar 字段
|
|
|
|
|
+ // 所以这里构造 payload 时删除 avatar
|
|
|
|
|
+ const payload: any = JSON.parse(JSON.stringify(form.value))
|
|
|
|
|
+ if (payload.avatar !== undefined) delete payload.avatar
|
|
|
|
|
+
|
|
|
const response = await uni.request({
|
|
const response = await uni.request({
|
|
|
url: 'https://wx.baiyun.work/update_user_info',
|
|
url: 'https://wx.baiyun.work/update_user_info',
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
@@ -318,14 +466,14 @@ const onSubmit = async () => {
|
|
|
'Content-Type': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
|
'Authorization': `Bearer ${token}`
|
|
'Authorization': `Bearer ${token}`
|
|
|
},
|
|
},
|
|
|
- data: updateData
|
|
|
|
|
|
|
+ data: payload
|
|
|
})
|
|
})
|
|
|
console.log('Update response:', response)
|
|
console.log('Update response:', response)
|
|
|
const resp = response.data as any
|
|
const resp = response.data as any
|
|
|
if (resp && resp.code === 200) {
|
|
if (resp && resp.code === 200) {
|
|
|
uni.showToast({ title: '信息更新成功', icon: 'success' })
|
|
uni.showToast({ title: '信息更新成功', icon: 'success' })
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
|
- uni.switchTab({ url: '/pages/doctor/profile/index' })
|
|
|
|
|
|
|
+ uni.switchTab({ url: '/pages/patient/profile/index' })
|
|
|
}, 1500)
|
|
}, 1500)
|
|
|
} else {
|
|
} else {
|
|
|
throw new Error('Update failed')
|
|
throw new Error('Update failed')
|
|
@@ -420,6 +568,16 @@ const onSubmit = async () => {
|
|
|
font-size: 24rpx;
|
|
font-size: 24rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.avatar-uploading {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 10rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ background: rgba(0,0,0,0.4);
|
|
|
|
|
+ padding: 6rpx 10rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.form-item {
|
|
.form-item {
|
|
|
margin-bottom: 30rpx;
|
|
margin-bottom: 30rpx;
|
|
|
}
|
|
}
|
|
@@ -442,6 +600,25 @@ const onSubmit = async () => {
|
|
|
max-width: 100%;
|
|
max-width: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.phone-section {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.get-phone-btn {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ height: 80rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, #07C160 0%, #00A854 100%);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.get-phone-btn:disabled {
|
|
|
|
|
+ opacity: 0.5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.radio-group {
|
|
.radio-group {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
gap: 40rpx;
|
|
gap: 40rpx;
|
|
@@ -454,6 +631,45 @@ const onSubmit = async () => {
|
|
|
color: #333;
|
|
color: #333;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+picker {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 80rpx;
|
|
|
|
|
+ border: 1rpx solid #ddd;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ padding: 0 20rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ color: hsl(0, 0%, 20%);
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-item, .picker, .input, .get-location-btn, .submit-btn {
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.location-section {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.get-location-btn {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ height: 80rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, #07C160 0%, #00A854 100%);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ padding: 0 10rpx;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.get-location-btn:disabled {
|
|
|
|
|
+ opacity: 0.5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.submit-section {
|
|
.submit-section {
|
|
|
margin-top: 60rpx;
|
|
margin-top: 60rpx;
|
|
|
}
|
|
}
|
|
@@ -471,4 +687,4 @@ const onSubmit = async () => {
|
|
|
.submit-btn:disabled {
|
|
.submit-btn:disabled {
|
|
|
opacity: 0.5;
|
|
opacity: 0.5;
|
|
|
}
|
|
}
|
|
|
-</style>
|
|
|
|
|
|
|
+</style>
|