Files
akmon/pages/sport/student/profile.uvue
2026-01-20 08:04:15 +08:00

900 lines
22 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
<scroll-view class="profile-page" direction="vertical">
<!-- 头部背景 -->
<view class="profile-header">
<view class="header-bg"></view>
<!-- 用户信息 -->
<view class="user-info">
<view class="avatar-container">
<image class="avatar" :src="userAvatar" mode="aspectFill" @click="changeAvatar"></image>
<view class="avatar-edit">
<text class="edit-icon"></text>
</view>
</view>
<view class="user-details">
<text class="username">{{ getUserName() }}</text>
<text class="user-id">学号:{{ getUserId() }}</text>
<text class="join-date">加入时间:{{ getJoinDate() }}</text>
</view>
</view>
<!-- 等级信息 -->
<view class="level-info">
<view class="level-badge">
<text class="level-text">Lv.{{ getUserLevel() }}</text>
</view>
<view class="xp-info">
<text class="xp-text">{{ getCurrentXP() }} / {{ getNextLevelXP() }} XP</text>
<view class="xp-bar">
<view class="xp-progress" :style="{ width: getXPProgress() + '%' }"></view>
</view>
</view>
</view>
</view>
<!-- 统计卡片 -->
<view class="stats-grid">
<view class="stat-item">
<text class="stat-number">{{ totalTrainingDays }}</text>
<text class="stat-label">训练天数</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ completedAssignments }}</text>
<text class="stat-label">完成作业</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ totalTrainingTime }}</text>
<text class="stat-label">训练时长(小时)</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ achievementCount }}</text>
<text class="stat-label">获得成就</text>
</view>
</view>
<!-- 功能菜单 */
<view class="menu-section">
<view class="section-title">个人设置</view>
<view class="menu-list">
<view class="menu-item" @click="editProfile">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">编辑资料</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="changePassword">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">修改密码</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="notificationSettings">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">通知设置</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="privacySettings">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">隐私设置</text>
</view>
<text class="menu-arrow">></text>
</view>
</view>
</view>
<!-- 训练偏好 -->
<view class="menu-section">
<view class="section-title">训练偏好</view>
<view class="menu-list">
<view class="menu-item" @click="goalSettings">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">训练目标</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="reminderSettings">
<view class="menu-left">
<text class="menu-icon">⏰</text>
<text class="menu-title">训练提醒</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="favoriteExercises">
<view class="menu-left">
<text class="menu-icon">❤️</text>
<text class="menu-title">喜欢的运动</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="preferencesAnalytics">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">偏好分析</text>
</view>
<text class="menu-arrow">></text>
</view>
</view>
</view>
<!-- 数据管理 -->
<view class="menu-section">
<view class="section-title">数据管理</view>
<view class="menu-list">
<view class="menu-item" @click="deviceManagement">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">设备管理</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="exportData">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">导出数据</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="backupData">
<view class="menu-left">
<text class="menu-icon">☁️</text>
<text class="menu-title">数据备份</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="clearCache">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">清除缓存</text>
</view>
<text class="menu-arrow">></text>
</view>
</view>
</view>
<!-- 关于 -->
<view class="menu-section">
<view class="section-title">关于</view>
<view class="menu-list">
<view class="menu-item" @click="helpCenter">
<view class="menu-left">
<text class="menu-icon">❓</text>
<text class="menu-title">帮助中心</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="feedback">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">意见反馈</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="aboutApp">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">关于应用</text>
</view>
<text class="menu-arrow">></text>
</view>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-section">
<button class="logout-btn" @click="logout">退出登录</button>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed, onUnmounted } from 'vue'
import { onLoad, onResize } from '@dcloudio/uni-app'
import { formatDateTime } from '../types.uts'
import supaClient from '@/components/supadb/aksupainstance.uts'
import { state, getCurrentUserId, setUserProfile } from '@/utils/store.uts'
import type { UserProfile } from '@/pages/user/types.uts'
const userId = ref('')
// Responsive state - using onResize for dynamic updates
const screenWidth = ref<number>(uni.getSystemInfoSync().windowWidth)
// Computed properties for responsive design
const isLargeScreen = computed(() : boolean => {
return screenWidth.value >= 768
})
// 响应式数据
const userInfo = ref<UTSJSONObject | null>(null)
const userAvatar = ref<string>('/static/default-avatar.png')
const totalTrainingDays = ref<number>(0)
const completedAssignments = ref<number>(0)
const totalTrainingTime = ref<number>(0)
const achievementCount = ref<number>(0)
const getUserName = () : string => {
const profile = state.userProfile
if (profile != null) {
const username = profile.username
if (typeof username === 'string' && username.length > 0) {
return username
}
}
if (userInfo.value == null) {
return '未登录'
}
return (userInfo.value as UTSJSONObject).getString('username') ?? '未设置'
}
const getUserId = () : string => {
if (typeof userId.value === 'string' && userId.value.length > 0) {
return userId.value
}
if (userInfo.value == null) {
return '---'
}
return (userInfo.value as UTSJSONObject).getString('id') ?? '未设置'
}
const getJoinDate = () : string => {
if (userInfo.value == null) return '---'
const joinDate = (userInfo.value as UTSJSONObject).getString('created_at') ?? ''
return joinDate.length > 0 ? formatDateTime(joinDate).split(' ')[0] : '未知'
}
const getUserLevel = () : number => {
if (userInfo.value == null) return 1
return (userInfo.value as UTSJSONObject).getNumber('level') ?? 1
}
const getCurrentXP = () : number => {
if (userInfo.value == null) return 0
return (userInfo.value as UTSJSONObject).getNumber('current_xp') ?? 0
}
const getNextLevelXP = () : number => {
const level = getUserLevel()
return level * 1000 // 每级需要1000XP
}
const getXPProgress = () : number => {
const current = getCurrentXP()
const next = getNextLevelXP()
const levelBase = (getUserLevel() - 1) * 1000
const levelCurrent = current - levelBase
const levelRequired = next - levelBase
return levelRequired > 0 ? (levelCurrent / levelRequired) * 100 : 0
} // 加载用户资料 - 优先使用 state.userProfile然后同步数据库
// 加载用户统计数据
const loadUserStats = async () => {
try { // 获取训练记录统计
const recordsResult = await supaClient
.from('ak_training_records')
.select('*', {})
.eq('user_id', userId.value)
.execute()
if (recordsResult != null && recordsResult.error == null && recordsResult.status == 200) {
console.log(recordsResult)
const records = recordsResult.data as UTSJSONObject[]
// 计算训练天数(去重日期)
const trainingDates = new Set<string>()
let totalMinutes = 0
records.forEach(record => {
const createdAt = (record as UTSJSONObject).getString('created_at') ?? ''
if (createdAt.length > 0) {
const date = new Date(createdAt).toDateString()
trainingDates.add(date)
}
const duration = (record as UTSJSONObject).getNumber('duration') ?? 0
totalMinutes += duration
})
totalTrainingDays.value = trainingDates.size
totalTrainingTime.value = Math.round(totalMinutes / 60)
}
else
{
console.log(recordsResult)
}
// 获取完成的作业数量
const assignmentsResult = await supaClient
.from('ak_assignments')
.select('*', {})
.eq('status', 'completed')
.execute()
if (assignmentsResult != null && assignmentsResult.error == null) {
const data = assignmentsResult.data as UTSJSONObject[]
completedAssignments.value = data.length
}
// 获取成就数量 - 使用高分作业作为成就代理
const achievementsResult = await supaClient
.from('ak_assignment_submissions')
.select('*', {})
.eq('student_id', userId.value)
.gte('final_score', 90) // 90分以上的作业算作成就
.execute()
if (achievementsResult != null && achievementsResult.error == null) {
const data = achievementsResult.data as UTSJSONObject[]
achievementCount.value = data.length
}
} catch (error) {
console.log(userId.value)
console.error('加载用户统计失败:', error)
// 使用模拟数据
totalTrainingDays.value = 45
completedAssignments.value = 28
totalTrainingTime.value = 67
achievementCount.value = 12
}
} // 更换头像 - 同时更新 state 和数据库
const changeAvatar = () => {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: function (res) {
const tempFilePath = res.tempFilePaths[0]
// 这里应该上传头像到服务器
userAvatar.value = tempFilePath // 更新 state.userProfile 中的头像 - 使用本地变量避免smart cast问题
const currentProfile = state.userProfile
if (currentProfile != null && typeof currentProfile.id === 'string' && (currentProfile.id?.length ?? 0) > 0) {
const updatedProfile : UserProfile = {
id: currentProfile.id,
username: currentProfile.username,
email: currentProfile.email,
gender: currentProfile.gender,
birthday: currentProfile.birthday,
height_cm: currentProfile.height_cm,
weight_kg: currentProfile.weight_kg,
bio: currentProfile.bio,
avatar_url: tempFilePath,
preferred_language: currentProfile.preferred_language,
role: currentProfile.role
}
setUserProfile(updatedProfile)
}// 更新数据库如果用户ID有效
const currentUserId = getCurrentUserId()
if (currentUserId.length > 0 && currentUserId !== 'demo_user_id') {
// 使用异步调用但不等待结果
supaClient
.from('ak_users')
.update({ avatar_url: tempFilePath })
.eq('id', currentUserId)
.execute()
.then(() => {
console.log('头像更新成功')
})
.catch((error) => {
console.error('更新头像失败:', error)
})
}
uni.showToast({
title: '头像更新成功',
icon: 'success'
})
}
})
}
// 编辑资料
const editProfile = () => {
uni.showModal({
title: '编辑资料',
content: '功能开发中...',
showCancel: false
})
}
// 修改密码
const changePassword = () => {
uni.showModal({
title: '修改密码',
content: '功能开发中...',
showCancel: false
})
}
// 通知设置
const notificationSettings = () => {
uni.showModal({
title: '通知设置',
content: '功能开发中...',
showCancel: false
})
}
// 隐私设置
const privacySettings = () => {
uni.showModal({
title: '隐私设置',
content: '功能开发中...',
showCancel: false
})
}
// 目标设置
const goalSettings = () => {
uni.navigateTo({
url: '/pages/sport/student/goal-settings'
})
}
// 提醒设置
const reminderSettings = () => {
uni.navigateTo({
url: '/pages/sport/student/reminder-settings'
})
}
// 喜欢的运动
const favoriteExercises = () => {
uni.navigateTo({
url: '/pages/sport/student/favorite-exercises'
})
}
// 偏好分析
const preferencesAnalytics = () => {
uni.navigateTo({
url: '/pages/sport/student/preferences-analytics'
})
}
// 设备管理
const deviceManagement = () => {
uni.navigateTo({
url: '/pages/sport/student/device-management'
})
}
// 导出数据
const exportData = () => {
uni.showLoading({
title: '导出中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '导出完成',
icon: 'success'
})
}, 2000)
}
// 备份数据
const backupData = () => {
uni.showLoading({
title: '备份中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '备份完成',
icon: 'success'
})
}, 1500)
}
// 清除缓存
const clearCache = () => {
uni.showModal({
title: '清除缓存',
content: '确定要清除所有缓存数据吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '清除中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '缓存已清除',
icon: 'success'
})
}, 1000)
}
}
})
}
// 帮助中心
const helpCenter = () => {
uni.showModal({
title: '帮助中心',
content: '功能开发中...',
showCancel: false
})
}
// 意见反馈
const feedback = () => {
uni.showModal({
title: '意见反馈',
content: '功能开发中...',
showCancel: false
})
}
// 关于应用
const aboutApp = () => {
uni.showModal({
title: '关于应用',
content: 'AI监测系统 v1.0.0\n\n 高性能AI监测管理平台',
showCancel: false
})
}
const loadUserProfile = async () => {
try {
// 如果 state.userProfile 存在,先使用它填充界面
const profile = state.userProfile
if (profile != null && typeof profile.id === 'string' && (profile.id?.length ?? 0) > 0) {
userInfo.value = {
id: profile.id,
username: profile.username != null ? profile.username : '',
email: profile.email != null ? profile.email : '',
avatar_url: profile.avatar_url != null ? profile.avatar_url : '',
created_at: new Date().toISOString() // 临时值
} as UTSJSONObject
const avatarUrl = profile.avatar_url
if (typeof avatarUrl === 'string' && (avatarUrl?.length ?? 0) > 0) {
userAvatar.value = avatarUrl as string
}
}
// 然后从数据库获取最新数据并更新 state
if (typeof userId.value === 'string' && userId.value.length > 0 && userId.value !== 'demo_user_id') {
const result = await supaClient
.from('ak_users')
.select('*', {})
.eq('id', userId.value)
.single()
.execute()
if (result != null && result.error == null && result.data != null) {
const res_data = result.data as UTSJSONObject[]
const resultData = res_data[0]
userInfo.value = resultData
const avatar = (userInfo.value as UTSJSONObject).getString('avatar_url') ?? ''
if (avatar.length > 0) {
userAvatar.value = avatar
}
// 更新 state.userProfile
const updatedProfile : UserProfile = {
id: resultData.getString('id') ?? '',
username: resultData.getString('username') ?? '',
email: resultData.getString('email') ?? '',
gender: resultData.getString('gender'),
birthday: resultData.getString('birthday'),
height_cm: resultData.getNumber('height_cm'),
weight_kg: resultData.getNumber('weight_kg'),
bio: resultData.getString('bio'),
avatar_url: resultData.getString('avatar_url'),
preferred_language: resultData.getString('preferred_language'),
role: resultData.getString('role')
}
setUserProfile(updatedProfile)
}
}
loadUserStats()
} catch (error) {
console.error('加载用户资料失败:', error)
}
}
// 退出登录 - 清空 state 和重定向
const logout = () => {
uni.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
success: function (res) {
if (res.confirm) {
// 使用 Promise 包装异步操作
supaClient.signOut()
.then(() => {
// 清空 state
const emptyProfile : UserProfile = { username: '', email: '' }
setUserProfile(emptyProfile)
uni.reLaunch({
url: '/pages/index/index'
})
})
.catch((error) => {
console.error('退出登录失败:', error)
uni.showToast({
title: '退出失败',
icon: 'none'
})
})
}
}
})
}
// 生命周期
onLoad((options : OnLoadOptions) => {
userId.value = options['id'] ?? ''
if (userId.value.length === 0) {
userId.value = getCurrentUserId()
}
loadUserProfile()
})
onMounted(() => {
// Initialize screen width
screenWidth.value = uni.getSystemInfoSync().windowWidth
})
// Handle resize events for responsive design
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
</script>
<style>
.profile-page {
flex: 1;
background: #f5f6fa;
}
/* 头部区域 */
.profile-header {
position: relative;
padding: 60rpx 40rpx 40rpx;
margin-bottom: 20rpx;
}
.header-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: linear-gradient(to right, #667eea, #764ba2);
border-radius: 0 0 40rpx 40rpx;
}
.user-info {
position: relative;
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.avatar-container {
position: relative;
margin-right: 30rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
}
.avatar-edit {
position: absolute;
bottom: 0;
right: 0;
width: 40rpx;
height: 40rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.edit-icon {
font-size: 20rpx;
}
.user-details {
flex: 1;
}
.username {
font-size: 36rpx;
font-weight: bold;
color: white;
margin-bottom: 8rpx;
}
.user-id,
.join-date {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 4rpx;
}
.level-info {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
}
.level-badge {
background: rgba(255, 255, 255, 0.2);
padding: 10rpx 20rpx;
border-radius: 25rpx;
backdrop-filter: blur(10rpx);
margin-left: 20rpx;
}
.level-text {
color: white;
font-size: 24rpx;
font-weight: bold;
}
.xp-info {
flex: 1;
}
.xp-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 8rpx;
}
.xp-bar {
height: 8rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 4rpx;
overflow: hidden;
}
.xp-progress {
height: 100%;
background: white;
transition: width 0.3s ease;
}
/* 统计网格 */
.stats-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 20rpx 30rpx;
}
.stat-item {
background: white;
border-radius: 20rpx;
padding: 40rpx 30rpx;
text-align: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
margin-right: 20rpx;
margin-bottom: 20rpx;
width: 46%;
flex: 0 0 46%;
}
.stat-number {
font-size: 48rpx;
font-weight: bold;
color: #2c3e50;
line-height: 1;
margin-bottom: 10rpx;
}
.stat-label {
font-size: 24rpx;
color: #7f8c8d;
}
/* 菜单区域 */
.menu-section {
margin: 0 20rpx 30rpx;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #2c3e50;
margin-bottom: 15rpx;
padding-left: 10rpx;
}
.menu-list {
background: white;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.menu-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1px solid #f8f9fa;
transition: background-color 0.2s;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background-color: #f8f9fa;
}
.menu-left {
display: flex;
flex-direction: row;
align-items: center;
}
.menu-icon {
font-size: 32rpx;
width: 40rpx;
text-align: center;
margin-right: 20rpx;
}
.menu-title {
font-size: 30rpx;
color: #2c3e50;
}
.menu-arrow {
font-size: 28rpx;
color: #bdc3c7;
}
/* 退出登录 */
.logout-section {
margin: 40rpx 20rpx;
}
.logout-btn {
width: 100%;
padding: 30rpx;
background: #e74c3c;
color: white;
border: none;
border-radius: 20rpx;
font-size: 32rpx;
font-weight: bold;
transition: transform 0.3s ease, background-color 0.3s ease;
}
.logout-btn:active {
transform: scale(0.98);
background: #c0392b;
}
</style>