guoyu/fronted_uniapp/pages/profile/profile.vue

587 lines
19 KiB
Vue
Raw Normal View History

2025-12-03 18:58:36 +08:00
<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>