|
|
@@ -0,0 +1,492 @@
|
|
|
+<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 }"
|
|
|
+ :disabled="isChoosing"
|
|
|
+ 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" placeholder="请输入姓名" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="label">年龄</text>
|
|
|
+ <input class="input" v-model="form.age" placeholder="请输入年龄" type="number" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="label">手机号</text>
|
|
|
+ <input class="input" v-model="form.phone" placeholder="请输入手机号" type="number" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="label">性别</text>
|
|
|
+ <view class="radio-group">
|
|
|
+ <label class="radio-item" @click="form.sex = 1">
|
|
|
+ <radio :checked="form.sex === 1" color="#07C160" />
|
|
|
+ <text>男</text>
|
|
|
+ </label>
|
|
|
+ <label class="radio-item" @click="form.sex = 2">
|
|
|
+ <radio :checked="form.sex === 2" color="#07C160" />
|
|
|
+ <text>女</text>
|
|
|
+ </label>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="label">省/市</text>
|
|
|
+ <view class="location-section">
|
|
|
+ <picker mode="region" :value="region" @change="onRegionChange">
|
|
|
+ <view class="picker">{{ region.join(' ') || '请选择省/市' }}</view>
|
|
|
+ </picker>
|
|
|
+ <button class="get-location-btn" @click="getCurrentLocation" :disabled="gettingLocation">
|
|
|
+ {{ gettingLocation ? '获取中...' : '使用当前定位' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="submit-section">
|
|
|
+ <button class="submit-btn" @click="onSubmit" :disabled="submitting">{{ submitting ? '提交中...' : '提交' }}</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref } from 'vue'
|
|
|
+import { onShow } from '@dcloudio/uni-app'
|
|
|
+import CustomNav from '@/components/custom-nav.vue'
|
|
|
+
|
|
|
+const form = ref({
|
|
|
+ avatar: '',
|
|
|
+ nickname: '',
|
|
|
+ phone: '',
|
|
|
+ age: '',
|
|
|
+ sex: 0,
|
|
|
+ address: ''
|
|
|
+})
|
|
|
+
|
|
|
+const region = ref<string[]>([])
|
|
|
+const submitting = ref(false)
|
|
|
+const isChoosing = ref(false)
|
|
|
+const gettingLocation = ref(false)
|
|
|
+
|
|
|
+// 从后端拉取已有用户信息并填充表单
|
|
|
+const fetchUserInfo = async () => {
|
|
|
+ try {
|
|
|
+ const token = uni.getStorageSync('token')
|
|
|
+ if (!token) return
|
|
|
+ uni.showLoading({ title: '加载中...' })
|
|
|
+ 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
|
|
|
+ // 填充基本字段
|
|
|
+ form.value.avatar = d.avatar || form.value.avatar
|
|
|
+ form.value.nickname = d.nickname || form.value.nickname
|
|
|
+ form.value.phone = d.phone || form.value.phone
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // address 可能为 '重庆市 重庆市' 或包含空格,用空格或中文空格分割
|
|
|
+ if (d.address) {
|
|
|
+ const parts = String(d.address).split(/\s+/).filter(Boolean)
|
|
|
+ region.value = parts
|
|
|
+ form.value.address = parts.join(' ')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('fetchUserInfo error:', err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 在页面显示时尝试填充
|
|
|
+onShow(() => {
|
|
|
+ fetchUserInfo()
|
|
|
+})
|
|
|
+
|
|
|
+const onChooseAvatar = (e: any) => {
|
|
|
+ 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
|
|
|
+ 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')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const startChooseAvatar = () => {
|
|
|
+ console.log('startChooseAvatar called, current isChoosing:', isChoosing.value)
|
|
|
+ if (isChoosing.value) {
|
|
|
+ console.log('Already choosing, ignoring')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ isChoosing.value = true
|
|
|
+ console.log('isChoosing set to true')
|
|
|
+ setTimeout(() => {
|
|
|
+ isChoosing.value = false
|
|
|
+ console.log('Timeout: isChoosing reset to false')
|
|
|
+ }, 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 () => {
|
|
|
+ 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.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.sex) {
|
|
|
+ uni.showToast({ title: '请选择性别', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!form.value.address) {
|
|
|
+ uni.showToast({ title: '请选择或自动获取省/市', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ const token = uni.getStorageSync('token')
|
|
|
+ const response = await uni.request({
|
|
|
+ url: 'https://wx.baiyun.work/update_user_info',
|
|
|
+ method: 'POST',
|
|
|
+ header: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Authorization': `Bearer ${token}`
|
|
|
+ },
|
|
|
+ data: form.value
|
|
|
+ })
|
|
|
+ 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/patient-family/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;
|
|
|
+}
|
|
|
+
|
|
|
+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 {
|
|
|
+ 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>
|