guoyu/fronted_uniapp/pages/profile/profile.vue
2025-12-03 18:58:36 +08:00

587 lines
19 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-container">
<!-- 顶部用户信息区域 -->
<view class="profile-header">
<view class="header-bg"></view>
<view class="user-info-section">
<view class="avatar-wrapper">
<view class="avatar">
<text class="avatar-text">{{ avatarText }}</text>
</view>
</view>
<view class="user-details">
<text class="username">{{ userInfo.realName || userInfo.username }}</text>
<view class="user-meta">
<text class="role-tag">{{ roleText }}</text>
<text class="user-id" v-if="userInfo.username">ID: {{ userInfo.username }}</text>
</view>
</view>
</view>
</view>
<!-- 学习记录统计卡片仅学生端 -->
<view class="learning-stats-section" v-if="isStudent">
<view class="stats-card">
<view class="stat-item">
<text class="stat-value">{{ todayStudyText }}</text>
<text class="stat-label">今日学习</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ weekStudyText }}</text>
<text class="stat-label">本周学习</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ totalDaysText }}</text>
<text class="stat-label">累计学习</text>
</view>
</view>
</view>
<!-- 功能菜单区域 -->
<view class="menu-sections">
<!-- 学生端学习相关 -->
<view class="menu-section" v-if="isStudent">
<view class="section-title">学习中心</view>
<view class="menu-card">
<view class="menu-item" @click="goToExamList">
<view class="menu-icon-wrapper exam">
<text class="menu-icon">📝</text>
</view>
<view class="menu-content">
<text class="menu-text">我的考试</text>
<text class="menu-desc">查看考试列表</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="goToScoreList">
<view class="menu-icon-wrapper score">
<text class="menu-icon">📊</text>
</view>
<view class="menu-content">
<text class="menu-text">我的成绩</text>
<text class="menu-desc">查看成绩详情</text>
</view>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<!-- 其他功能 -->
<view class="menu-section">
<view class="section-title">其他</view>
<view class="menu-card">
<view class="menu-item" @click="handleLogout">
<view class="menu-icon-wrapper logout">
<text class="menu-icon">🚪</text>
</view>
<view class="menu-content">
<text class="menu-text">退出登录</text>
<text class="menu-desc">安全退出当前账号</text>
</view>
<text class="menu-arrow"></text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { getMyRecords, getMyStatistics } from '@/api/study/learningRecord.js'
import { getMyCourses } from '@/api/study/course.js'
export default {
computed: {
...mapGetters('auth', ['userInfo', 'userRole']),
isStudent() {
return this.userRole === 'student'
},
roleText() {
const roleMap = {
'admin': '管理员',
'student': '学员'
}
return roleMap[this.userInfo.role] || '学员'
},
avatarText() {
const name = this.userInfo.realName || this.userInfo.username || '用'
return name.length > 0 ? name.charAt(0).toUpperCase() : '用'
},
totalDurationText() {
return this.formatDuration(this.totalDuration)
},
todayStudyText() {
// 今日学习时长(分钟)
const minutes = Math.floor(this.todayDuration / 60)
return minutes > 0 ? `${minutes} min` : '0 min'
},
weekStudyText() {
// 本周学习时长(小时)
const hours = Math.floor(this.weekDuration / 3600)
return hours > 0 ? `${hours} h` : '0 h'
},
totalDaysText() {
// 累计学习天数
return `${this.totalDays} days`
},
},
data() {
return {
totalDuration: 0,
totalCount: 0,
completedCourses: 0,
todayDuration: 0, // 今日学习时长(秒)
weekDuration: 0, // 本周学习时长(秒)
totalDays: 0 // 累计学习天数
}
},
onLoad() {
// 如果是学生端,加载学习记录统计
if (this.isStudent) {
this.loadLearningStats()
}
},
onShow() {
// 通知底部导航栏更新
uni.$emit('tabbar-update')
// 如果是学生端,刷新学习记录统计
if (this.isStudent) {
this.loadLearningStats()
}
},
methods: {
...mapActions('auth', ['logout']),
async loadLearningStats() {
try {
// 获取学习统计信息(从后台接口)
const statsResponse = await getMyStatistics()
if (statsResponse.code === 200) {
const stats = statsResponse.data || {}
// 设置统计数据
this.todayDuration = stats.todayDuration || 0 // 今日学习时长(秒)
this.weekDuration = stats.weekDuration || 0 // 本周学习时长(秒)
this.totalDays = stats.totalDays || 0 // 累计学习天数
}
} catch (error) {
console.error('加载学习统计失败:', error)
// 如果接口调用失败,设置默认值
this.todayDuration = 0
this.weekDuration = 0
this.totalDays = 0
}
},
formatDuration(seconds) {
if (!seconds) return '0分钟'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) {
return `${hours}小时${minutes}分钟`
}
return `${minutes}分钟`
},
goToExamList() {
uni.navigateTo({
url: '/pages/exam/list'
})
},
goToScoreList() {
uni.navigateTo({
url: '/pages/score/list'
})
},
async handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
confirmColor: 'rgb(55 140 224)',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '退出中...' })
try {
await this.logout()
uni.hideLoading()
uni.reLaunch({
url: '/pages/login/login'
})
} catch (error) {
uni.hideLoading()
uni.showToast({
title: '退出失败',
icon: 'none'
})
}
}
}
})
}
}
}
</script>
<style lang="scss" scoped>
.profile-container {
width: 100%;
min-height: 100vh;
background: #f5f7fa;
padding-bottom: 120rpx; // 为底部导航栏留出空间
box-sizing: border-box;
@media (min-width: 768px) {
padding-bottom: 140rpx;
}
}
// 顶部用户信息区域(钉钉风格)
.profile-header {
position: relative;
background: linear-gradient(135deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
padding: 60rpx 30rpx 50rpx;
padding-top: calc(var(--status-bar-height) + 60rpx);
overflow: hidden;
@media (min-width: 768px) {
padding: 80rpx 60rpx 70rpx;
padding-top: calc(var(--status-bar-height) + 80rpx);
}
.header-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="20" cy="20" r="2" fill="rgba(255,255,255,0.1)"/><circle cx="80" cy="40" r="1.5" fill="rgba(255,255,255,0.1)"/><circle cx="40" cy="70" r="1" fill="rgba(255,255,255,0.1)"/></svg>');
opacity: 0.3;
}
.user-info-section {
position: relative;
display: flex;
align-items: center;
z-index: 1;
.avatar-wrapper {
margin-right: 24rpx;
@media (min-width: 768px) {
margin-right: 40rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10rpx);
border: 4rpx solid rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
@media (min-width: 768px) {
width: 160rpx;
height: 160rpx;
border: 6rpx solid rgba(255, 255, 255, 0.3);
}
.avatar-text {
font-size: 48rpx;
font-weight: bold;
color: #fff;
@media (min-width: 768px) {
font-size: 64rpx;
}
}
}
}
.user-details {
flex: 1;
.username {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #fff;
margin-bottom: 16rpx;
@media (min-width: 768px) {
font-size: 48rpx;
margin-bottom: 24rpx;
}
}
.user-meta {
display: flex;
align-items: center;
gap: 16rpx;
@media (min-width: 768px) {
gap: 24rpx;
}
.role-tag {
display: inline-block;
padding: 6rpx 16rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
font-size: 22rpx;
color: #fff;
backdrop-filter: blur(10rpx);
@media (min-width: 768px) {
padding: 8rpx 20rpx;
font-size: 26rpx;
border-radius: 24rpx;
}
}
.user-id {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
@media (min-width: 768px) {
font-size: 28rpx;
}
}
}
}
}
}
// 学习记录统计卡片
.learning-stats-section {
margin: 20rpx 30rpx 30rpx;
@media (min-width: 768px) {
margin: 30rpx 40rpx 40rpx;
}
.stats-card {
background: #fff;
border-radius: 20rpx;
padding: 40rpx 30rpx;
display: flex;
justify-content: space-between;
gap: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
@media (min-width: 768px) {
border-radius: 24rpx;
padding: 50rpx 40rpx;
gap: 30rpx;
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20rpx 10rpx;
text-align: center;
background: transparent;
@media (min-width: 768px) {
padding: 24rpx 16rpx;
}
.stat-value {
font-size: 40rpx;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 12rpx;
line-height: 1.2;
@media (min-width: 768px) {
font-size: 48rpx;
margin-bottom: 16rpx;
}
}
.stat-label {
font-size: 24rpx;
color: #999;
font-weight: 400;
@media (min-width: 768px) {
font-size: 28rpx;
}
}
}
}
}
// 功能菜单区域
.menu-sections {
margin: 0 30rpx 30rpx;
@media (min-width: 768px) {
margin: 0 40rpx 40rpx;
}
.menu-section {
margin-bottom: 24rpx;
@media (min-width: 768px) {
margin-bottom: 32rpx;
}
.section-title {
font-size: 26rpx;
color: #999;
margin-bottom: 16rpx;
padding: 0 4rpx;
font-weight: 500;
@media (min-width: 768px) {
font-size: 28rpx;
margin-bottom: 20rpx;
padding: 0 8rpx;
}
}
.menu-card {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
@media (min-width: 768px) {
border-radius: 24rpx;
}
.menu-item {
display: flex;
align-items: center;
padding: 28rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
transition: background-color 0.25s ease, transform 0.2s ease;
will-change: transform;
position: relative;
@media (min-width: 768px) {
padding: 36rpx 40rpx;
}
&:active {
background-color: #f8f9fa;
transform: translate3d(4rpx, 0, 0);
}
&:last-child {
border-bottom: none;
}
.menu-icon-wrapper {
width: 72rpx;
height: 72rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
transition: transform 0.3s ease;
@media (min-width: 768px) {
width: 88rpx;
height: 88rpx;
border-radius: 20rpx;
margin-right: 32rpx;
}
&.record {
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
}
&.exam {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
}
&.score {
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%);
}
&.voice {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
}
&.logout {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
}
.menu-icon {
font-size: 36rpx;
@media (min-width: 768px) {
font-size: 44rpx;
}
}
}
.menu-content {
flex: 1;
display: flex;
flex-direction: column;
.menu-text {
font-size: 30rpx;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 6rpx;
transition: color 0.3s;
@media (min-width: 768px) {
font-size: 32rpx;
margin-bottom: 8rpx;
}
}
.menu-desc {
font-size: 24rpx;
color: #999;
@media (min-width: 768px) {
font-size: 26rpx;
}
}
}
.menu-arrow {
font-size: 32rpx;
color: #d9d9d9;
margin-left: 16rpx;
flex-shrink: 0;
transition: transform 0.3s ease, color 0.3s;
@media (min-width: 768px) {
font-size: 36rpx;
margin-left: 24rpx;
}
}
&:active {
.menu-icon-wrapper {
transform: scale(0.95);
}
.menu-text {
color: rgb(55 140 224);
}
.menu-arrow {
transform: translateX(4rpx);
color: rgb(55 140 224);
}
}
}
}
}
}
</style>