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

820 lines
20 KiB
Plaintext
Raw Permalink 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.
<!-- 教师仪表板 - UTSJSONObject 优化版本 -->
<template> <scroll-view direction="vertical" class="teacher-dashboard">
<!-- 加载状态 -->
<view class="loading-overlay" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
<!-- 错误状态 -->
<view class="error-overlay" v-if="error != '' && loading == false">
<text class="error-text">{{ error }}</text>
<button class="retry-btn" @click="retryLoad">
<text class="retry-text">重试</text>
</button>
</view>
<view class="header">
<text class="title">教师工作台</text>
<text class="welcome">欢迎回来,{{ teacherName }}</text>
</view>
<!-- 消息中心入口 -->
<view class="message-section">
<view class="message-card" @click="navigateToMessages">
<text class="message-icon">💬</text>
<view class="message-info">
<text class="message-title">消息中心</text>
<text class="message-desc">查看您的消息和通知</text>
</view>
<text class="message-badge" v-if="unreadMessageCount > 0">{{ unreadMessageCount > 99 ? '99+' : unreadMessageCount }}</text>
<text class="message-arrow"></text>
</view>
</view>
<!-- 快速统计 -->
<view class="stats-section">
<view class="stats-grid">
<view class="stat-card">
<text class="stat-icon">📋</text>
<text class="stat-number">{{ stats.total_assignments }}</text>
<text class="stat-label">总作业数</text>
</view>
<view class="stat-card">
<text class="stat-icon">✅</text>
<text class="stat-number">{{ stats.completed_assignments }}</text>
<text class="stat-label">已完成</text>
</view>
<view class="stat-card">
<text class="stat-icon">⏰</text>
<text class="stat-number">{{ stats.pending_review }}</text>
<text class="stat-label">待评阅</text>
</view>
<view class="stat-card" @click="navigateToStudents">
<text class="stat-icon">👥</text>
<text class="stat-number">{{ stats.total_students }}</text>
<text class="stat-label">学生总数</text>
</view>
</view>
</view>
<!-- 快速操作 -->
<view class="actions-section">
<text class="section-title">快速操作</text>
<view class="actions-grid">
<view class="action-card" @click="navigateToProjects">
<text class="action-icon">🏋️‍♂️</text>
<text class="action-title">项目管理</text>
<text class="action-desc">创建和管理训练项目</text>
</view>
<view class="action-card" @click="navigateToAssignments">
<text class="action-icon">📝</text>
<text class="action-title">作业管理</text>
<text class="action-desc">布置和管理训练作业</text>
</view>
<view class="action-card" @click="navigateToRecords">
<text class="action-icon">📊</text>
<text class="action-title">记录管理</text>
<text class="action-desc">查看学生训练记录</text>
</view>
<view class="action-card" @click="navigateToAnalytics">
<text class="action-icon">📈</text>
<text class="action-title">数据分析</text>
<text class="action-desc">训练数据统计分析</text>
</view>
</view>
</view> <!-- 最近活动 -->
<view class="recent-section">
<text class="section-title">最近活动</text>
<view v-if="loading" class="loading-activities">
<text class="loading-text">加载活动中...</text>
</view>
<view v-else-if="recentActivities.length == 0" class="empty-state">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无最近活动</text>
</view> <view v-else class="activities-list">
<view v-for="activity in recentActivities" :key="activity.id" class="activity-item">
<text class="activity-icon">{{ activity.type == 'assignment' ? '📝' : (activity.type == 'project' ? '🏋️‍♀️' : (activity.type == 'record' ? '📊' : (activity.type == 'evaluation' ? '✅' : '📌'))) }}</text>
<view class="activity-content">
<text class="activity-title">{{ activity.title != null && activity.title != '' ? activity.title : (activity.description != null && activity.description != '' ? activity.description : '无标题') }}</text>
<text class="activity-time">{{ formatDateTimeLocal(activity.created_at) }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import type { AkReqResponse } from '@/uni_modules/ak-req/index.uts'
import type {
StatisticsData
} from '../types.uts'
import {
formatDateTime,
getUserName
} from '../types.uts'
import { getCurrentUserId, getCurrentUserClassId } from '@/utils/store.uts'
import { MsgDataServiceReal } from '@/utils/msgDataServiceReal.uts'
import {MessageStats} from '@/utils/msgTypes.uts'
// 本地格式化函数,用于模板调用
const formatDateTimeLocal = (dateStr: string): string => {
return formatDateTime(dateStr)
}
// 定义教师统计数据类型
type TeacherStats = {
total_assignments: number
completed_assignments: number
pending_review: number
total_students: number
}
// 定义作业数据类型(用于统计)
type AssignmentData = {
id: string
status: string
}
// 定义用户数据类型
type UserData = {
id: string
}
// 定义教师活动类型(统一用于显示)
type TeacherActivity = {
id: string
title: string | null
description: string | null
status: string | null
type: string | null
created_at: string
updated_at: string
}
// 响应式数据
const teacherName = ref<string>('教师')
const stats = ref<TeacherStats>({
total_assignments: 0,
completed_assignments: 0,
pending_review: 0,
total_students: 0
})
const recentActivities = ref<Array<TeacherActivity>>([])
const loading = ref<boolean>(false)
const error = ref<string>('')
// 消息相关数据
const unreadMessageCount = ref<number>(0)
// 导航函数
const navigateToProjects = () => {
uni.navigateTo({
url: '/pages/sport/teacher/projects'
})
}
const navigateToAssignments = () => {
uni.navigateTo({
url: '/pages/sport/teacher/assignments'
})
}
const navigateToRecords = () => {
uni.navigateTo({
url: '/pages/sport/teacher/records'
})
}
const navigateToAnalytics = () => {
uni.navigateTo({
url: '/pages/sport/teacher/analytics'
})
}
const navigateToStudents = () => {
uni.navigateTo({
url: '/pages/sport/teacher/students'
})
}
const navigateToMessages = () => {
try {
uni.navigateTo({
url: '/pages/msg/index',
success: () => {
console.log('成功导航到消息页面')
},
fail: (err) => {
console.error('导航到消息页面失败:', err)
uni.showToast({
title: '页面跳转失败',
icon: 'none'
})
}
})
} catch (error) {
console.error('导航到消息页面异常:', error)
uni.showToast({
title: '页面跳转异常',
icon: 'none'
})
}
}
// 加载教师统计数据
const loadTeacherStats = async () => {
console.log('=== loadTeacherStats 开始 ===')
try {
const currentUser = getCurrentUserId()
if (currentUser == null || currentUser == '') {
console.warn('用户未登录,设置默认统计数据')
stats.value = {
total_assignments: 0,
completed_assignments: 0,
pending_review: 0,
total_students: 0
} as TeacherStats
return
}
// 先设置默认值防止UI显示异常
stats.value = {
total_assignments: 0,
completed_assignments: 0,
pending_review: 0,
total_students: 0
} as TeacherStats
// 获取作业统计
try {
const assignmentStatsResponse = await supa
.from('ak_assignments')
.select('*', { count: 'exact', head: true })
.eq('teacher_id', currentUser)
.execute()
if (assignmentStatsResponse.status >= 200 && assignmentStatsResponse.status < 300) {
stats.value.total_assignments = assignmentStatsResponse.total ?? 0
}
} catch (err) {
console.error('作业统计请求异常:', err)
}
// 获取已完成作业统计
try {
const completedStatsResponse = await supa
.from('ak_assignments')
.select('*', { count: 'exact', head: true })
.eq('teacher_id', currentUser)
.eq('status', 'completed')
.execute()
if (completedStatsResponse.status >= 200 && completedStatsResponse.status < 300) {
stats.value.completed_assignments = completedStatsResponse.total ?? 0
}
} catch (err) {
console.error('已完成作业统计请求异常:', err)
}
// 获取待评阅作业统计
try {
const pendingStatsResponse = await supa
.from('ak_assignments')
.select('*', { count: 'exact', head: true })
.eq('teacher_id', currentUser)
.eq('status', 'submitted')
.execute()
if (pendingStatsResponse.status >= 200 && pendingStatsResponse.status < 300) {
stats.value.pending_review = pendingStatsResponse.total ?? 0
}
} catch (err) {
console.error('待评阅作业统计请求异常:', err)
}
// 获取学生统计 - 基于当前用户的班级
try {
const currentUserClassId = getCurrentUserClassId()
if (currentUserClassId != null && currentUserClassId !== '') {
// 获取同班级的学生数量
const studentStatsResponse = await supa
.from('ak_users')
.select('*', { count: 'exact', head: true })
.eq('role', 'student')
.eq('class_id', currentUserClassId)
.execute()
if (studentStatsResponse.status >= 200 && studentStatsResponse.status < 300) {
stats.value.total_students = studentStatsResponse.total ?? 0
console.log('同班级学生数量:', stats.value.total_students)
}
} else {
console.warn('当前用户未分配班级,无法统计学生数量')
stats.value.total_students = 0
}
} catch (err) {
console.error('学生统计请求异常:', err)
}
} catch (error) {
console.error('loadTeacherStats整体失败:', error)
// 设置默认值,避免页面卡死
stats.value = {
total_assignments: 0,
completed_assignments: 0,
pending_review: 0,
total_students: 0
} as TeacherStats
}
}
// 加载最近活动数据
const loadRecentActivities = async () => {
console.log('=== loadRecentActivities 开始 ===')
try {
const currentUser = getCurrentUserId()
if (currentUser == null || currentUser == '') {
console.warn('用户未登录,设置空活动列表')
recentActivities.value = []
return
}
// 先设置空数组避免UI异常
recentActivities.value = []
// 获取最近的作业活动
const activitiesResponse = await supa
.from('ak_assignments')
.select('id, title, description, status, created_at, updated_at', {})
.eq('teacher_id', currentUser)
.order('updated_at', { ascending: false })
.limit(5)
.execute()
if (activitiesResponse.status >= 200 && activitiesResponse.status < 300 && activitiesResponse.data != null) {
const rawData = activitiesResponse.data as Array<UTSJSONObject>
// 将UTSJSONObject转换为TeacherActivity类型
const processedData = rawData.map((item): TeacherActivity => {
return {
id: (item['id'] as string) ?? '',
title: (item['title'] as string) ?? null,
description: (item['description'] as string) ?? null,
status: (item['status'] as string) ?? null,
type: 'assignment', // 默认为作业类型
created_at: (item['created_at'] as string) ?? '',
updated_at: (item['updated_at'] as string) ?? ''
}
})
recentActivities.value = processedData
} else {
console.warn('获取最近活动失败:', activitiesResponse.status)
recentActivities.value = []
}
} catch (error) {
console.error('loadRecentActivities失败:', error)
recentActivities.value = [] // 设置空数组避免 UI 错误
}
}// 加载消息统计
const loadMessageStats = async () => {
console.log('=== loadMessageStats 开始 ===')
try {
const currentUser = getCurrentUserId()
console.log('loadMessageStats - 当前用户ID:', currentUser, '类型:', typeof currentUser)
if (currentUser === null || currentUser === '') {
console.warn('用户未登录,无法加载消息统计,但会设置默认值')
// 设置默认值后直接返回确保Promise正常resolve
unreadMessageCount.value = 0
console.log('=== loadMessageStats 提前结束(用户未登录)===')
return
}
// 先设置默认值
unreadMessageCount.value = 0
console.log('正在获取消息统计...')
const result = await MsgDataServiceReal.getMessageStats(currentUser)
console.log('消息统计响应:', result)
if (result.status === 200 && result.data != null) {
let unreadCount =0;
if (Array.isArray(result.data)) {
const stats = result.data[0];
unreadCount = stats.unread_messages
}
else
{
const stats = result.data;
unreadCount = stats.unread_messages
}
unreadMessageCount.value = unreadCount;
console.log('设置未读消息数:', unreadMessageCount.value);
} else {
console.warn('获取消息统计失败:', result.status, result.error)
}
} catch (error) {
console.error('loadMessageStats失败:', error)
// 静默失败,不显示错误提示
unreadMessageCount.value = 0
}
console.log('=== loadMessageStats 结束 ===')
} // 初始化函数 - 简化版
const loadDashboardData = async () => {
console.log('=== loadDashboardData 开始 ===')
if (loading.value) {
console.log('已经在加载中,跳过重复请求')
return
}
loading.value = true
error.value = ''
try {
console.log('开始顺序加载数据...')
// 简单的顺序加载不搞复杂的Promise.all
console.log('1. 加载教师统计...')
await loadTeacherStats()
console.log('2. 加载最近活动...')
await loadRecentActivities()
console.log('3. 加载消息统计...')
await loadMessageStats()
console.log('所有数据加载完成')
} catch (err) {
console.error('数据加载失败:', err)
error.value = '数据加载失败,请重试'
} finally {
loading.value = false
console.log('=== loadDashboardData 结束 ===')
}
}
// 简化版加载函数,用于调试
// 重试加载数据
const retryLoad = () => {
console.log('用户点击重试按钮')
loadDashboardData()
}
// 生命周期
onMounted(() => {
console.log('Dashboard页面已挂载')
// 获取当前教师名
const teacherId = getCurrentUserId()
console.log('当前用户ID:', teacherId)
if (teacherId!='') {
teacherName.value = '教师-' + teacherId.substring(0, 6)
} else {
teacherName.value = '教师'
console.warn('用户未登录或ID为空')
}
loadDashboardData()
})
</script>
<style> .teacher-dashboard {
flex:1;
background-color: #f5f5f5;
padding: 32rpx;
box-sizing: border-box;
position: relative;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-text {
font-size: 28rpx;
color: #666666;
}
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 40rpx;
}
.error-text {
font-size: 28rpx;
color: #ff3b30;
text-align: center;
margin-bottom: 32rpx;
}
.retry-btn {
background-color: #007aff;
color: #ffffff;
border: none;
border-radius: 8rpx;
padding: 16rpx 32rpx;
}
.retry-text {
font-size: 28rpx;
color: #ffffff;
}
.stat-card, .action-card {
/* Remove text-align, font-size, color, font-weight from here */
}
/* Move all text-related styles to the corresponding .stat-icon, .stat-number, .stat-label, .action-icon, etc. selectors for <text> only */
.stat-icon, .stat-number, .stat-label, .action-icon, .action-title, .action-desc, .empty-text, .activity-icon, .activity-title, .activity-time {
display: inline-block;
}
.header {
margin-bottom: 40rpx;
}
.title {
font-size: 48rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
}
.welcome {
font-size: 28rpx;
color: #666666;
}
/* 消息中心样式 */
.message-section {
margin-bottom: 32rpx;
}
.message-card {
display: flex;
flex-direction: row;
align-items: center;
background: linear-gradient(to top right, #667eea , #764ba2 );
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
}
.message-card:hover {
transform: translateY(-2rpx);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
}
.message-icon {
font-size: 40rpx;
margin-right: 24rpx;
}
.message-info {
flex: 1;
}
.message-title {
font-size: 30rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 4rpx;
}
.message-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.message-badge {
position: absolute;
top: 12rpx;
right: 60rpx;
background: #ff3b30;
color: #ffffff;
font-size: 20rpx;
padding: 6rpx 12rpx;
border-radius: 16rpx;
min-width: 32rpx;
text-align: center;
box-shadow: 0 2px 8px rgba(255, 59, 48, 0.3);
}
.message-badge .badge-text {
font-weight: bold;
}
.message-arrow {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.7);
margin-left: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 24rpx;
}
/* 统计卡片样式 */
.stats-section {
margin-bottom: 40rpx;
}
.stats-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.stats-grid .stat-card {
margin-right: 24rpx;
margin-bottom: 24rpx;
}
.stats-grid .stat-card:last-child {
margin-right: 0;
}
.stat-card {
flex: 1;
min-width: 200rpx;
background: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-icon, .stat-number, .stat-label {
display: inline-block;
text-align: center;
}
.stat-icon {
font-size: 48rpx;
margin-bottom: 16rpx;
}
.stat-number {
font-size: 36rpx;
font-weight: bold;
color: #007aff;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #666666;
}
/* 操作卡片样式 */
.actions-section {
margin-bottom: 40rpx;
}
.actions-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.actions-grid .action-card {
margin-right: 24rpx;
margin-bottom: 24rpx;
}
.actions-grid .action-card:last-child {
margin-right: 0;
}
.action-card {
flex: 1;
min-width: 280rpx;
background: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
}
.action-card:hover {
transform: translateY(-4rpx);
}
.action-icon {
font-size: 56rpx;
margin-bottom: 16rpx;
text-align: center;
}
.action-title {
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
text-align: center;
}
.action-desc {
font-size: 24rpx;
color: #666666;
line-height: 1.4;
text-align: center;
}
/* 最近活动样式 */
.recent-section {
background: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} .empty-state {
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-icon {
font-size: 64rpx;
margin-bottom: 16rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999999;
text-align: center;
}
.loading-activities {
padding: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
.activities-list {
display: flex;
flex-direction: column;
}
.activities-list .activity-item {
margin-bottom: 16rpx;
}
.activities-list .activity-item:last-child {
margin-bottom: 0;
}
.activity-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 16rpx;
border-radius: 12rpx;
background: #f8f9fa;
}
.activity-icon {
font-size: 32rpx;
margin-right: 16rpx;
}
.activity-content {
flex: 1;
}
.activity-title {
font-size: 28rpx;
color: #333333;
margin-bottom: 4rpx;
}
.activity-time {
font-size: 22rpx;
color: #999999;
}
/* 响应式设计 */
@media screen and (max-width: 768px) {
.stats-grid
{
flex-direction: row;
}
.actions-grid {
flex-direction: row;
justify-content: center;
}
.stat-card,
.action-card {
min-width: auto;
}
}
</style>