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

637 lines
16 KiB
Plaintext

<template>
<scroll-view direction="vertical" class="progress-container"> <!-- Header -->
<view class="header">
<view class="header-left">
<button v-if="fromStats" @click="goBack" class="back-btn">
<simple-icon type="arrow-left" :size="16" color="#FFFFFF" />
<text>返回</text>
</button>
<text class="title">{{ fromStats ? (studentName!='' ? `${studentName} - 学习进度` : '学生学习进度') : '学习进度' }}</text>
</view>
<view class="header-actions">
<button @click="refreshData" class="refresh-btn">
<simple-icon type="refresh" :size="16" color="#FFFFFF" />
<text>刷新</text>
</button>
</view>
</view>
<!-- Loading State -->
<view v-if="loading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<!-- Content -->
<scroll-view v-else class="content" direction="vertical" :style="{ height: contentHeight + 'px' }">
<!-- Overall Progress -->
<view class="progress-overview">
<text class="section-title">总体进度</text>
<view class="overall-stats">
<view class="stat-card">
<text class="stat-value">{{ overallProgress }}%</text>
<text class="stat-label">完成率</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ completedAssignments }}</text>
<text class="stat-label">已完成作业</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ totalTrainingTime }}</text>
<text class="stat-label">训练时长(分钟)</text>
</view>
</view>
</view> <!-- Progress by Subject -->
<view class="progress-by-subject">
<text class="section-title">各科目进度</text>
<view v-for="subject in subjectProgress" :key="subject.getString('id')" class="subject-item">
<view class="subject-header">
<text class="subject-name">{{ subject.getString('name') }}</text>
<text class="subject-percentage">{{ subject.getNumber('progress') }}%</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: (subject.getNumber('progress') ?? 0) + '%' }"></view>
</view>
<view class="subject-details">
<text class="detail-text">已完成:
{{ subject.getNumber('completed') }}/{{ subject.getNumber('total') }}</text>
<text class="detail-text">最近训练: {{ formatDate(subject.getString('lastTraining') ?? '') }}</text>
</view>
</view>
</view> <!-- Recent Activities -->
<view class="recent-activities">
<text class="section-title">最近活动</text>
<view v-for="activity in recentActivities" :key="activity.getString('id')" class="activity-item">
<view class="activity-icon">
<simple-icon :type="getActivityIcon(activity.getString('type') ?? 'info')" :size="20"
color="#4CAF50" />
</view>
<view class="activity-content">
<text class="activity-title">{{ activity.getString('title') }}</text>
<text class="activity-desc">{{ activity.getString('description') }}</text>
<text class="activity-time">{{ formatDateTime(activity.getString('time') ?? '') }}</text>
</view>
</view>
</view> <!-- Weekly Goals -->
<view class="weekly-goals">
<text class="section-title">本周目标</text>
<view v-for="goal in weeklyGoals" :key="goal.getString('id')" class="goal-item">
<view class="goal-header">
<text class="goal-name">{{ goal.getString('name') }}</text>
<text class="goal-status" :class="{ 'completed': isGoalCompleted(goal) }">
{{ isGoalCompleted(goal) ? '已完成' : '进行中' }}
</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: (goal.getNumber('progress') ?? 0) + '%' }"></view>
</view>
<text class="goal-desc">{{ goal.getString('description') }}</text>
</view>
</view>
</scroll-view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { onLoad, onResize } from '@dcloudio/uni-app'
import { state, getCurrentUserId } from '@/utils/store.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// 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 loading = ref(true)
const overallProgress = ref(0)
const completedAssignments = ref(0)
const totalTrainingTime = ref(0)
const subjectProgress = ref<UTSJSONObject[]>([])
const recentActivities = ref<UTSJSONObject[]>([])
const weeklyGoals = ref<UTSJSONObject[]>([])
const contentHeight = ref(0)
// Page parameters
const userId = ref('')
const studentName = ref('')
const fromStats = ref(false)// 生命周期
// Forward declaration for loadProgressData
let loadProgressData : () => Promise<void> = async () => {
}
// Handle page load with parameters
onLoad((options : OnLoadOptions) => {
// Get student ID from page parameters if provided
userId.value = options['id'] ?? ''
if (userId.value.length > 0) {
fromStats.value = true
}
studentName.value = decodeURIComponent(options['studentName'] ?? '') ?? ''
// Use setTimeout to avoid async function type mismatch
setTimeout(() => {
loadProgressData().then((res) => {
// Data loaded successfully
console.log(res)
})
}, 100)
})
// 计算内容高度
const calculateContentHeight = () => {
const systemInfo = uni.getSystemInfoSync()
const windowHeight = systemInfo.windowHeight
const headerHeight = 60
contentHeight.value = windowHeight - headerHeight
} // 加载进度数据
// 加载总体进度
const loadOverallProgress = async (studentId : string) => {
try {
// 获取作业完成情况
const assignmentsResult = await supa
.from('ak_student_assignments')
.select('status', {})
.eq('student_id', studentId)
.execute()
if (assignmentsResult.data != null) {
const assignments = assignmentsResult.data as UTSJSONObject[]
const completed = assignments.filter(a => ((a as UTSJSONObject).getString('status') ?? 'pending') === 'completed').length
completedAssignments.value = completed
overallProgress.value = assignments.length > 0 ? Math.round((completed / assignments.length) * 100) : 0
}// 获取训练时长
const trainingResult = await supa
.from('ak_training_records')
.select('duration', {})
.eq('student_id', studentId)
.execute()
if (trainingResult.data != null) {
const records = trainingResult.data as UTSJSONObject[]
totalTrainingTime.value = records.reduce((total, record) => {
return total + ((record as UTSJSONObject).getNumber('duration') ?? 0)
}, 0)
}
} catch (error) {
console.error('加载总体进度失败:', error)
}
}
// 加载各科目进度
const loadSubjectProgress = async (studentId : string) => {
try {
const result = await supa
.from('ak_training_projects')
.select(`
id, name,
training_records:ak_training_records(status),
assignments:ak_student_assignments(status)
`, {})
.execute()
if (result.data != null) {
const projects = result.data as UTSJSONObject[]
subjectProgress.value = projects.map(project => {
const records = (project as UTSJSONObject).getArray('training_records') as UTSJSONObject[] ?? []
const assignments = (project as UTSJSONObject).getArray('assignments') as UTSJSONObject[] ?? []
const completedRecords = records.filter(r => ((r as UTSJSONObject).getString('status') ?? 'pending') === 'completed').length
const completedAssignments = assignments.filter(a => ((a as UTSJSONObject).getString('status') ?? 'pending') === 'completed').length
const total = records.length + assignments.length
const completed = completedRecords + completedAssignments
const progressData = new UTSJSONObject()
progressData.set('id', (project as UTSJSONObject).getString('id') ?? '')
progressData.set('name', (project as UTSJSONObject).getString('name') ?? '')
progressData.set('progress', total > 0 ? Math.round((completed / total) * 100) : 0)
progressData.set('completed', completed)
progressData.set('total', total)
progressData.set('lastTraining', new Date().toISOString())
return progressData
})
}
} catch (error) {
console.error('加载科目进度失败:', error)
}
}
// 加载最近活动
const loadRecentActivities = async (studentId : string) => {
try {
const result = await supa
.from('ak_training_records')
.select('*, ak_training_projects(name)', {})
.eq('student_id', studentId)
.order('created_at', { ascending: false })
.limit(10)
.execute()
if (result.data != null) {
const records = result.data as UTSJSONObject[]
recentActivities.value = records.map(record => {
const activityData = new UTSJSONObject()
activityData.set('id', (record as UTSJSONObject).getString('id') ?? '')
activityData.set('type', 'training')
activityData.set('title', `完成训练: ${((record as UTSJSONObject).getAny('ak_training_projects') as UTSJSONObject ?? new UTSJSONObject()).getString('name') ?? '未知项目'}`)
activityData.set('description', `耗时: ${(record as UTSJSONObject).getNumber('duration') ?? 0}分钟`)
activityData.set('time', (record as UTSJSONObject).getString('created_at') ?? '')
return activityData
})
}
} catch (error) {
console.error('加载最近活动失败:', error)
}
}
// 加载周目标
const loadWeeklyGoals = async (studentId : string) => {
// 创建示例周目标
const goal1 = new UTSJSONObject()
goal1.set('id', '1')
goal1.set('name', '完成3次训练')
goal1.set('description', '本周完成至少3次训练课程')
goal1.set('progress', 67)
goal1.set('completed', false)
const goal2 = new UTSJSONObject()
goal2.set('id', '2')
goal2.set('name', '提交2份作业')
goal2.set('description', '按时提交本周的训练作业')
goal2.set('progress', 100)
goal2.set('completed', true)
const goal3 = new UTSJSONObject()
goal3.set('id', '3')
goal3.set('name', '训练时长达到120分钟')
goal3.set('description', '累计训练时长达到目标')
goal3.set('progress', 45)
goal3.set('completed', false)
weeklyGoals.value = [goal1, goal2, goal3]
} // 刷新数据
const refreshData = () => {
loadProgressData().then(() => {
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}).catch((error) => {
console.error('Refresh failed:', error)
})
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
// 格式化日期
const formatDate = (dateString : string) : string => {
try {
const date = new Date(dateString)
return `${date.getMonth() + 1}月${date.getDate()}日`
} catch {
return '暂无'
}
}
// 格式化日期时间
const formatDateTime = (dateString : string) : string => {
try {
const date = new Date(dateString)
return `${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
} catch {
return '暂无'
}
}
loadProgressData = async () => {
try {
loading.value = true
if (userId.value.length === 0) {
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none',
duration: 2000
})
setTimeout(() => {
uni.redirectTo({ url: '/pages/user/login' })
}, 1000)
return
}
// 序列化加载各种数据以避免Promise.all兼容性问题
await loadOverallProgress(userId.value)
await loadSubjectProgress(userId.value)
await loadRecentActivities(userId.value)
await loadWeeklyGoals(userId.value)
} catch (error) {
console.error('加载进度数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 获取活动图标
const getActivityIcon = (type : string) : string => {
switch (type) {
case 'training': return 'play'
case 'assignment': return 'edit'
case 'achievement': return 'trophy'
default: return 'info'
}
}
// 安全解包目标完成状态
const isGoalCompleted = (goal: UTSJSONObject): boolean => {
return goal.getBoolean('completed') === true
}
onMounted(() => {
screenWidth.value = uni.getSystemInfoSync().windowWidth
calculateContentHeight()
// Use setTimeout to avoid async function type mismatch
setTimeout(() => {
loadProgressData().then(() => {
// Data loaded successfully
}).catch((error) => {
console.error('Failed to load data:', error)
})
}, 100)
})
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
</script>
<style scoped>
.progress-container {
flex: 1;
background-color: #f5f5f5;
}
.header {
height: 60px;
background-image: linear-gradient(to top right, #4CAF50, #45a049);
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.header-left {
flex-direction: row;
align-items: center;
flex: 1;
}
.back-btn {
background-color: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 20px;
padding: 8px 12px;
flex-direction: row;
align-items: center;
margin-right: 12px;
}
.back-btn simple-icon {
margin-right: 4px;
}
.back-btn text {
color: #FFFFFF;
font-size: 14px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #FFFFFF;
}
.header-actions {
flex-direction: row;
align-items: center;
}
.refresh-btn {
background-color: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 20px;
padding: 8px 12px;
flex-direction: row;
align-items: center;
}
.refresh-btn simple-icon {
margin-right: 4px;
}
.refresh-btn text {
color: #FFFFFF;
font-size: 14px;
}
.loading-container {
flex: 1;
justify-content: center;
align-items: center;
}
.loading-text {
font-size: 16px;
color: #666;
}
.content {
flex: 1;
padding: 16px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
.progress-overview {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.overall-stats {
flex-direction: row;
justify-content: space-between;
}
.stat-card {
flex: 1;
align-items: center;
padding: 12px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #4CAF50;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
.progress-by-subject {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.subject-item {
margin-bottom: 16px;
}
.subject-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.subject-name {
font-size: 16px;
font-weight: bold;
color: #333;
}
.subject-percentage {
font-size: 14px;
color: #4CAF50;
font-weight: bold;
}
.progress-bar {
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
margin-bottom: 8px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #4CAF50;
border-radius: 4px;
}
.subject-details {
flex-direction: row;
justify-content: space-between;
}
.detail-text {
font-size: 12px;
color: #666;
}
.recent-activities {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.activity-item {
flex-direction: row;
align-items: flex-start;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.activity-icon {
width: 40px;
height: 40px;
background-color: #f0f0f0;
border-radius: 20px;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.activity-content {
flex: 1;
}
.activity-title {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.activity-desc {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.activity-time {
font-size: 11px;
color: #999;
}
.weekly-goals {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
}
.goal-item {
margin-bottom: 16px;
}
.goal-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.goal-name {
font-size: 16px;
font-weight: bold;
color: #333;
}
.goal-status {
font-size: 12px;
color: #FF9800;
background-color: #FFF3E0;
padding: 4px 8px;
border-radius: 12px;
}
.goal-status.completed {
color: #4CAF50;
background-color: #E8F5E8;
}
.goal-desc {
font-size: 12px;
color: #666;
margin-top: 8px;
}
</style>