| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- <template>
- <CustomNav title="完善基本信息" leftType="home" />
- <view class="complete-container">
- <view class="form-section">
- <view class="avatar-section">
- <view class="avatar">
- <button
- class="avatar-wrapper"
- :class="{ disabled: isChoosing || avatarUploading || loading }"
- :disabled="loading || isChoosing || avatarUploading"
- open-type="chooseAvatar"
- @tap="startChooseAvatar"
- @chooseavatar="onChooseAvatar"
- >
- <view class="avatar-frame">
- <image v-if="form.avatar" class="avatar-img" :src="form.avatar" mode="aspectFill" />
- <text v-else class="avatar-placeholder">点击选择头像</text>
- </view>
- </button>
- </view>
- </view>
- <view class="form-item">
- <text class="label">姓名</text>
- <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.age" :disabled="loading" placeholder="请输入年龄" type="number" />
- </view>
- <view class="form-item">
- <text class="label">性别</text>
- <view class="radio-group">
- <label class="radio-item" @click="setSex(1)">
- <radio :checked="form.sex === 1" color="#07C160" />
- <text>男</text>
- </label>
- <label class="radio-item" @click="setSex(2)">
- <radio :checked="form.sex === 2" color="#07C160" />
- <text>女</text>
- </label>
- </view>
- </view>
- <view class="submit-section">
- <button class="submit-btn" @click="onSubmit" :disabled="loading || submitting || avatarUploading">{{ submitting ? '提交中...' : '提交' }}</button>
- </view>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- import { ref } from 'vue'
- const isHttpUrl = (url: string) => /^https?:\/\//i.test(url)
- const loading = ref(false)
- import { onShow } from '@dcloudio/uni-app'
- import CustomNav from '@/components/custom-nav.vue'
- const form = ref({
- avatar: '',
- nickname: '',
- age: '',
- sex: 0
- })
- const submitting = ref(false)
- const isChoosing = ref(false)
- const avatarNeedsUpload = ref(false)
- const avatarUploading = ref(false)
- const avatarEditedByUser = ref(false)
- // 从后端拉取已有用户信息并填充表单
- const fetchUserInfo = async () => {
- try {
- const token = uni.getStorageSync('token')
- if (!token) return
- loading.value = true
- uni.showLoading({ title: '加载中...', mask: true })
- const response = await uni.request({
- url: 'https://wx.baiyun.work/user_info',
- method: 'POST',
- header: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`
- },
- data: {}
- })
- uni.hideLoading()
- console.log('fetchUserInfo response:', response)
- if (response.statusCode === 401) {
- // 未授权,跳转登录
- uni.removeStorageSync('token')
- uni.reLaunch({ url: '/pages/public/login/index' })
- return
- }
- 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.nickname = d.nickname || form.value.nickname
- form.value.age = d.age || form.value.age
- // sex 可能为 'MALE'/'FEMALE' 或数字/1/2
- if (d.sex) {
- if (typeof d.sex === 'string') {
- const s = d.sex.toUpperCase()
- form.value.sex = s === 'MALE' ? 1 : s === 'FEMALE' ? 2 : form.value.sex
- } else if (typeof d.sex === 'number') {
- form.value.sex = d.sex
- }
- }
- }
- } catch (err) {
- console.error('fetchUserInfo error:', err)
- } finally {
- uni.hideLoading()
- loading.value = false
-
- }
- }
- // 在页面显示时尝试填充
- onShow(() => {
- fetchUserInfo()
- })
- const onChooseAvatar = (e: any) => {
- if (loading.value) return
- console.log('onChooseAvatar called with event:', e)
- try {
- const detail = e?.detail
- console.log('Event detail:', detail)
- let url = ''
- if (!detail) {
- url = typeof e === 'string' ? e : ''
- console.log('No detail, using event as string:', url)
- } else if (typeof detail === 'string') {
- url = detail
- console.log('Detail is string:', url)
- } else if (detail.avatarUrl) {
- url = detail.avatarUrl
- console.log('Using detail.avatarUrl:', url)
- } else if (Array.isArray(detail) && detail[0]) {
- url = detail[0]
- console.log('Using first element of array:', url)
- }
- if (url) {
- form.value.avatar = url
- avatarEditedByUser.value = true
- avatarNeedsUpload.value = true // 标记需要上传
- console.log('Avatar URL updated to:', form.value.avatar)
- }
- if (detail && detail.nickName && !form.value.nickname) {
- form.value.nickname = detail.nickName
- console.log('Nickname updated from detail:', form.value.nickname)
- }
- } catch (err) {
- console.error('Error in onChooseAvatar:', err)
- } finally {
- isChoosing.value = false
- console.log('isChoosing reset to false')
- }
- }
- // 安全设置性别:在 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 = () => {
- console.log('startChooseAvatar called, current isChoosing:', isChoosing.value)
- if (isChoosing.value) {
- console.log('Already choosing, ignoring')
- return
- }
- if (loading.value) return
- avatarEditedByUser.value = true
- avatarNeedsUpload.value = true
- isChoosing.value = true
- console.log('isChoosing set to true')
- setTimeout(() => {
- isChoosing.value = false
- console.log('Timeout: isChoosing reset to false')
- }, 3000)
- }
- const onSubmit = async () => {
- if (submitting.value) return
- // 检查所有必需字段
- if (!form.value.avatar) {
- uni.showToast({ title: '请选择头像', icon: 'none' })
- return
- }
- if (!form.value.nickname) {
- uni.showToast({ title: '请输入姓名', icon: 'none' })
- return
- }
- // 姓名格式校验:2-30 个中文、字母、·或空格
- const nameRegex = /^[\u4e00-\u9fa5A-Za-z·\s]{2,10}$/
- if (!nameRegex.test(String(form.value.nickname))) {
- uni.showToast({ title: '姓名格式不正确,请输入2-10位中文或字母', icon: 'none' })
- return
- }
- if (!form.value.age) {
- uni.showToast({ title: '请输入年龄', icon: 'none' })
- return
- }
- // 年龄为 1-120 的整数
- const ageNum = Number(form.value.age)
- if (!Number.isInteger(ageNum) || ageNum < 1 || ageNum > 120) {
- uni.showToast({ title: '年龄请输入1到120之间的整数', icon: 'none' })
- return
- }
- if (!form.value.sex) {
- uni.showToast({ title: '请选择性别', icon: 'none' })
- return
- }
- submitting.value = true
- try {
- // 确保头像已上传
- const avatarUploaded = await ensureAvatarUploaded()
- if (!avatarUploaded) {
- uni.showToast({ title: '头像上传失败', icon: 'none' })
- return
- }
- const token = uni.getStorageSync('token')
- // 排除 avatar 字段,因为后端不允许在 update_user_info 中包含 avatar
- const { avatar, ...updateData } = form.value
- const response = await uni.request({
- url: 'https://wx.baiyun.work/update_user_info',
- method: 'POST',
- header: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`
- },
- data: updateData
- })
- console.log('Update response:', response)
- const resp = response.data as any
- if (resp && resp.code === 200) {
- uni.showToast({ title: '信息更新成功', icon: 'success' })
- setTimeout(() => {
- uni.switchTab({ url: '/pages/doctor/profile/index' })
- }, 1500)
- } else {
- throw new Error('Update failed')
- }
- } catch (err) {
- console.error('Update error:', err)
- uni.showToast({ title: '更新失败', icon: 'error' })
- } finally {
- submitting.value = false
- }
- }
- </script>
- <style>
- .complete-container {
- min-height: 100vh;
- /* 为固定在顶部的 CustomNav 留出空间(状态栏 + 导航栏 44px) */
- padding-top: calc(var(--status-bar-height) + 44px + 40rpx);
- /* 保留侧边与内部间距,使用 border-box 避免计算误差 */
- padding-right: 40rpx;
- padding-left: 40rpx;
- /* 底部安全区:使用项目中声明的 --window-bottom 或 fallback */
- padding-bottom: calc(var(--window-bottom, 0px) + 40rpx);
- box-sizing: border-box;
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- }
- .form-section {
- background-color: #fff;
- border-radius: 20rpx;
- padding: 40rpx;
- width: 100%;
- max-width: 600rpx;
- box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
- }
- .avatar-section {
- display: flex;
- justify-content: center;
- margin-bottom: 40rpx;
- }
- .avatar {
- width: 160rpx;
- height: 160rpx;
- border-radius: 90rpx;
- background-color: #eee;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .avatar-wrapper {
- width: 160rpx;
- height: 160rpx;
- border-radius: 90rpx;
- overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- border: 2rpx solid #07C160;
- background: #fff;
- }
- .avatar-wrapper.disabled {
- opacity: 0.5;
- }
- .avatar-frame {
- width: 100%;
- height: 100%;
- border-radius: 60rpx;
- overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .avatar-img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .avatar-placeholder {
- color: #999;
- font-size: 24rpx;
- }
- .form-item {
- margin-bottom: 30rpx;
- }
- .label {
- display: block;
- font-size: 32rpx;
- color: #333;
- margin-bottom: 10rpx;
- }
- .input {
- width: 100%;
- height: 80rpx;
- border: 1rpx solid #ddd;
- border-radius: 8rpx;
- padding: 0 20rpx;
- font-size: 32rpx;
- box-sizing: border-box;
- max-width: 100%;
- }
- .radio-group {
- display: flex;
- gap: 40rpx;
- }
- .radio-item {
- display: flex;
- align-items: center;
- font-size: 32rpx;
- color: #333;
- }
- .submit-section {
- margin-top: 60rpx;
- }
- .submit-btn {
- width: 100%;
- background: linear-gradient(135deg, #07C160 0%, #00A854 100%);
- color: #fff;
- border-radius: 12rpx;
- font-size: 32rpx;
- line-height: 80rpx;
- border: none;
- }
- .submit-btn:disabled {
- opacity: 0.5;
- }
- </style>
|