整合api请求、添加购买会员卡页面、登陆页面

This commit is contained in:
future
2026-06-23 22:17:53 +08:00
parent 1c547a717e
commit 8d8c823616
70 changed files with 7666 additions and 2656 deletions
+315 -231
View File
@@ -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';