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

418 lines
18 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 v-if="signInRecords.length === 0" class="empty-hint">
<text>暂无签到记录</text>
</view>
</view>
<view class="sign-section">
<view class="sign-header">
<view class="section-title-row">
<text class="section-symbol">&#10003;</text>
<text class="section-title">团课签到记录</text>
</view>
<text class="section-more">最近{{ courseCheckInRecords.length }}</text>
</view>
<view v-for="(record, idx) in courseCheckInRecords" :key="'gc' + idx" class="sign-item">
<view class="sign-left">
<text class="sign-date">{{ record.fullDate }}</text>
<text class="sign-time">{{ record.shortTime }}</text>
</view>
<view class="sign-right">
<text class="sign-type">{{ record.courseName }}</text>
<text class="sign-source">{{ record.location }}</text>
<text class="sign-status success">已签到</text>
</view>
</view>
<view v-if="courseCheckInRecords.length === 0" class="empty-hint">
<text>暂无团课签到记录</text>
</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 bookingApi = require('../../api/booking')
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: [],
courseCheckInRecords: [],
// 编辑弹窗
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 memberInfo = store.getMemberInfo()
const memberId = memberInfo ? (memberInfo.memberId || memberInfo.id) : null
const promises = [
memberApi.getMemberInfo(),
memberApi.getCheckInRecords({ page: 0, size: 5 })
]
if (memberId) {
promises.push(bookingApi.getMemberBookings(memberId))
}
const results = await Promise.allSettled(promises)
const [memberResult, signInResult] = results
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 ? '成功' : '失败'
}))
}
// 处理团课签到记录(预约状态为「已出席」的课程)
if (results.length >= 3 && results[2].status === 'fulfilled') {
const bookings = results[2].value
const arr = Array.isArray(bookings) ? bookings : (bookings && bookings.content ? bookings.content : [])
this.courseCheckInRecords = arr
.filter(b => String(b.status) === '2')
.map(b => {
const st = b.courseStartTime || ''
const dt = st ? new Date(st.replace(/ /, 'T')) : null
const y = dt && !isNaN(dt) ? String(dt.getFullYear()) : ''
const mo = dt && !isNaN(dt) ? String(dt.getMonth() + 1).padStart(2, '0') : ''
const d = dt && !isNaN(dt) ? String(dt.getDate()).padStart(2, '0') : ''
const hh = dt && !isNaN(dt) ? String(dt.getHours()).padStart(2, '0') : '--'
const mm = dt && !isNaN(dt) ? String(dt.getMinutes()).padStart(2, '0') : '--'
const inTime = b.checkInTime || b.updatedAt || ''
const indt = inTime ? new Date(inTime.replace(/ /, 'T')) : null
let checkInTimeStr = ''
if (indt && !isNaN(indt)) {
checkInTimeStr = String(indt.getMonth() + 1).padStart(2, '0') + '-' +
String(indt.getDate()).padStart(2, '0') + ' ' +
String(indt.getHours()).padStart(2, '0') + ':' +
String(indt.getMinutes()).padStart(2, '0')
}
return {
courseName: b.courseName || '未命名课程',
location: b.location || '',
fullDate: y + '-' + mo + '-' + d,
shortTime: hh + ':' + mm,
checkInTime: checkInTimeStr || (mo + '-' + d)
}
})
}
} 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; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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; }
.empty-hint { text-align: center; padding: 20px 0; color: #BDBDBD; font-size: 13px; }
.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>