558 lines
22 KiB
Vue
558 lines
22 KiB
Vue
<template>
|
|
<view class="scroll-container theme-light">
|
|
<view class="Pixso-frame-2_791">
|
|
<PageHeader title="个人信息" subtitle="" :show-back="true" />
|
|
<view class="Pixso-frame-2_802">
|
|
<view class="frame-content-2_802">
|
|
<view class="avatar-block">
|
|
<view class="avatar-block__inner">
|
|
<image
|
|
class="avatar-block__photo"
|
|
:key="avatarKey"
|
|
:src="avatarSrc"
|
|
mode="aspectFill"
|
|
@tap="previewAvatar"
|
|
/>
|
|
<view
|
|
class="avatar-block__change"
|
|
hover-class="mi-tap--hover"
|
|
:hover-stay-time="150"
|
|
@tap.stop="changeAvatar"
|
|
>
|
|
<image
|
|
class="avatar-block__icon"
|
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/camera.png"
|
|
mode="aspectFit"
|
|
/>
|
|
<text class="avatar-block__text">更换</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="Pixso-frame-2_810">
|
|
<view class="frame-content-2_810">
|
|
<view
|
|
class="Pixso-frame-2_811 user-info-row"
|
|
hover-class="mi-tap-row--hover"
|
|
:hover-stay-time="150"
|
|
@tap="editName"
|
|
>
|
|
<view class="frame-content-2_811">
|
|
<text class="Pixso-paragraph-2_812">姓名</text>
|
|
<text class="Pixso-paragraph-2_813">{{ name }}</text>
|
|
<image
|
|
class="Pixso-vector-2_814"
|
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright1.png"
|
|
mode="aspectFit"
|
|
/>
|
|
</view>
|
|
</view>
|
|
<view class="Pixso-frame-2_816"></view>
|
|
<view
|
|
class="Pixso-frame-2_817 user-info-row"
|
|
hover-class="mi-tap-row--hover"
|
|
:hover-stay-time="150"
|
|
@tap="rebindPhone"
|
|
>
|
|
<view class="frame-content-2_817">
|
|
<text class="Pixso-paragraph-2_818">手机号</text>
|
|
<view class="Pixso-frame-2_819">
|
|
<view class="frame-content-2_819">
|
|
<text class="Pixso-paragraph-2_820">{{ displayPhone }}</text>
|
|
<view
|
|
class="Pixso-frame-2_821"
|
|
hover-class="mi-tap-btn--hover"
|
|
:hover-stay-time="150"
|
|
@tap.stop="rebindPhone"
|
|
>
|
|
<text class="Pixso-paragraph-2_822">换绑</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<image
|
|
class="Pixso-vector-2_823"
|
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright.png"
|
|
mode="aspectFit"
|
|
/>
|
|
</view>
|
|
</view>
|
|
<view class="Pixso-frame-2_825"></view>
|
|
<view class="Pixso-frame-2_826">
|
|
<view class="frame-content-2_826">
|
|
<text class="Pixso-paragraph-2_827">性别</text>
|
|
<view class="Pixso-frame-2_828">
|
|
<view class="frame-content-2_828">
|
|
<view
|
|
class="gender-btn"
|
|
:class="{ 'gender-btn--active': gender === 'female' }"
|
|
hover-class="mi-tap-btn--hover"
|
|
:hover-stay-time="150"
|
|
@tap="selectGender('female')"
|
|
>
|
|
<image
|
|
class="gender-btn__icon"
|
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/venus.png"
|
|
mode="aspectFit"
|
|
/>
|
|
<text class="gender-btn__text">女</text>
|
|
</view>
|
|
<view
|
|
class="gender-btn"
|
|
:class="{ 'gender-btn--active': gender === 'male' }"
|
|
hover-class="mi-tap-btn--hover"
|
|
:hover-stay-time="150"
|
|
@tap="selectGender('male')"
|
|
>
|
|
<image
|
|
class="gender-btn__icon"
|
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mars.png"
|
|
mode="aspectFit"
|
|
/>
|
|
<text class="gender-btn__text">男</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="Pixso-frame-2_841"></view>
|
|
<picker mode="date" :value="birthdayValue" @change="onBirthdayChange">
|
|
<view
|
|
class="Pixso-frame-2_842 user-info-row"
|
|
hover-class="mi-tap-row--hover"
|
|
:hover-stay-time="150"
|
|
>
|
|
<view class="frame-content-2_842">
|
|
<text class="Pixso-paragraph-2_843">生日</text>
|
|
<text class="Pixso-paragraph-2_844">{{ birthday }}</text>
|
|
<image
|
|
class="Pixso-vector-2_845"
|
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright0.png"
|
|
mode="aspectFit"
|
|
/>
|
|
</view>
|
|
</view>
|
|
</picker>
|
|
<view class="Pixso-frame-2_847"></view>
|
|
<view class="Pixso-frame-2_848">
|
|
<view class="frame-content-2_848">
|
|
<text class="Pixso-paragraph-2_849">身高</text>
|
|
<view
|
|
class="Pixso-frame-2_850 user-info-measure"
|
|
hover-class="mi-tap-row--hover"
|
|
:hover-stay-time="150"
|
|
@tap="editHeight"
|
|
>
|
|
<view class="frame-content-2_850">
|
|
<text class="Pixso-paragraph-2_851">{{ height }}</text>
|
|
<text class="Pixso-paragraph-2_852">cm</text>
|
|
</view>
|
|
</view>
|
|
<view class="Pixso-frame-2_853"></view>
|
|
<text class="Pixso-paragraph-2_854">体重</text>
|
|
<view
|
|
class="Pixso-frame-2_855 user-info-measure"
|
|
hover-class="mi-tap-row--hover"
|
|
:hover-stay-time="150"
|
|
@tap="editWeight"
|
|
>
|
|
<view class="frame-content-2_855">
|
|
<text class="Pixso-paragraph-2_856">{{ weight }}</text>
|
|
<text class="Pixso-paragraph-2_857">kg</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="Pixso-frame-2_858"></view>
|
|
<view class="Pixso-frame-2_864">
|
|
<view class="frame-content-2_864">
|
|
<text class="Pixso-paragraph-2_865">健身目标</text>
|
|
<view class="goal-tags">
|
|
<view
|
|
v-for="goal in fitnessGoalOptions"
|
|
:key="goal"
|
|
class="goal-tag"
|
|
:class="{ 'goal-tag--selected': isGoalSelected(goal) }"
|
|
hover-class="mi-tap-btn--hover"
|
|
:hover-stay-time="150"
|
|
@tap="toggleGoal(goal)"
|
|
>
|
|
<text class="goal-tag__text">{{ goal }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="user-info-save-bar">
|
|
<view
|
|
class="user-info-save-bar__btn"
|
|
hover-class="mi-tap-save--hover"
|
|
:hover-stay-time="150"
|
|
@tap="handleSave"
|
|
>
|
|
<text class="user-info-save-bar__text">保存</text>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { onShow } from '@dcloudio/uni-app'
|
|
import PageHeader from '@/components/index/PageHeader.vue'
|
|
import { getUserInfo, updateUserInfo } from '@/api/main.js'
|
|
import { previewImage, persistChosenImage } from '@/common/memberInfo/media.js'
|
|
import { maskPhone, normalizePhoneForStore } from '@/common/memberInfo/format.js'
|
|
import { getOssUrl, OSS_BASE_URL } from '@/utils/request.js'
|
|
import {
|
|
validateName,
|
|
validatePhoneForRebind,
|
|
validateHeight,
|
|
validateWeight,
|
|
validateBirthday,
|
|
validateFitnessGoals,
|
|
validateUserProfile,
|
|
showValidationError
|
|
} from '@/common/memberInfo/validate.js'
|
|
import { backToMemberCenter } from '@/common/constants/routes.js'
|
|
|
|
const DEFAULT_AVATAR = getOssUrl('/static/headerImg/default.png')
|
|
const DEFAULT_NAME = '活氧舱用户'
|
|
|
|
const name = ref('')
|
|
const phone = ref('')
|
|
const gender = ref('female')
|
|
const birthday = ref('')
|
|
const height = ref('')
|
|
const weight = ref('')
|
|
const fitnessGoals = ref([])
|
|
const avatar = ref(DEFAULT_AVATAR)
|
|
const avatarKey = ref(0)
|
|
const avatarDirty = ref(false)
|
|
const fitnessGoalOptions = ref(['减脂1', '增肌', '塑形', '增重', '康复', '体能'])
|
|
const loading = ref(false)
|
|
|
|
const avatarSrc = computed(() => {
|
|
return avatar.value || DEFAULT_AVATAR
|
|
})
|
|
|
|
const displayPhone = computed(() => {
|
|
return maskPhone(phone.value)
|
|
})
|
|
|
|
const birthdayValue = computed(() => {
|
|
const match = String(birthday.value).match(/(\d{4})年(\d{2})月(\d{2})日/)
|
|
if (match) {
|
|
return `${match[1]}-${match[2]}-${match[3]}`
|
|
}
|
|
return '1995-06-15'
|
|
})
|
|
|
|
function goBack() {
|
|
backToMemberCenter()
|
|
}
|
|
|
|
function mapApiToProfile(apiData) {
|
|
const genderMap = {
|
|
'MALE': 'male',
|
|
'FEMALE': 'female',
|
|
'UNKNOWN': 'female'
|
|
}
|
|
let birthdayVal = ''
|
|
if (apiData.birthday) {
|
|
const parts = apiData.birthday.split('-')
|
|
if (parts.length === 3) {
|
|
birthdayVal = `${parts[0]}年${parts[1]}月${parts[2]}日`
|
|
}
|
|
}
|
|
return {
|
|
name: apiData.nickname || DEFAULT_NAME,
|
|
phone: apiData.phone || '',
|
|
gender: genderMap[apiData.gender] || 'female',
|
|
birthday: birthdayVal,
|
|
avatar: apiData.avatar || DEFAULT_AVATAR
|
|
}
|
|
}
|
|
|
|
function mapProfileToApi(profile) {
|
|
const genderMap = {
|
|
'male': 'MALE',
|
|
'female': 'FEMALE'
|
|
}
|
|
let birthdayVal = ''
|
|
if (profile.birthday) {
|
|
const match = String(profile.birthday).match(/(\d{4})年(\d{2})月(\d{2})日/)
|
|
if (match) {
|
|
birthdayVal = `${match[1]}-${match[2]}-${match[3]}`
|
|
}
|
|
}
|
|
return {
|
|
nickname: profile.name,
|
|
gender: genderMap[profile.gender] || 'UNKNOWN',
|
|
birthday: birthdayVal,
|
|
avatar: profile.avatar
|
|
}
|
|
}
|
|
|
|
async function fetchUserInfo() {
|
|
// 如果正在加载中,不重复请求
|
|
if (loading.value) {
|
|
console.log('[userInfo] 正在加载中,跳过重复请求')
|
|
return
|
|
}
|
|
loading.value = true
|
|
console.log('[userInfo] fetchUserInfo 开始执行')
|
|
console.log('[userInfo] 步骤1: 调用 getUserInfo API...')
|
|
try {
|
|
const res = await getUserInfo({ cache: false })
|
|
console.log('[userInfo] 步骤2: API返回,res =', res)
|
|
console.log('[userInfo] 步骤3: res类型 =', typeof res, 'res.data =', res?.data)
|
|
|
|
// 兼容两种数据格式:1.直接返回数据对象 2.返回 {code, data, message} 格式
|
|
const apiData = res.data || res
|
|
console.log('[userInfo] 步骤4: apiData =', apiData)
|
|
|
|
if (apiData && (apiData.nickname !== undefined || apiData.phone !== undefined)) {
|
|
console.log('[userInfo] 步骤5: 收到有效数据,更新页面')
|
|
const profile = mapApiToProfile(apiData)
|
|
console.log('[userInfo] 步骤6: 映射后数据 =', profile)
|
|
name.value = profile.name
|
|
phone.value = profile.phone
|
|
gender.value = profile.gender
|
|
birthday.value = profile.birthday
|
|
setAvatar(profile.avatar)
|
|
console.log('[userInfo] 步骤7: 页面数据已更新,name =', name.value)
|
|
} else {
|
|
console.log('[userInfo] 步骤5: 数据无效,跳过更新')
|
|
}
|
|
} catch (err) {
|
|
console.error('[userInfo] 获取用户信息失败:', err)
|
|
uni.showToast({ title: '获取信息失败: ' + (err.message || '网络错误'), icon: 'none' })
|
|
} finally {
|
|
loading.value = false
|
|
console.log('[userInfo] fetchUserInfo 执行完成')
|
|
}
|
|
}
|
|
|
|
function loadProfile(options = {}) {
|
|
// API模式下从data直接获取,不从store获取
|
|
}
|
|
|
|
function setAvatar(path) {
|
|
const next = path || DEFAULT_AVATAR
|
|
if (avatar.value !== next) {
|
|
avatar.value = next
|
|
avatarKey.value += 1
|
|
}
|
|
}
|
|
|
|
function getProfilePayload() {
|
|
return {
|
|
name: name.value,
|
|
phone: normalizePhoneForStore(phone.value),
|
|
gender: gender.value,
|
|
birthday: birthday.value,
|
|
height: height.value,
|
|
weight: weight.value,
|
|
fitnessGoals: [...fitnessGoals.value],
|
|
avatar: avatar.value
|
|
}
|
|
}
|
|
|
|
function handleSave() {
|
|
const result = validateUserProfile(
|
|
getProfilePayload(),
|
|
fitnessGoalOptions.value
|
|
)
|
|
if (!result.ok) {
|
|
showValidationError(result.message)
|
|
return
|
|
}
|
|
|
|
applyValidatedProfile(result.value)
|
|
avatarDirty.value = false
|
|
|
|
saveUserInfo(result.value)
|
|
}
|
|
|
|
async function saveUserInfo(profile) {
|
|
loading.value = true
|
|
try {
|
|
const apiData = mapProfileToApi(profile)
|
|
console.log('[userInfo] PUT /api/member/info 请求:', apiData)
|
|
const res = await updateUserInfo(apiData)
|
|
console.log('[userInfo] PUT /api/member/info 返回:', res)
|
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
|
setTimeout(() => goBack(), 600)
|
|
} catch (err) {
|
|
console.error('保存用户信息失败', err)
|
|
uni.showToast({ title: '保存失败', icon: 'none' })
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function applyValidatedProfile(profile) {
|
|
name.value = profile.name
|
|
phone.value = profile.phone
|
|
gender.value = profile.gender
|
|
birthday.value = profile.birthday
|
|
height.value = profile.height
|
|
weight.value = profile.weight
|
|
fitnessGoals.value = [...profile.fitnessGoals]
|
|
}
|
|
|
|
function changeAvatar() {
|
|
uni.chooseImage({
|
|
count: 1,
|
|
sizeType: ['compressed'],
|
|
sourceType: ['album', 'camera'],
|
|
success: (res) => {
|
|
const tempPath =
|
|
res.tempFilePaths?.[0] || res.tempFiles?.[0]?.tempFilePath
|
|
if (!tempPath) return
|
|
avatarDirty.value = true
|
|
setAvatar(tempPath)
|
|
persistChosenImage(tempPath)
|
|
}
|
|
})
|
|
}
|
|
|
|
function previewAvatar() {
|
|
previewImage(avatarSrc.value)
|
|
}
|
|
|
|
function editName() {
|
|
uni.showModal({
|
|
title: '修改姓名',
|
|
editable: true,
|
|
placeholderText: '请输入姓名',
|
|
content: String(name.value),
|
|
success: (res) => {
|
|
if (!res.confirm) return
|
|
const result = validateName(res.content)
|
|
if (!result.ok) {
|
|
showValidationError(result.message)
|
|
return
|
|
}
|
|
name.value = result.value
|
|
}
|
|
})
|
|
}
|
|
|
|
function rebindPhone() {
|
|
uni.showModal({
|
|
title: '换绑手机号',
|
|
editable: true,
|
|
placeholderText: '请输入新手机号',
|
|
content: '',
|
|
success: (res) => {
|
|
if (!res.confirm) return
|
|
const result = validatePhoneForRebind(res.content)
|
|
if (!result.ok) {
|
|
showValidationError(result.message)
|
|
return
|
|
}
|
|
phone.value = normalizePhoneForStore(result.value)
|
|
}
|
|
})
|
|
}
|
|
|
|
function selectGender(g) {
|
|
gender.value = g
|
|
}
|
|
|
|
function onBirthdayChange(e) {
|
|
const val = e.detail.value
|
|
if (!val) return
|
|
const parts = val.split('-')
|
|
if (parts.length === 3) {
|
|
birthday.value = `${parts[0]}年${parts[1]}月${parts[2]}日`
|
|
}
|
|
}
|
|
|
|
function editHeight() {
|
|
uni.showModal({
|
|
title: '修改身高',
|
|
editable: true,
|
|
placeholderText: '100-250,单位 cm',
|
|
content: String(height.value),
|
|
success: (res) => {
|
|
if (!res.confirm) return
|
|
const result = validateHeight(res.content)
|
|
if (!result.ok) {
|
|
showValidationError(result.message)
|
|
return
|
|
}
|
|
height.value = result.value
|
|
}
|
|
})
|
|
}
|
|
|
|
function editWeight() {
|
|
uni.showModal({
|
|
title: '修改体重',
|
|
editable: true,
|
|
placeholderText: '20-300,单位 kg',
|
|
content: String(weight.value),
|
|
success: (res) => {
|
|
if (!res.confirm) return
|
|
const result = validateWeight(res.content)
|
|
if (!result.ok) {
|
|
showValidationError(result.message)
|
|
return
|
|
}
|
|
weight.value = result.value
|
|
}
|
|
})
|
|
}
|
|
|
|
function toggleGoal(goal) {
|
|
const index = fitnessGoals.value.indexOf(goal)
|
|
if (index >= 0) {
|
|
fitnessGoals.value.splice(index, 1)
|
|
return
|
|
}
|
|
const preview = [...fitnessGoals.value, goal]
|
|
const result = validateFitnessGoals(preview, fitnessGoalOptions.value)
|
|
if (!result.ok) {
|
|
showValidationError(result.message)
|
|
return
|
|
}
|
|
fitnessGoals.value.push(goal)
|
|
}
|
|
|
|
function isGoalSelected(goal) {
|
|
return fitnessGoals.value.includes(goal)
|
|
}
|
|
|
|
onMounted(() => {
|
|
console.log('[userInfo] onMounted 被调用,开始获取用户信息')
|
|
fetchUserInfo()
|
|
})
|
|
|
|
onShow(() => {
|
|
console.log('[userInfo] onShow 被调用')
|
|
// 每次进入页面都尝试获取最新数据
|
|
fetchUserInfo()
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@import '@/common/style/base.css';
|
|
@import '@/common/style/memberInfo/pages/page-reset.css';
|
|
@import '@/common/style/memberInfo/pages/sub-page-base.css';
|
|
@import '@/common/style/memberInfo/member-info-component-reset.css';
|
|
@import '@/common/style/memberInfo/member-info-sub-nav.css';
|
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
|
@import '@/common/style/memberInfo/pages/user-info-page.css';
|
|
@import '@/common/style/memberInfo/pages/user-info-pixso.css';
|
|
|
|
.user-info-row,
|
|
.user-info-measure {
|
|
border-radius: 8px;
|
|
}
|
|
</style>
|