会员个人中心页面初步完成
This commit is contained in:
@@ -0,0 +1,483 @@
|
||||
<template>
|
||||
<view class="scroll-container theme-light">
|
||||
<view class="Pixso-frame-2_791">
|
||||
<MemberInfoSubNav title="个人信息" @back="goBack" />
|
||||
<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="/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="/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="/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="/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="/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="/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_859">
|
||||
<view class="frame-content-2_859">
|
||||
<text class="Pixso-paragraph-2_860">微信</text>
|
||||
<text class="Pixso-paragraph-2_861">已授权绑定</text>
|
||||
<view class="stroke-wrapper-2_862">
|
||||
<view class="Pixso-frame-2_862">
|
||||
<text class="Pixso-paragraph-2_863">已绑定</text>
|
||||
</view>
|
||||
<view class="stroke-2_862"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</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 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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
||||
import { fitnessGoalOptions } from '@/common/memberInfo/mockData.js'
|
||||
import { loadMemberStore, saveUserProfile } from '@/common/memberInfo/store.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,
|
||||
validateHeight,
|
||||
validateWeight,
|
||||
validateBirthday,
|
||||
validateFitnessGoals,
|
||||
validateUserProfile,
|
||||
showValidationError
|
||||
} from '@/common/memberInfo/validate.js'
|
||||
|
||||
const DEFAULT_AVATAR = '/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 storedAvatar = profile.avatar || DEFAULT_AVATAR
|
||||
const hasUnsavedLocalAvatar =
|
||||
options.preserveLocalAvatar &&
|
||||
this.avatarDirty &&
|
||||
this.avatar &&
|
||||
this.avatar !== storedAvatar
|
||||
|
||||
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 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
|
||||
|
||||
// 真机先用 tempFilePath 立即展示,避免 saveFile 异步期间被 onShow 覆盖
|
||||
this.setAvatar(tempPath)
|
||||
this.avatarDirty = true
|
||||
uni.showToast({ title: '头像已选择', icon: 'success' })
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@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>
|
||||
Reference in New Issue
Block a user