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

1328 lines
32 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="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>