1328 lines
32 KiB
Plaintext
1328 lines
32 KiB
Plaintext
<!-- 学生仪表板 - UTSJSONObject 优化版本 -->
|
||
<template>
|
||
<scroll-view direction="vertical" class="student-dashboard" :scroll-y="true" :enable-back-to-top="true">
|
||
<view class="header">
|
||
<text class="title">我的监测</text>
|
||
<text class="welcome">{{ studentName }},继续加油!</text>
|
||
</view>
|
||
|
||
<!-- 健康监测入口 -->
|
||
<view class="actions-section">
|
||
<view class="actions-grid">
|
||
<view class="action-card sense-entry" @click="navigateToSense">
|
||
<view class="sense-icons">
|
||
<view class="sense-item">
|
||
<text class="sense-icon">❤️</text>
|
||
<text class="sense-value">--</text>
|
||
</view>
|
||
<view class="sense-item">
|
||
<text class="sense-icon">🩸</text>
|
||
<text class="sense-value">--</text>
|
||
</view>
|
||
<view class="sense-item">
|
||
<text class="sense-icon">💨</text>
|
||
<text class="sense-value">--</text>
|
||
</view>
|
||
<view class="sense-item">
|
||
<text class="sense-icon">🛌</text>
|
||
<text class="sense-value">--</text>
|
||
</view>
|
||
</view>
|
||
<text class="action-title">健康监测</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 消息中心预览 -->
|
||
<view class="message-section">
|
||
<view class="message-preview-card">
|
||
<view class="card-header" @click="navigateToMessages">
|
||
<text class="card-title">消息中心</text>
|
||
<button class="more-btn" @click="navigateToMessages">
|
||
<text class="more-text" @click="navigateToMessages">查看全部</text>
|
||
</button>
|
||
</view>
|
||
<scroll-view class="message-list" direction="vertical" v-if="hasRecentMessages">
|
||
<view class="message-item" v-for="(message, index) in recentMessages" :key="index"
|
||
@click="viewMessage(message)">
|
||
<view class="message-header">
|
||
<text class="message-type">{{ getMessageTypeName(message.getString('type') ?? '') }}</text>
|
||
<text
|
||
class="message-time">{{ formatMessageTime(message.getString('created_at') ?? '') }}</text>
|
||
<view class="message-status" v-if="message.getBoolean('is_read') == false">
|
||
<text class="unread-dot">●</text>
|
||
</view>
|
||
</view>
|
||
<text class="message-title"
|
||
v-if="message.getString('title')">{{ message.getString('title') }}</text>
|
||
<text class="message-content">{{ getMessagePreview(message.getString('content') ?? '') }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="empty-messages" v-else-if="!messageLoading">
|
||
<text class="empty-icon">📭</text>
|
||
<text class="empty-text">暂无消息</text>
|
||
</view>
|
||
<view class="loading-messages" v-else>
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 进度概览 -->
|
||
<view class="progress-section">
|
||
<view class="progress-card">
|
||
<view class="progress-header">
|
||
<text class="progress-title">本周进度</text>
|
||
<text class="progress-percentage">{{ weeklyProgress }}%</text>
|
||
</view>
|
||
<view class="progress-bar">
|
||
<view class="progress-fill" :style="{ width: `${weeklyProgress}%` }"></view>
|
||
</view>
|
||
<view class="progress-stats">
|
||
<view class="stat-item">
|
||
<text class="stat-number">{{ completedAssignments }}</text>
|
||
<text class="stat-label">已完成</text>
|
||
</view>
|
||
<view class="stat-item">
|
||
<text class="stat-number">{{ totalAssignments }}</text>
|
||
<text class="stat-label">总作业</text>
|
||
</view>
|
||
<view class="stat-item">
|
||
<text class="stat-number">{{ averageScore }}</text>
|
||
<text class="stat-label">平均分</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 快速操作 -->
|
||
<view class="actions-section">
|
||
<text class="section-title">快速操作</text>
|
||
<view class="actions-grid">
|
||
<view class="action-card" @click="navigateToAssignments">
|
||
<text class="action-icon">📝</text>
|
||
<text class="action-title">我的作业</text>
|
||
<text class="action-badge" v-if="pendingAssignments > 0">{{ pendingAssignments }}</text>
|
||
</view>
|
||
<view class="action-card" @click="navigateToRecords">
|
||
<text class="action-icon">📊</text>
|
||
<text class="action-title">训练记录</text>
|
||
</view>
|
||
<view class="action-card" @click="navigateToProgress">
|
||
<text class="action-icon">📈</text>
|
||
<text class="action-title">进度跟踪</text>
|
||
</view>
|
||
<view class="action-card" @click="navigateToLocation">
|
||
<text class="action-icon">📍</text>
|
||
<text class="action-title">手环位置</text>
|
||
<view class="location-status" v-if="currentLocationStatus">
|
||
<text class="location-status-text">{{ currentLocationStatus }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="action-card" @click="navigateToProfile">
|
||
<view class="action-icon">👤</view>
|
||
<text class="action-title">个人资料</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 待完成作业 -->
|
||
<view class="assignments-section" v-if="hasPendingAssignments">
|
||
<view class="section-header">
|
||
<text class="section-title">待完成作业</text>
|
||
<text class="section-more" @click="navigateToAssignments">查看全部</text>
|
||
</view>
|
||
<supadb ref="assignmentsRef" collection="ak_assignments" :filter="pendingAssignmentsFilter" getcount="exact"
|
||
:page-size="3" @process-data="handlePendingAssignmentsData" @error="handleError">
|
||
</supadb>
|
||
<view class="assignments-list">
|
||
<view v-for="assignment in pendingAssignmentsList" :key="getAssignmentIdLocal(assignment)"
|
||
class="assignment-item" @click="startAssignment(assignment)">
|
||
<view class="assignment-info">
|
||
<text class="assignment-title">{{ getAssignmentDisplayTitleLocal(assignment) }}</text>
|
||
<text
|
||
class="assignment-due">截止:{{ formatDateTimeLocal(getAssignmentDueDateLocal(assignment)) }}</text>
|
||
</view>
|
||
<view class="assignment-status">
|
||
<view class="status-badge pending">
|
||
<text class="badge-text">待完成</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 最近训练记录 -->
|
||
<view class="records-section">
|
||
<view class="section-header">
|
||
<text class="section-title">最近训练</text>
|
||
<text class="section-more" @click="navigateToRecords">查看全部</text>
|
||
</view>
|
||
<supadb ref="recordsRef" collection="ak_training_records" :filter="recentRecordsFilter" getcount="exact"
|
||
:page-size="5" :orderby="'created_at.desc'" @process-data="handleRecentRecordsData"
|
||
@error="handleError">
|
||
</supadb>
|
||
<view v-if="recentRecordsList.length === 0" class="empty-state">
|
||
<text class="empty-text">暂无训练记录</text>
|
||
</view>
|
||
<view v-else class="records-list">
|
||
<view v-for="record in recentRecordsList" :key="getRecordIdLocal(record)" class="record-item"
|
||
@click="viewRecord(record)">
|
||
<view class="record-icon">🏃♂️</view>
|
||
<view class="record-info">
|
||
<text class="record-title">{{ getRecordTitle(record) }}</text>
|
||
<text class="record-time">{{ formatDateTimeLocal(getRecordSubmittedAtLocal(record)) }}</text>
|
||
</view>
|
||
<view class="record-score">
|
||
<text class="score-text">{{ getRecordScoreLocal(record) }}分</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||
|
||
import { getCurrentUserId } from '@/utils/store.uts'
|
||
import type {
|
||
AssignmentData,
|
||
RecordData,
|
||
UserData,
|
||
} from '../types.uts'
|
||
import {
|
||
getAssignmentId,
|
||
getAssignmentDisplayTitle,
|
||
getAssignmentDueDate,
|
||
getRecordId,
|
||
getRecordScore,
|
||
getRecordSubmittedAt,
|
||
formatDateTime,
|
||
getUserName
|
||
} from '../types.uts'
|
||
import { MsgDataServiceReal } from '@/utils/msgDataServiceReal.uts'
|
||
import type { MessageListParams } from '@/utils/msgTypes.uts'
|
||
// Unified user ID handling
|
||
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 studentName = ref<string>('同学')
|
||
const weeklyProgress = ref<number>(0)
|
||
const completedAssignments = ref<number>(0)
|
||
const totalAssignments = ref<number>(0)
|
||
const averageScore = ref<number>(0)
|
||
const pendingAssignments = ref<number>(0)
|
||
const pendingAssignmentsList = ref<AssignmentData[]>([])
|
||
const recentRecordsList = ref<RecordData[]>([])
|
||
// 消息相关数据
|
||
const unreadMessageCount = ref<number>(0)
|
||
const recentMessages = ref<Array<UTSJSONObject>>([])
|
||
const messageLoading = ref<boolean>(false)
|
||
|
||
// 位置相关数据
|
||
const currentLocationStatus = ref<string | null>(null)
|
||
|
||
// 组件引用
|
||
const assignmentsRef = ref<SupadbComponentPublicInstance | null>(null)
|
||
const recordsRef = ref<SupadbComponentPublicInstance | null>(null)
|
||
|
||
// 当前用户ID - use unified userId
|
||
const currentUserId = computed(() => userId.value)
|
||
|
||
// 计算属性
|
||
const pendingAssignmentsFilter = computed(() => {
|
||
const filter = new UTSJSONObject()
|
||
if (currentUserId.value !== null && currentUserId.value !== '') {
|
||
filter.set('status', 'active')
|
||
// 这里可能需要根据实际数据库结构调整
|
||
}
|
||
return filter
|
||
})
|
||
const recentRecordsFilter = computed(() => {
|
||
const filter = new UTSJSONObject()
|
||
if (currentUserId.value !== null && currentUserId.value !== '') {
|
||
filter.set('user_id', currentUserId.value)
|
||
}
|
||
return filter
|
||
})
|
||
|
||
// 数组长度检查的计算属性
|
||
const hasRecentMessages = computed((): boolean => {
|
||
return Array.isArray(recentMessages.value) && recentMessages.value.length > 0
|
||
})
|
||
|
||
const hasPendingAssignments = computed((): boolean => {
|
||
return Array.isArray(pendingAssignmentsList.value) && pendingAssignmentsList.value.length > 0
|
||
})
|
||
|
||
const hasNoRecentRecords = computed((): boolean => {
|
||
return !Array.isArray(recentRecordsList.value) || recentRecordsList.value.length === 0
|
||
})
|
||
// 数据访问函数 - 本地包装器函数以确保模板兼容性
|
||
const getRecordTitle = (record : RecordData) : string => {
|
||
const assignmentTitle = (record as UTSJSONObject).getString('assignment_title') ?? ''
|
||
const projectName = (record as UTSJSONObject).getString('project_name') ?? ''
|
||
|
||
if (assignmentTitle !== '') {
|
||
return assignmentTitle
|
||
} else if (projectName !== '') {
|
||
return projectName
|
||
} else {
|
||
return '训练记录'
|
||
}
|
||
}
|
||
|
||
// 本地包装函数,用于在模板中调用导入的工具函数
|
||
const getAssignmentIdLocal = (assignment : AssignmentData) : string => {
|
||
return getAssignmentId(assignment)
|
||
}
|
||
|
||
const getAssignmentDisplayTitleLocal = (assignment : AssignmentData) : string => {
|
||
return getAssignmentDisplayTitle(assignment)
|
||
}
|
||
|
||
const getAssignmentDueDateLocal = (assignment : AssignmentData) : string => {
|
||
return getAssignmentDueDate(assignment)
|
||
}
|
||
|
||
const getRecordIdLocal = (record : RecordData) : string => {
|
||
return getRecordId(record)
|
||
}
|
||
|
||
const getRecordScoreLocal = (record : RecordData) : number => {
|
||
return getRecordScore(record)
|
||
}
|
||
|
||
const getRecordSubmittedAtLocal = (record : RecordData) : string => {
|
||
return getRecordSubmittedAt(record)
|
||
}
|
||
|
||
const formatDateTimeLocal = (dateString : string) : string => {
|
||
return formatDateTime(dateString)
|
||
}
|
||
|
||
// 数据处理函数
|
||
const handlePendingAssignmentsData = (result : UTSJSONObject) => {
|
||
const data = result.get('data')
|
||
if (data != null && Array.isArray(data)) {
|
||
pendingAssignmentsList.value = data.slice(0, 3) as AssignmentData[]
|
||
pendingAssignments.value = data.length
|
||
}
|
||
|
||
}
|
||
// 统计计算函数
|
||
const calculateStats = (records : RecordData[]) => {
|
||
if (records.length === 0) return
|
||
|
||
let totalScore = 0
|
||
let completedCount = 0
|
||
for (let i = 0; i < records.length; i++) {
|
||
const record = records[i]
|
||
const score = getRecordScoreLocal(record)
|
||
const status = record.get('status') as string | null
|
||
|
||
if (status != null && status === 'completed') {
|
||
completedCount++
|
||
totalScore += score
|
||
}
|
||
}
|
||
|
||
completedAssignments.value = completedCount
|
||
totalAssignments.value = records.length
|
||
averageScore.value = completedCount > 0 ? Math.round(totalScore / completedCount) : 0
|
||
|
||
// 计算本周进度(简化版本)
|
||
weeklyProgress.value = totalAssignments.value > 0 ? Math.round((completedCount / totalAssignments.value) * 100) : 0
|
||
}
|
||
|
||
const handleRecentRecordsData = (result : UTSJSONObject) => {
|
||
const data = result.get('data')
|
||
if (data != null && Array.isArray(data)) {
|
||
recentRecordsList.value = data.slice(0, 5) as RecordData[]
|
||
|
||
// 计算统计数据
|
||
calculateStats(data as RecordData[])
|
||
}
|
||
}
|
||
|
||
const handleError = (error : any) => {
|
||
console.error('Student dashboard data load error:', error)
|
||
uni.showToast({
|
||
title: '数据加载失败',
|
||
icon: 'error'
|
||
})
|
||
}
|
||
|
||
// 导航函数
|
||
const navigateToAssignments = () => {
|
||
try {
|
||
uni.navigateTo({
|
||
url: '/pages/sport/student/assignments',
|
||
success: () => {
|
||
console.log('成功导航到作业页面')
|
||
},
|
||
fail: (err) => {
|
||
console.error('导航失败:', err)
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('导航异常:', error)
|
||
}
|
||
}
|
||
const navigateToRecords = () => {
|
||
try {
|
||
uni.navigateTo({
|
||
url: '/pages/sport/student/records',
|
||
success: () => {
|
||
console.log('成功导航到训练记录页面')
|
||
},
|
||
fail: (err) => {
|
||
console.error('导航失败:', err)
|
||
uni.showToast({
|
||
title: '页面跳转失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('导航异常:', error)
|
||
uni.showToast({
|
||
title: '页面跳转异常',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
const navigateToProgress = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/sport/student/progress'
|
||
})
|
||
}
|
||
const navigateToProfile = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/sport/student/profile'
|
||
})
|
||
}
|
||
|
||
const navigateToLocation = () => {
|
||
try {
|
||
uni.navigateTo({
|
||
url: '/pages/sport/student/location',
|
||
success: () => {
|
||
console.log('成功导航到手环位置页面')
|
||
},
|
||
fail: (err) => {
|
||
console.error('导航到手环位置页面失败:', err)
|
||
uni.showToast({
|
||
title: '页面跳转失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('导航到手环位置页面异常:', error)
|
||
uni.showToast({
|
||
title: '页面跳转异常',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
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 startAssignment = (assignment : AssignmentData) => {
|
||
const assignmentId = getAssignmentIdLocal(assignment)
|
||
uni.navigateTo({
|
||
url: `/pages/sport/student/assignment-detail?id=${assignmentId}`
|
||
})
|
||
}
|
||
|
||
const viewRecord = (record : RecordData) => {
|
||
const recordId = getRecordIdLocal(record)
|
||
uni.navigateTo({
|
||
url: `/pages/sport/student/record-detail?id=${recordId}` })
|
||
}
|
||
|
||
|
||
// 加载位置状态
|
||
const loadLocationStatus = async () => {
|
||
try {
|
||
const userId = getCurrentUserId()
|
||
if (userId === null || userId === '') {
|
||
currentLocationStatus.value = '未连接'
|
||
return
|
||
}
|
||
|
||
// 模拟获取位置状态
|
||
// 在实际项目中,这里会调用LocationService.getCurrentLocation
|
||
const deviceId = `device_${userId}`
|
||
// 为了避免阻塞其他数据加载,这里使用简化的模拟
|
||
setTimeout(() => {
|
||
currentLocationStatus.value = '在线'
|
||
}, 2000)
|
||
} catch (error) {
|
||
console.error('加载位置状态失败:', error)
|
||
currentLocationStatus.value = '离线'
|
||
}
|
||
}
|
||
|
||
// 加载消息统计
|
||
const loadMessageStats = async () => {
|
||
try {
|
||
const currentUser = getCurrentUserId()
|
||
if (currentUser === null || currentUser === '') {
|
||
console.warn('用户未登录,无法加载消息统计')
|
||
return
|
||
}
|
||
|
||
const result = await MsgDataServiceReal.getMessageStats(currentUser)
|
||
if (result.status === 200 && result.data != null) {
|
||
const stats = result.data as UTSJSONObject
|
||
const unreadCount = stats.getNumber('unread_messages')
|
||
unreadMessageCount.value = unreadCount != null ? unreadCount.toInt() : 0
|
||
}
|
||
} catch (error) {
|
||
console.error('加载消息统计失败:', error)
|
||
// 静默失败,不显示错误提示
|
||
}
|
||
}
|
||
// 加载模拟消息数据(当API不可用时使用)
|
||
const loadMockMessages = async () => {
|
||
const mockMessages = [
|
||
{
|
||
id: '1',
|
||
type: 'assignment',
|
||
title: '新作业发布',
|
||
content: '体能训练作业已发布,请及时完成',
|
||
created_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(), // 30分钟前
|
||
is_read: false,
|
||
is_urgent: true
|
||
},
|
||
{
|
||
id: '2',
|
||
type: 'system',
|
||
title: '系统维护通知',
|
||
content: '系统将于今晚进行维护升级,请注意保存数据',
|
||
created_at: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), // 2小时前
|
||
is_read: false,
|
||
is_urgent: false
|
||
},
|
||
{
|
||
id: '3',
|
||
type: 'achievement',
|
||
title: '恭喜获得新成就',
|
||
content: '您已连续完成7天训练,获得"坚持训练"徽章',
|
||
created_at: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(), // 6小时前
|
||
is_read: true,
|
||
is_urgent: false
|
||
},
|
||
{
|
||
id: '4',
|
||
type: 'training',
|
||
title: '训练提醒',
|
||
content: '距离下次训练还有1小时,请做好准备',
|
||
created_at: new Date(Date.now() - 12 * 60 * 60 * 1000).toISOString(), // 12小时前
|
||
is_read: true,
|
||
is_urgent: false
|
||
},
|
||
{
|
||
id: '5',
|
||
type: 'social',
|
||
title: '班级排名更新',
|
||
content: '本周班级排名已更新,您当前排名第3位',
|
||
created_at: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // 1天前
|
||
is_read: true,
|
||
is_urgent: false
|
||
}
|
||
]
|
||
|
||
const messages : Array<UTSJSONObject> = []
|
||
let unreadCount = 0
|
||
|
||
for (let i = 0; i < mockMessages.length; i++) {
|
||
const msg = mockMessages[i]
|
||
const msgObj = new UTSJSONObject()
|
||
msgObj.set('id', msg.id)
|
||
msgObj.set('type', msg.type)
|
||
msgObj.set('title', msg.title)
|
||
msgObj.set('content', msg.content)
|
||
msgObj.set('created_at', msg.created_at)
|
||
msgObj.set('is_read', msg.is_read)
|
||
msgObj.set('is_urgent', msg.is_urgent)
|
||
|
||
if (msg.is_read == false) {
|
||
unreadCount++
|
||
}
|
||
|
||
messages.push(msgObj)
|
||
}
|
||
|
||
recentMessages.value = messages
|
||
unreadMessageCount.value = unreadCount
|
||
}
|
||
|
||
// 加载最近消息
|
||
const loadRecentMessages = async () => {
|
||
messageLoading.value = true
|
||
try {
|
||
const currentUser = getCurrentUserId()
|
||
if (currentUser === null || currentUser === '') {
|
||
console.warn('用户未登录,无法加载消息')
|
||
await loadMockMessages()
|
||
return
|
||
}
|
||
|
||
// 构建查询参数
|
||
const params : MessageListParams = {
|
||
receiver_type: 'user',
|
||
receiver_id: currentUser,
|
||
limit: 5,
|
||
offset: 0
|
||
}
|
||
|
||
const result = await MsgDataServiceReal.getMessages(params)
|
||
if (result.status === 200 && result.data != null && Array.isArray(result.data)) {
|
||
const dataArray = result.data as Array<any>
|
||
const messages : Array<UTSJSONObject> = []
|
||
for (let i = 0; i < dataArray.length; i++) {
|
||
const msg = dataArray[i]
|
||
const msgObj = new UTSJSONObject()
|
||
|
||
// 从消息对象中提取数据
|
||
const messageData = msg as UTSJSONObject
|
||
msgObj.set('id', messageData.get('id') ?? '')
|
||
msgObj.set('type', messageData.get('message_type_id') ?? 'system')
|
||
msgObj.set('title', messageData.get('title') ?? '')
|
||
msgObj.set('content', messageData.get('content') ?? '')
|
||
msgObj.set('created_at', messageData.get('created_at') ?? '')
|
||
msgObj.set('is_read', messageData.get('status') === 'read')
|
||
msgObj.set('is_urgent', messageData.get('is_urgent') ?? false)
|
||
|
||
messages.push(msgObj)
|
||
}
|
||
recentMessages.value = messages
|
||
|
||
// 更新未读消息数量
|
||
let unreadCount = 0
|
||
for (let i = 0; i < messages.length; i++) {
|
||
const msg = messages[i]
|
||
const isRead = msg.getBoolean('is_read')
|
||
if (isRead == null || isRead == false) {
|
||
unreadCount++
|
||
}
|
||
}
|
||
unreadMessageCount.value = unreadCount
|
||
} else {
|
||
// 如果API调用失败,使用模拟数据
|
||
await loadMockMessages()
|
||
}
|
||
} catch (error) {
|
||
console.error('加载最近消息失败:', error)
|
||
// 出错时使用模拟数据
|
||
await loadMockMessages()
|
||
} finally {
|
||
messageLoading.value = false
|
||
}
|
||
}
|
||
|
||
|
||
// 消息相关工具方法
|
||
function getMessageTypeName(type : string) : string {
|
||
const typeNames = new Map<string, string>()
|
||
typeNames.set('assignment', '作业通知')
|
||
typeNames.set('system', '系统通知')
|
||
typeNames.set('achievement', '成就通知')
|
||
typeNames.set('training', '训练提醒')
|
||
typeNames.set('social', '班级消息')
|
||
typeNames.set('announcement', '公告通知')
|
||
typeNames.set('reminder', '提醒消息')
|
||
|
||
return typeNames.get(type) ?? '普通消息'
|
||
}
|
||
|
||
function formatMessageTime(timeStr : string) : string {
|
||
if (timeStr == '') return '--'
|
||
|
||
const time = new Date(timeStr)
|
||
const now = new Date()
|
||
const diff = now.getTime() - time.getTime()
|
||
|
||
if (diff < 60000) { // 1分钟内
|
||
return '刚刚'
|
||
} else if (diff < 3600000) { // 1小时内
|
||
const minutes = Math.floor(diff / 60000)
|
||
return `${minutes}分钟前`
|
||
} else if (diff < 86400000) { // 24小时内
|
||
const hours = Math.floor(diff / 3600000)
|
||
return `${hours}小时前`
|
||
} else {
|
||
const days = Math.floor(diff / 86400000)
|
||
return `${days}天前`
|
||
}
|
||
}
|
||
|
||
function getMessagePreview(content : string) : string {
|
||
if (content.length > 40) {
|
||
return content.substring(0, 40) + '...'
|
||
}
|
||
return content
|
||
}
|
||
|
||
function viewMessage(message : UTSJSONObject) {
|
||
const messageId = message.getString('id') ?? ''
|
||
console.log('查看消息:', messageId)
|
||
|
||
// 跳转到消息详情页面
|
||
uni.navigateTo({
|
||
url: `/pages/msg/detail?id=${messageId}`,
|
||
fail: () => {
|
||
uni.showToast({
|
||
title: '消息详情页面不存在',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
} // 数据加载函数
|
||
const loadDashboardData = () => {
|
||
if (assignmentsRef.value != null) {
|
||
assignmentsRef.value!!.refresh()
|
||
}
|
||
|
||
if (recordsRef.value != null) {
|
||
recordsRef.value!!.refresh()
|
||
}
|
||
|
||
// 加载消息统计
|
||
loadMessageStats()
|
||
|
||
// 加载最近消息
|
||
loadRecentMessages()
|
||
|
||
// 加载位置状态
|
||
loadLocationStatus()
|
||
}
|
||
// 生命周期
|
||
onLoad((options : OnLoadOptions) => {
|
||
userId.value = options['id'] ?? getCurrentUserId()
|
||
})
|
||
onMounted(() => {
|
||
// 获取当前用户信息
|
||
const userInfo = uni.getStorageSync('userInfo')
|
||
if (userInfo != null && userInfo != '') {
|
||
try {
|
||
const user = JSON.parse(userInfo as string) as UserData
|
||
const userName = getUserName(user)
|
||
studentName.value = (userName !== null && userName !== '') ? userName : '同学'
|
||
// userId is now set in onLoad instead of here
|
||
} catch (e) {
|
||
console.warn('Failed to parse user info:', e)
|
||
}
|
||
}
|
||
|
||
loadDashboardData()
|
||
// Initialize screen width
|
||
screenWidth.value = uni.getSystemInfoSync().windowWidth
|
||
})
|
||
|
||
// Handle resize events for responsive design
|
||
onResize((size) => {
|
||
screenWidth.value = size.size.windowWidth
|
||
})
|
||
|
||
const onShow = () => {
|
||
loadDashboardData()
|
||
}
|
||
|
||
const navigateToSense = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/sense/index'
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.student-dashboard {
|
||
display: flex;
|
||
flex: 1;
|
||
height: 100vh;
|
||
background-color: #f5f5f5;
|
||
padding: 32rpx;
|
||
padding-bottom: 40rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.header {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.section-more {
|
||
font-size: 26rpx;
|
||
color: #007aff;
|
||
}
|
||
|
||
/* 进度卡片样式 */
|
||
.progress-section {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.progress-card {
|
||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||
border-radius: 20rpx;
|
||
padding: 40rpx;
|
||
}
|
||
|
||
.progress-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.progress-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.progress-percentage {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 8rpx;
|
||
background-color: rgba(255, 255, 255, 0.3);
|
||
border-radius: 4rpx;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background-color: #ffffff;
|
||
border-radius: 4rpx;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.progress-stats {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.stat-item {}
|
||
|
||
.stat-number {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 22rpx;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* 快速操作样式 */
|
||
.actions-section {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
.actions-grid {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
} .action-card {
|
||
flex: 0 0 calc(48% - 8rpx);
|
||
flex-direction: column;
|
||
position: relative;
|
||
background: #ffffff;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx 24rpx;
|
||
margin-bottom: 16rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.action-card:nth-child(2n) {
|
||
margin-left: 16rpx;
|
||
}
|
||
|
||
.action-card:hover {
|
||
transform: translateY(-4rpx);
|
||
}
|
||
|
||
.action-icon {
|
||
font-size: 40rpx;
|
||
margin-bottom: 12rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.action-title {
|
||
font-size: 26rpx;
|
||
color: #333333;
|
||
text-align: center;
|
||
}
|
||
|
||
.action-badge {
|
||
position: absolute;
|
||
top: 16rpx;
|
||
right: 16rpx;
|
||
background: #ff3b30;
|
||
color: #ffffff;
|
||
font-size: 20rpx;
|
||
padding: 4rpx 8rpx;
|
||
border-radius: 12rpx;
|
||
min-width: 24rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.location-status {
|
||
margin-top: 8rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.location-status-text {
|
||
font-size: 20rpx;
|
||
padding: 4rpx 8rpx;
|
||
border-radius: 8rpx;
|
||
background: #e3f2fd;
|
||
color: #1976d2;
|
||
}
|
||
|
||
/* 作业列表样式 */
|
||
.assignments-section {
|
||
background: #ffffff;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx;
|
||
margin-bottom: 32rpx;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.assignments-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.assignment-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24rpx;
|
||
border: 1px solid #e5e5e5;
|
||
margin-bottom: 16rpx;
|
||
border-radius: 12rpx;
|
||
background: #f8f9fa;
|
||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||
}
|
||
|
||
.assignment-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.assignment-item:hover {
|
||
border-color: #007aff;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.assignment-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.assignment-title {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.assignment-due {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
}
|
||
|
||
.assignment-status {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 8rpx 16rpx;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.status-badge.pending {
|
||
background-color: #ff9500;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.badge-text {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
/* 记录列表样式 */
|
||
.records-section {
|
||
background: #ffffff;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 40rpx;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 28rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.records-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.record-item {
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.record-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.record-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 20rpx;
|
||
border-radius: 12rpx;
|
||
background: #f8f9fa;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.record-item:hover {
|
||
background: #e3f2fd;
|
||
}
|
||
|
||
.record-icon {
|
||
font-size: 32rpx;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.record-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.record-title {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.record-time {
|
||
font-size: 22rpx;
|
||
color: #666666;
|
||
}
|
||
|
||
.record-score {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.score-text {
|
||
font-size: 24rpx;
|
||
font-weight: bold;
|
||
color: #34c759;
|
||
}
|
||
|
||
/* 健康监测入口样式 */
|
||
.sense-entry {
|
||
background: #fffbe6;
|
||
border: 2rpx solid #ffe58f;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx 0 12rpx 0;
|
||
margin-bottom: 12rpx;
|
||
align-items: center;
|
||
}
|
||
|
||
.sense-icons {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.sense-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin: 0 12rpx;
|
||
}
|
||
|
||
.sense-icon {
|
||
font-size: 36rpx;
|
||
margin-bottom: 2rpx;
|
||
}
|
||
|
||
.sense-value {
|
||
font-size: 24rpx;
|
||
color: #faad14;
|
||
}
|
||
|
||
/* 消息预览卡片样式 */
|
||
.message-preview-card {
|
||
background-color: #ffffff;
|
||
border-radius: 12rpx;
|
||
padding: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.card-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
|
||
.more-btn {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
background-color: #f0f0f0;
|
||
border-radius: 20rpx;
|
||
border: none;
|
||
}
|
||
|
||
.more-text {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
|
||
|
||
.message-list {
|
||
height: 300rpx;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.message-item {
|
||
padding: 16rpx;
|
||
margin-bottom: 12rpx;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8rpx;
|
||
border-left: 4rpx solid #667eea;
|
||
}
|
||
|
||
.message-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.message-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.message-type {
|
||
font-size: 22rpx;
|
||
color: #667eea;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.message-time {
|
||
font-size: 20rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.message-status {
|
||
margin-left: 8rpx;
|
||
}
|
||
|
||
.unread-dot {
|
||
color: #FF3B30;
|
||
font-size: 16rpx;
|
||
}
|
||
|
||
.message-title {
|
||
font-size: 26rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.message-content {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.empty-messages {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40rpx 20rpx;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 64rpx;
|
||
margin-bottom: 16rpx;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 26rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.loading-messages {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40rpx 20rpx;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 26rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media screen and (max-width: 768px) {
|
||
.actions-grid {
|
||
flex-direction: row;
|
||
}
|
||
|
||
.action-card {
|
||
min-width: auto;
|
||
}
|
||
}
|
||
</style> |