Files
gym-manage/gym-manage-uniapp/pages/profile/profile.vue
T

346 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="profile-page">
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
<text class="nav-title">&#9679; 个人中心</text>
<text class="settings-symbol" @click="goToSettings">&#9881;</text>
</view>
</view>
<scroll-view class="content" scroll-y :style="{ height: 'calc(100vh - ' + totalHeaderHeight + 'px)' }">
<view class="content-inner">
<view class="user-card">
<view class="user-row">
<image class="big-avatar-img" :src="avatarSrc" mode="aspectFill" @error="onAvatarError" />
<view class="user-info">
<text class="user-name">{{ userInfo.nickname }}</text>
<view class="user-tag">
<text>会员号{{ userInfo.memberNo }}</text>
</view>
</view>
<text class="edit-symbol" @click="openEdit">&#9998;</text>
</view>
<view class="user-details">
<view class="detail-row">
<text class="detail-label">手机号</text>
<text class="detail-value">{{ userInfo.phone }}</text>
</view>
<view class="detail-row">
<text class="detail-label">性别</text>
<text class="detail-value">{{ genderLabel }}</text>
</view>
<view class="detail-row">
<text class="detail-label">生日</text>
<text class="detail-value">{{ userInfo.birthday || '未设置' }}</text>
</view>
<view class="detail-row">
<text class="detail-label">上次登录</text>
<text class="detail-value">{{ userInfo.lastLoginAt || '暂无' }}</text>
</view>
</view>
</view>
<view class="stats-card">
<view class="stat-item">
<text class="stat-number">{{ userInfo.totalSignInDays }}</text>
<text class="stat-label">累计签到()</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-number">{{ userInfo.monthSignInCount }}</text>
<text class="stat-label">本月签到</text>
</view>
</view>
<view class="sign-section">
<view class="sign-header">
<view class="section-title-row">
<text class="section-symbol">&#9776;</text>
<text class="section-title">最近签到</text>
</view>
<text class="section-more" @click="viewAllSignIn">查看全部</text>
</view>
<view v-for="(record, idx) in signInRecords" :key="idx" class="sign-item">
<view class="sign-left">
<text class="sign-date">{{ record.date }}</text>
<text class="sign-time">{{ record.time }}</text>
</view>
<view class="sign-right">
<text class="sign-type">{{ record.typeLabel }}</text>
<text class="sign-source">{{ record.sourceLabel }}</text>
<text class="sign-status" :class="record.status">{{ record.statusLabel }}</text>
</view>
</view>
</view>
<view class="bottom-safe"></view>
</view>
</scroll-view>
<!-- 编辑资料弹窗 -->
<view v-if="showEdit" class="edit-overlay" @click="closeEdit">
<view class="edit-modal" @click.stop>
<view class="edit-modal-header">
<text class="edit-modal-title">编辑资料</text>
<text class="edit-modal-close" @click="closeEdit">&#10005;</text>
</view>
<scroll-view class="edit-modal-body" scroll-y>
<view class="edit-field">
<text class="edit-label">昵称</text>
<input class="edit-input" type="text" :value="editForm.nickname" @input="onNicknameInput" placeholder="请输入昵称" />
</view>
<view class="edit-field">
<text class="edit-label">性别</text>
<picker class="edit-picker" mode="selector" :range="genderOptions" range-key="label" :value="genderIndex" @change="onGenderChange">
<view class="edit-picker-value">{{ genderOptions[genderIndex].label }}</view>
</picker>
</view>
<view class="edit-field">
<text class="edit-label">生日</text>
<picker class="edit-picker" mode="date" :value="editForm.birthday" :end="today" @change="onBirthdayChange">
<view class="edit-picker-value">{{ editForm.birthday || '请选择生日' }}</view>
</picker>
</view>
</scroll-view>
<view class="edit-modal-footer">
<button class="edit-btn cancel" @click="closeEdit">取消</button>
<button class="edit-btn confirm" :disabled="saving" @click="saveProfile">
{{ saving ? '保存中...' : '保存' }}
</button>
</view>
</view>
</view>
</view>
</template>
<script>
const store = require('../../store/index')
const memberApi = require('../../api/member')
const defaultAvatar = require('../../common/img/20200414210134_qbeyi.jpg')
export default {
data() {
return {
statusBarHeight: 0,
navBarHeight: 44,
totalHeaderHeight: 44,
loading: true,
userInfo: {
nickname: '加载中...', memberNo: '', phone: '', gender: 0,
birthday: '', lastLoginAt: '', avatar: '',
totalSignInDays: 0, monthSignInCount: 0
},
signInRecords: [],
// 编辑弹窗
showEdit: false,
saving: false,
editForm: { nickname: '', gender: 0, birthday: '' },
genderOptions: [
{ label: '男', value: 'MALE' },
{ label: '女', value: 'FEMALE' }
],
today: '',
showDefaultAvatar: false
}
},
onLoad() {
const systemInfo = uni.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 20
let navBarHeight = 44
// #ifdef MP-WEIXIN
const menuButton = uni.getMenuButtonBoundingClientRect()
if (menuButton) {
navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height
}
// #endif
this.statusBarHeight = statusBarHeight
this.navBarHeight = navBarHeight
this.totalHeaderHeight = statusBarHeight + navBarHeight
// 设置今天日期,限制生日选择不晚于今天
const d = new Date()
this.today = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
},
onShow() {
if (!store.isLoggedIn) return
this.loadData()
},
onPullDownRefresh() {
this.loadData().finally(() => { uni.stopPullDownRefresh() })
},
methods: {
async loadData() {
this.loading = true
try {
const [memberResult, signInResult] = await Promise.allSettled([
memberApi.getMemberInfo(),
memberApi.getCheckInRecords({ page: 0, size: 5 })
])
if (memberResult.status === 'fulfilled') {
const info = memberResult.value
this.userInfo = {
nickname: info.nickname || '会员',
memberNo: info.memberNo || ('NM' + String(info.id || '').padStart(6, '0')),
phone: info.phone ? info.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '未绑定',
gender: info.gender != null ? info.gender : 0,
birthday: info.birthday || '',
lastLoginAt: info.lastLoginAt ? info.lastLoginAt.replace('T', ' ').substring(0, 16) : '',
avatar: info.avatar || '',
totalSignInDays: info.totalSignInDays || 0,
monthSignInCount: info.monthSignInCount || 0
}
store.updateMemberInfo(this.userInfo)
}
if (signInResult.status === 'fulfilled') {
const records = Array.isArray(signInResult.value)
? signInResult.value
: (signInResult.value.content || [])
this.signInRecords = records.map(r => ({
date: (r.checkInTime || r.createdAt || '').substring(5, 10) || '--',
time: (r.checkInTime || r.createdAt || '').substring(11, 16) || '--',
typeLabel: r.typeLabel || '签到',
sourceLabel: r.sourceLabel || '小程序',
status: r.status === 'SUCCESS' || r.status === 1 ? 'success' : 'fail',
statusLabel: r.status === 'SUCCESS' || r.status === 1 ? '成功' : '失败'
}))
}
} catch (e) {
console.error('个人中心数据加载失败:', e)
} finally {
this.loading = false
}
},
openEdit() {
const genderMap = { 0: 'UNKNOWN', 1: 'MALE', 2: 'FEMALE' }
const genderVal = typeof this.userInfo.gender === 'string'
? this.userInfo.gender
: (genderMap[this.userInfo.gender] || 'MALE')
this.editForm = {
nickname: this.userInfo.nickname === '加载中...' ? '' : this.userInfo.nickname,
gender: genderVal,
birthday: this.userInfo.birthday || ''
}
this.showEdit = true
},
closeEdit() { this.showEdit = false },
onNicknameInput(e) { this.editForm.nickname = e.detail.value },
onGenderChange(e) {
const idx = Number(e.detail.value)
this.editForm.gender = this.genderOptions[idx].value
},
onBirthdayChange(e) {
this.editForm.birthday = e.detail.value
},
async saveProfile() {
if (this.saving) return
this.saving = true
try {
const body = {
nickname: this.editForm.nickname || undefined,
gender: this.editForm.gender || undefined,
birthday: this.editForm.birthday || undefined
}
const result = await memberApi.updateMemberInfo(body)
if (result) {
this.userInfo.nickname = result.nickname || this.userInfo.nickname
this.userInfo.avatar = result.avatar || this.userInfo.avatar
this.userInfo.gender = result.gender != null ? result.gender : this.userInfo.gender
this.userInfo.birthday = result.birthday || this.userInfo.birthday
store.updateMemberInfo(this.userInfo)
}
uni.showToast({ title: '保存成功', icon: 'success' })
this.showEdit = false
} catch (e) {
console.error('保存资料失败:', e)
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
} finally {
this.saving = false
}
},
onAvatarError() { this.showDefaultAvatar = true },
goToSettings() { uni.showToast({ title: '设置页面', icon: 'none' }) },
viewAllSignIn() { uni.showToast({ title: '全部签到记录', icon: 'none' }) }
},
computed: {
avatarSrc() {
if (this.showDefaultAvatar) return defaultAvatar
return this.userInfo.avatar || defaultAvatar
},
genderLabel() {
const g = this.userInfo.gender
if (g === 1 || g === 'MALE' || g === '男') return '男'
if (g === 2 || g === 'FEMALE' || g === '女') return '女'
return '未设置'
},
genderIndex() {
const genderMap = { 0: 'UNKNOWN', 1: 'MALE', 2: 'FEMALE' }
const genderStr = typeof this.editForm.gender === 'string'
? this.editForm.gender
: genderMap[this.editForm.gender] || 'UNKNOWN'
return Math.max(0, this.genderOptions.findIndex(o => o.value === genderStr))
}
}
}
</script>
<style scoped>
.profile-page { min-height: 100vh; background: #F5F7FA; overflow-x: hidden; }
.header-wrap { background: #1A1A1A; }
.nav-bar { background: #1A1A1A; padding: 0 20px; display: flex; justify-content: space-between; align-items: center; }
.nav-title { font-size: 18px; font-weight: 700; color: #FFFFFF; }
.settings-symbol { font-size: 20px; color: rgba(255,255,255,0.8); }
.content-inner { padding: 16px 16px 0; }
.user-card { background: #FFFFFF; border-radius: 20px; padding: 20px; margin-bottom: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
.user-row { display: flex; align-items: center; margin-bottom: 18px; }
.big-avatar-img { width: 56px; height: 56px; border-radius: 50%; flex-shrink: 0; margin-right: 14px; }
.user-info { flex: 1; display: flex; flex-direction: column; }
.user-name { font-size: 18px; font-weight: 700; color: #1E1E1E; }
.user-tag { font-size: 13px; color: #00C853; font-weight: 500; }
.edit-symbol { font-size: 20px; color: #7A7E84; }
.user-details { background: #F5F7FA; border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; }
.detail-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.detail-label { font-size: 13px; color: #7A7E84; }
.detail-value { font-size: 14px; font-weight: 600; color: #1E1E1E; }
.stats-card { background: #1A1A1A; border-radius: 20px; padding: 18px 20px; display: flex; justify-content: space-around; margin-bottom: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
.stat-item { display: flex; flex-direction: column; align-items: center; }
.stat-number { font-size: 24px; font-weight: 700; color: #00E676; }
.stat-label { font-size: 12px; color: rgba(255,255,255,0.6); margin-top: 4px; }
.stat-divider { width: 1px; background: rgba(255,255,255,0.15); }
.section-title-row { display: flex; align-items: center; margin-bottom: 12px; }
.section-symbol { font-size: 16px; color: #00E676; margin-right: 8px; }
.section-title { font-size: 16px; font-weight: 700; color: #1E1E1E; }
.sign-section { margin-bottom: 16px; }
.sign-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.sign-header .section-title-row { margin-bottom: 0; }
.section-more { font-size: 13px; color: #00C853; }
.sign-item { background: #FFFFFF; border-radius: 12px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
.sign-left { display: flex; flex-direction: column; }
.sign-date { font-size: 14px; font-weight: 600; color: #1E1E1E; }
.sign-time { font-size: 12px; color: #7A7E84; margin-top: 2px; }
.sign-right { display: flex; align-items: center; }
.sign-type { font-size: 12px; color: #7A7E84; margin-right: 6px; }
.sign-source { font-size: 11px; color: #BDBDBD; background: #F5F7FA; padding: 1px 6px; border-radius: 8px; margin-right: 6px; }
.sign-status { font-size: 12px; font-weight: 600; padding: 3px 10px; border-radius: 20px; }
.sign-status.success { background: rgba(0,230,118,0.15); color: #00C853; }
.sign-status.fail { background: rgba(255,82,82,0.15); color: #FF5252; }
.bottom-safe { height: 50px; }
/* 编辑弹窗 */
.edit-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
.edit-modal { width: 85%; max-width: 340px; background: #FFFFFF; border-radius: 20px; overflow: hidden; }
.edit-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 18px 20px 12px; border-bottom: 1px solid #EEEEEE; }
.edit-modal-title { font-size: 17px; font-weight: 700; color: #1E1E1E; }
.edit-modal-close { font-size: 18px; color: #7A7E84; }
.edit-modal-body { padding: 16px 20px; max-height: 60vh; }
.edit-field { display: flex; align-items: center; margin-bottom: 16px; }
.edit-label { width: 52px; font-size: 14px; color: #7A7E84; flex-shrink: 0; }
.edit-input { flex: 1; height: 40px; background: #F5F7FA; border-radius: 10px; padding: 0 12px; font-size: 14px; color: #1E1E1E; }
.edit-picker { flex: 1; }
.edit-picker-value { height: 40px; background: #F5F7FA; border-radius: 10px; padding: 0 12px; font-size: 14px; color: #1E1E1E; display: flex; align-items: center; }
.edit-modal-footer { display: flex; padding: 12px 20px 18px; gap: 12px; border-top: 1px solid #EEEEEE; }
.edit-btn { flex: 1; height: 42px; border-radius: 21px; font-size: 15px; font-weight: 600; display: flex; align-items: center; justify-content: center; border: none; }
.edit-btn::after { border: none; }
.edit-btn.cancel { background: #F5F7FA; color: #7A7E84; }
.edit-btn.confirm { background: #00E676; color: #1A1A1A; }
.edit-btn.confirm[disabled] { opacity: 0.5; }
</style>