整合api请求、添加购买会员卡页面、登陆页面
This commit is contained in:
@@ -210,13 +210,13 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { fitnessGoalOptions } from '@/common/memberInfo/mockData.js'
|
||||
import { loadMemberStore, saveUserProfile } from '@/common/memberInfo/store.js'
|
||||
import { getUserInfo, updateUserInfo } from '@/api/main.js'
|
||||
import { previewImage, persistChosenImage } from '@/common/memberInfo/media.js'
|
||||
import { maskPhone, normalizePhoneForStore } from '@/common/memberInfo/format.js'
|
||||
import { subPageMixin } from '@/common/memberInfo/mixins.js'
|
||||
import {
|
||||
validateName,
|
||||
validatePhoneForRebind,
|
||||
@@ -227,246 +227,330 @@ import {
|
||||
validateUserProfile,
|
||||
showValidationError
|
||||
} from '@/common/memberInfo/validate.js'
|
||||
import { backToMemberCenter } from '@/common/constants/routes.js'
|
||||
|
||||
const DEFAULT_AVATAR = 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
|
||||
|
||||
export default {
|
||||
components: { MemberInfoSubNav },
|
||||
mixins: [subPageMixin],
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
phone: '',
|
||||
gender: 'female',
|
||||
birthday: '',
|
||||
height: '',
|
||||
weight: '',
|
||||
fitnessGoals: [],
|
||||
avatar: DEFAULT_AVATAR,
|
||||
avatarKey: 0,
|
||||
avatarDirty: false,
|
||||
fitnessGoalOptions
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
avatarSrc() {
|
||||
return this.avatar || DEFAULT_AVATAR
|
||||
},
|
||||
displayPhone() {
|
||||
return maskPhone(this.phone)
|
||||
},
|
||||
birthdayValue() {
|
||||
const match = String(this.birthday).match(/(\d{4})年(\d{2})月(\d{2})日/)
|
||||
if (match) {
|
||||
return `${match[1]}-${match[2]}-${match[3]}`
|
||||
}
|
||||
return '1995-06-15'
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
// 从相册/相机返回会再次触发 onShow,不能覆盖刚选未保存的头像
|
||||
this.loadProfile({ preserveLocalAvatar: true })
|
||||
},
|
||||
methods: {
|
||||
loadProfile(options = {}) {
|
||||
const store = loadMemberStore()
|
||||
const profile = store.profile
|
||||
this.name = profile.name
|
||||
this.phone = profile.phone
|
||||
this.gender = profile.gender
|
||||
this.birthday = profile.birthday
|
||||
this.height = profile.height
|
||||
this.weight = profile.weight
|
||||
this.fitnessGoals = [...(profile.fitnessGoals || [])]
|
||||
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 storedAvatar = profile.avatar || DEFAULT_AVATAR
|
||||
const hasUnsavedLocalAvatar =
|
||||
options.preserveLocalAvatar &&
|
||||
this.avatarDirty &&
|
||||
this.avatar &&
|
||||
this.avatar !== storedAvatar
|
||||
const avatarSrc = computed(() => {
|
||||
return avatar.value || DEFAULT_AVATAR
|
||||
})
|
||||
|
||||
if (!hasUnsavedLocalAvatar) {
|
||||
this.setAvatar(storedAvatar)
|
||||
this.avatarDirty = false
|
||||
}
|
||||
},
|
||||
setAvatar(path) {
|
||||
const next = path || DEFAULT_AVATAR
|
||||
if (this.avatar !== next) {
|
||||
this.avatar = next
|
||||
this.avatarKey += 1
|
||||
}
|
||||
},
|
||||
getProfilePayload() {
|
||||
return {
|
||||
name: this.name,
|
||||
phone: normalizePhoneForStore(this.phone),
|
||||
gender: this.gender,
|
||||
birthday: this.birthday,
|
||||
height: this.height,
|
||||
weight: this.weight,
|
||||
fitnessGoals: [...this.fitnessGoals],
|
||||
avatar: this.avatar
|
||||
}
|
||||
},
|
||||
handleSave() {
|
||||
const result = validateUserProfile(
|
||||
this.getProfilePayload(),
|
||||
this.fitnessGoalOptions
|
||||
)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
const displayPhone = computed(() => {
|
||||
return maskPhone(phone.value)
|
||||
})
|
||||
|
||||
const store = loadMemberStore()
|
||||
saveUserProfile(store, result.value)
|
||||
this.applyValidatedProfile(result.value)
|
||||
this.avatarDirty = false
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => this.goBack(), 600)
|
||||
},
|
||||
applyValidatedProfile(profile) {
|
||||
this.name = profile.name
|
||||
this.phone = profile.phone
|
||||
this.gender = profile.gender
|
||||
this.birthday = profile.birthday
|
||||
this.height = profile.height
|
||||
this.weight = profile.weight
|
||||
this.fitnessGoals = [...profile.fitnessGoals]
|
||||
},
|
||||
changeAvatar() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const tempPath =
|
||||
res.tempFilePaths?.[0] || res.tempFiles?.[0]?.tempFilePath
|
||||
if (!tempPath) return
|
||||
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'
|
||||
})
|
||||
|
||||
// 真机先用 tempFilePath 立即展示,避免 saveFile 异步期间被 onShow 覆盖
|
||||
this.setAvatar(tempPath)
|
||||
this.avatarDirty = true
|
||||
uni.showToast({ title: '头像已选择', icon: 'success' })
|
||||
function goBack() {
|
||||
backToMemberCenter()
|
||||
}
|
||||
|
||||
persistChosenImage(tempPath).then((savedPath) => {
|
||||
if (savedPath && savedPath !== this.avatar) {
|
||||
this.setAvatar(savedPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
previewAvatar() {
|
||||
previewImage(this.avatarSrc, DEFAULT_AVATAR)
|
||||
},
|
||||
editName() {
|
||||
uni.showModal({
|
||||
title: '修改姓名',
|
||||
editable: true,
|
||||
placeholderText: '请输入姓名(2-8字)',
|
||||
content: this.name,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validateName(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.name = result.value
|
||||
}
|
||||
})
|
||||
},
|
||||
rebindPhone() {
|
||||
uni.showModal({
|
||||
title: '换绑手机号',
|
||||
editable: true,
|
||||
placeholderText: '请输入11位手机号',
|
||||
content: normalizePhoneForStore(this.phone) || this.phone,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validatePhoneForRebind(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.phone = result.value
|
||||
uni.showToast({ title: '手机号已更新', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
selectGender(gender) {
|
||||
this.gender = gender
|
||||
},
|
||||
onBirthdayChange(e) {
|
||||
const value = e.detail.value
|
||||
const [y, m, d] = value.split('-')
|
||||
const formatted = `${y}年${m}月${d}日`
|
||||
const result = validateBirthday(formatted)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.birthday = result.value
|
||||
},
|
||||
editHeight() {
|
||||
uni.showModal({
|
||||
title: '修改身高',
|
||||
editable: true,
|
||||
placeholderText: '50-250,单位 cm',
|
||||
content: String(this.height),
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validateHeight(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.height = result.value
|
||||
}
|
||||
})
|
||||
},
|
||||
editWeight() {
|
||||
uni.showModal({
|
||||
title: '修改体重',
|
||||
editable: true,
|
||||
placeholderText: '20-300,单位 kg',
|
||||
content: String(this.weight),
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const result = validateWeight(res.content)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.weight = result.value
|
||||
}
|
||||
})
|
||||
},
|
||||
toggleGoal(goal) {
|
||||
const index = this.fitnessGoals.indexOf(goal)
|
||||
if (index >= 0) {
|
||||
this.fitnessGoals.splice(index, 1)
|
||||
return
|
||||
}
|
||||
const preview = [...this.fitnessGoals, goal]
|
||||
const result = validateFitnessGoals(preview, this.fitnessGoalOptions)
|
||||
if (!result.ok) {
|
||||
showValidationError(result.message)
|
||||
return
|
||||
}
|
||||
this.fitnessGoals.push(goal)
|
||||
},
|
||||
isGoalSelected(goal) {
|
||||
return this.fitnessGoals.includes(goal)
|
||||
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 || '',
|
||||
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>
|
||||
<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';
|
||||
|
||||
Reference in New Issue
Block a user