|
|
@@ -6,8 +6,8 @@
|
|
|
<view class="avatar">
|
|
|
<button
|
|
|
class="avatar-wrapper"
|
|
|
- :class="{ disabled: isChoosing }"
|
|
|
- :disabled="isChoosing"
|
|
|
+ :class="{ disabled: isChoosing || avatarUploading || loading }"
|
|
|
+ :disabled="loading || isChoosing || avatarUploading"
|
|
|
open-type="chooseAvatar"
|
|
|
@tap="startChooseAvatar"
|
|
|
@chooseavatar="onChooseAvatar"
|
|
|
@@ -15,6 +15,7 @@
|
|
|
<view class="avatar-frame">
|
|
|
<image v-if="form.avatar" class="avatar-img" :src="form.avatar" mode="aspectFill" />
|
|
|
<text v-else class="avatar-placeholder">点击选择头像</text>
|
|
|
+ <text v-if="avatarUploading" class="avatar-uploading">上传中...</text>
|
|
|
</view>
|
|
|
</button>
|
|
|
</view>
|
|
|
@@ -22,27 +23,27 @@
|
|
|
|
|
|
<view class="form-item">
|
|
|
<text class="label">姓名</text>
|
|
|
- <input class="input" type="nickname" v-model="form.nickname" placeholder="请输入姓名" />
|
|
|
+ <input class="input" type="nickname" v-model="form.nickname" :disabled="loading" placeholder="请输入姓名" />
|
|
|
</view>
|
|
|
|
|
|
<view class="form-item">
|
|
|
<text class="label">手机号</text>
|
|
|
- <input class="input" v-model="form.phone" placeholder="请输入手机号" type="number" />
|
|
|
+ <input class="input" v-model="form.phone" :disabled="loading" placeholder="请输入手机号" type="number" />
|
|
|
</view>
|
|
|
|
|
|
<view class="form-item">
|
|
|
<text class="label">年龄</text>
|
|
|
- <input class="input" v-model="form.age" placeholder="请输入年龄" type="number" />
|
|
|
+ <input class="input" v-model="form.age" :disabled="loading" placeholder="请输入年龄" type="number" />
|
|
|
</view>
|
|
|
|
|
|
<view class="form-item">
|
|
|
<text class="label">性别</text>
|
|
|
<view class="radio-group">
|
|
|
- <label class="radio-item" @click="form.sex = 1">
|
|
|
+ <label class="radio-item" @click="setSex(1)">
|
|
|
<radio :checked="form.sex === 1" color="#07C160" />
|
|
|
<text>男</text>
|
|
|
</label>
|
|
|
- <label class="radio-item" @click="form.sex = 2">
|
|
|
+ <label class="radio-item" @click="setSex(2)">
|
|
|
<radio :checked="form.sex === 2" color="#07C160" />
|
|
|
<text>女</text>
|
|
|
</label>
|
|
|
@@ -52,10 +53,10 @@
|
|
|
<view class="form-item">
|
|
|
<text class="label">省/市</text>
|
|
|
<view class="location-section">
|
|
|
- <picker mode="region" :value="region" @change="onRegionChange">
|
|
|
+ <picker mode="region" :value="region" :disabled="loading" @change="onRegionChange">
|
|
|
<view class="picker">{{ region.join(' ') || '请选择省/市' }}</view>
|
|
|
</picker>
|
|
|
- <button class="get-location-btn" @click="getCurrentLocation" :disabled="gettingLocation">
|
|
|
+ <button class="get-location-btn" @click="getCurrentLocation" :disabled="loading || gettingLocation">
|
|
|
{{ gettingLocation ? '获取中...' : '使用当前定位' }}
|
|
|
</button>
|
|
|
</view>
|
|
|
@@ -64,7 +65,7 @@
|
|
|
|
|
|
|
|
|
<view class="submit-section">
|
|
|
- <button class="submit-btn" @click="onSubmit" :disabled="submitting">{{ submitting ? '提交中...' : '提交' }}</button>
|
|
|
+ <button class="submit-btn" @click="onSubmit" :disabled="loading || submitting || avatarUploading">{{ submitting ? '提交中...' : avatarUploading ? '头像上传中...' : '提交' }}</button>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
@@ -72,6 +73,7 @@
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { ref } from 'vue'
|
|
|
+const loading = ref(false)
|
|
|
import { onShow } from '@dcloudio/uni-app'
|
|
|
import CustomNav from '@/components/custom-nav.vue'
|
|
|
|
|
|
@@ -87,6 +89,10 @@ const form = ref({
|
|
|
const region = ref<string[]>([])
|
|
|
const submitting = ref(false)
|
|
|
const isChoosing = ref(false)
|
|
|
+const avatarUploading = ref(false)
|
|
|
+const avatarNeedsUpload = ref(false)
|
|
|
+// 标记用户是否主动选择/更改头像(用来控制是否上传)
|
|
|
+const avatarEditedByUser = ref(false)
|
|
|
const gettingLocation = ref(false)
|
|
|
|
|
|
// 从后端拉取已有用户信息并填充表单
|
|
|
@@ -94,7 +100,9 @@ const fetchUserInfo = async () => {
|
|
|
try {
|
|
|
const token = uni.getStorageSync('token')
|
|
|
if (!token) return
|
|
|
- uni.showLoading({ title: '加载中...' })
|
|
|
+ loading.value = true
|
|
|
+ // 使用 mask: true 阻止用户在加载/下载时继续交互
|
|
|
+ uni.showLoading({ title: '加载中...', mask: true })
|
|
|
const response = await uni.request({
|
|
|
url: 'https://wx.baiyun.work/user_info',
|
|
|
method: 'POST',
|
|
|
@@ -104,7 +112,7 @@ const fetchUserInfo = async () => {
|
|
|
},
|
|
|
data: {}
|
|
|
})
|
|
|
- uni.hideLoading()
|
|
|
+
|
|
|
console.log('fetchUserInfo response:', response)
|
|
|
if (response.statusCode === 401) {
|
|
|
// 未授权,跳转登录
|
|
|
@@ -115,8 +123,31 @@ const fetchUserInfo = async () => {
|
|
|
const resp = response.data as any
|
|
|
if (resp && resp.code === 200 && resp.data) {
|
|
|
const d = resp.data
|
|
|
+ // 不再判断 avatar 地址:默认从服务端下载头像(如果有 userId)
|
|
|
+ // 这样即使后端返回 URL,也会下载到本地临时路径以便统一展示
|
|
|
+ {
|
|
|
+ const userId = d.id || d.userId
|
|
|
+ if (userId) {
|
|
|
+ try {
|
|
|
+ const downloadRes = await uni.downloadFile({
|
|
|
+ url: `https://wx.baiyun.work/user/avatar/${userId}`,
|
|
|
+ header: {
|
|
|
+ Authorization: `Bearer ${token}`
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) {
|
|
|
+ d.avatar = downloadRes.tempFilePath
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Download avatar error:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
// 填充基本字段
|
|
|
- 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.phone = d.phone || form.value.phone
|
|
|
form.value.age = d.age || form.value.age
|
|
|
@@ -136,9 +167,11 @@ const fetchUserInfo = async () => {
|
|
|
form.value.address = parts.join(' ')
|
|
|
}
|
|
|
}
|
|
|
- } catch (err) {
|
|
|
- uni.hideLoading()
|
|
|
+ } catch (err) {
|
|
|
console.error('fetchUserInfo error:', err)
|
|
|
+ } finally {
|
|
|
+ uni.hideLoading()
|
|
|
+ loading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -148,7 +181,107 @@ onShow(() => {
|
|
|
})
|
|
|
|
|
|
|
|
|
+const isHttpUrl = (url: string) => /^https?:\/\//i.test(url)
|
|
|
+
|
|
|
+// 安全设置性别:在 loading 时禁止更改
|
|
|
+const setSex = (s: number) => {
|
|
|
+ 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)
|
|
|
try {
|
|
|
const detail = e?.detail
|
|
|
@@ -169,7 +302,10 @@ const onChooseAvatar = (e: any) => {
|
|
|
}
|
|
|
if (url) {
|
|
|
form.value.avatar = url
|
|
|
- console.log('Avatar URL updated to:', form.value.avatar)
|
|
|
+ // 用户主动选择头像 -> 标记需要上传
|
|
|
+ avatarEditedByUser.value = true
|
|
|
+ avatarNeedsUpload.value = true
|
|
|
+ console.log('Avatar URL updated to:', form.value.avatar, 'needsUpload:', avatarNeedsUpload.value)
|
|
|
}
|
|
|
if (detail && detail.nickName && !form.value.nickname) {
|
|
|
form.value.nickname = detail.nickName
|
|
|
@@ -185,6 +321,13 @@ const onChooseAvatar = (e: any) => {
|
|
|
|
|
|
const startChooseAvatar = () => {
|
|
|
console.log('startChooseAvatar called, current isChoosing:', isChoosing.value)
|
|
|
+ if (loading.value) {
|
|
|
+ console.log('Loading in progress, avatar choose ignored')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 用户点击头像组件表示想要更改头像:设置为需要上传(即使最终取消也将保留此标志)
|
|
|
+ avatarEditedByUser.value = true
|
|
|
+ avatarNeedsUpload.value = true
|
|
|
if (isChoosing.value) {
|
|
|
console.log('Already choosing, ignoring')
|
|
|
return
|
|
|
@@ -292,9 +435,30 @@ const onSubmit = async () => {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ // 如果头像还没有上传到服务端(非本域或本地临时文件),先上传
|
|
|
+ try {
|
|
|
+ if (avatarUploading.value) {
|
|
|
+ uni.showToast({ title: '头像上传中,请稍候', icon: 'none' })
|
|
|
+ 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')
|
|
|
+ // 因为头像使用专属接口上传,后端不允许在 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({
|
|
|
url: 'https://wx.baiyun.work/update_user_info',
|
|
|
method: 'POST',
|
|
|
@@ -302,7 +466,7 @@ const onSubmit = async () => {
|
|
|
'Content-Type': 'application/json',
|
|
|
'Authorization': `Bearer ${token}`
|
|
|
},
|
|
|
- data: form.value
|
|
|
+ data: payload
|
|
|
})
|
|
|
console.log('Update response:', response)
|
|
|
const resp = response.data as any
|
|
|
@@ -404,6 +568,16 @@ const onSubmit = async () => {
|
|
|
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 {
|
|
|
margin-bottom: 30rpx;
|
|
|
}
|