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

686 lines
18 KiB
Plaintext
Raw 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.
<template>
<scroll-view direction="vertical" class="assignments-page" :scroll-y="true" :enable-back-to-top="true">
<!-- 顶部统计卡片 -->
<view class="stats-container">
<view class="stat-card">
<view class="stat-number">{{ totalAssignments }}</view>
<view class="stat-label">总作业</view>
</view>
<view class="stat-card">
<view class="stat-number">{{ completedAssignments }}</view>
<view class="stat-label">已完成</view>
</view>
<view class="stat-card">
<view class="stat-number">{{ pendingAssignments }}</view>
<view class="stat-label">待完成</view>
</view>
</view>
<!-- 筛选器 -->
<view class="filter-container">
<scroll-view class="filter-scroll" direction="horizontal" :show-scrollbar="false">
<view class="filter-item" :class="{ active: selectedStatus === 'all' }" @click="filterByStatus('all')">
全部
</view>
<view class="filter-item" :class="{ active: selectedStatus === 'pending' }"
@click="filterByStatus('pending')">
待完成
</view>
<view class="filter-item" :class="{ active: selectedStatus === 'completed' }"
@click="filterByStatus('completed')">
已完成
</view>
<view class="filter-item" :class="{ active: selectedStatus === 'overdue' }"
@click="filterByStatus('overdue')">
已逾期
</view>
</scroll-view>
</view>
<!-- 作业列表 -->
<scroll-view class="assignments-list" scroll-y="true" :refresher-enabled="true"
:refresher-triggered="isRefreshing" @refresherrefresh="refreshAssignments">
<view class="assignment-item" v-for="assignment in filteredAssignments"
:key="getAssignmentIdLocal(assignment)" @click="viewAssignmentDetail(assignment)">
<view class="assignment-header">
<view class="assignment-title">{{ getAssignmentTitleLocal(assignment) }}</view>
<view class="assignment-status" :class="getAssignmentStatusLocal(assignment)">
{{ getAssignmentStatusText(assignment) }}
</view>
</view>
<view class="assignment-meta">
<view class="meta-item">
<text class="meta-icon"></text>
<text class="meta-text">{{ getProjectNameLocal(assignment) }}</text>
</view>
<view class="meta-item">
<text class="meta-icon">⏰</text>
<text class="meta-text">{{ formatDueDate(assignment) }}</text>
</view>
</view>
<view class="assignment-description">
{{ getAssignmentDescriptionLocal(assignment) }}
</view>
<view class="assignment-progress" v-if="getAssignmentProgress(assignment) > 0">
<view class="progress-bar">
<view class="progress-fill" :style="{ width: getAssignmentProgress(assignment) + '%' }"></view>
</view>
<text class="progress-text">{{ getAssignmentProgress(assignment) }}% 完成</text>
</view>
<view class="assignment-actions">
<button class="action-btn primary" @click.stop="startTraining(assignment)"
v-if="getAssignmentStatusLocal(assignment) === 'pending'">
开始训练
</button>
<button class="action-btn secondary" @click.stop="viewDetails(assignment)">
查看详情
</button>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="filteredAssignments.length === 0 && !isLoading">
<text class="empty-icon"></text>
<text class="empty-title">暂无作业</text>
<text class="empty-description">{{ getEmptyStateText() }}</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="isLoading">
<text class="loading-text">加载中...</text>
</view>
</scroll-view>
<!-- 浮动操作按钮 -->
<view class="fab-container">
<view class="fab" @click="quickFilter">
<text class="fab-icon"></text>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed, onUnmounted } from 'vue'
import { onLoad, onResize } from '@dcloudio/uni-app'
import {
getAssignmentId, getAssignmentTitle, getAssignmentDescription,
getProjectName, formatDateTime, getAssignmentStatus
} from '../types.uts'
import { getCurrentUserId } from '@/utils/store.uts'
import supaClient from '@/components/supadb/aksupainstance.uts'
// 响应式数据
const assignments = ref<UTSJSONObject[]>([])
const filteredAssignments = ref<UTSJSONObject[]>([])
const selectedStatus = ref<string>('all')
const isLoading = ref<boolean>(false)
const isRefreshing = ref<boolean>(false)
const totalAssignments = ref<number>(0)
const completedAssignments = ref<number>(0)
const pendingAssignments = ref<number>(0)
const assignmentSubscription = ref<any | null>(null)
const studentId = ref<string>('')
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 updateStatistics = () => {
totalAssignments.value = assignments.value.length
completedAssignments.value = assignments.value.filter(assignment =>
getAssignmentStatus(assignment) === 'completed').length
pendingAssignments.value = assignments.value.filter(assignment =>
getAssignmentStatus(assignment) === 'pending').length
}
// 应用筛选
const applyFilter = () => {
if (selectedStatus.value === 'all') {
filteredAssignments.value = assignments.value
} else {
filteredAssignments.value = assignments.value.filter(assignment =>
getAssignmentStatus(assignment) === selectedStatus.value)
}
}
// 加载作业列表
const loadAssignments = async () => {
try {
isLoading.value = true // 直接从 ak_assignments 表查询按学生ID筛选
const result = await supaClient
.from('ak_assignments')
.select('*', {})
.eq('student_id', studentId.value)
.order('created_at', { ascending: false })
.execute()
if (result.error == null) {
assignments.value = result.data as UTSJSONObject[]
updateStatistics()
applyFilter()
} else {
uni.showToast({
title: '加载失败:' + (result.error?.message ?? '未知错误'),
icon: 'none'
})
}
} catch (error) {
console.error('加载作业失败:', error)
uni.showToast({
title: '加载作业失败',
icon: 'none'
})
} finally {
isLoading.value = false
}
}
// 示例:实时查询作业状态变化
const watchAssignmentUpdates = async () => {
try {
uni.showToast({
title: "订阅功能尚未开放",
duration: 3000
})
// 使用 supaClient 的实时订阅功能
// const subscription = supaClient
// .from('ak_assignments')
// .on('UPDATE', (payload) => {
// console.log('作业更新:', payload)
// // 实时更新本地数据
// updateLocalAssignment(payload.new)
// })
// .subscribe()
// // 保存订阅引用以便后续取消
// assignmentSubscription.value = subscription
} catch (error) {
console.error('订阅作业更新失败:', error)
}
} // 生命周期
onLoad((options : OnLoadOptions) => {
// 从页面参数获取学生ID如果没有则从store中获取
studentId.value = options['studentId'] ?? ''
userId.value = options['id'] ?? getCurrentUserId()
if (studentId.value === '') {
studentId.value = getCurrentUserId()
}
console.log('onLoad - studentId:', studentId.value)
})
onMounted(() => {
loadAssignments()
watchAssignmentUpdates()
// Initialize screen width
screenWidth.value = uni.getSystemInfoSync().windowWidth
})
// Handle resize events for responsive design
onResize((size) => {
screenWidth.value = size.size.windowWidth
})// 安全获取作业信息
const getAssignmentIdLocal = (assignment : UTSJSONObject) : string => {
return getAssignmentId(assignment)
}
const getAssignmentTitleLocal = (assignment : UTSJSONObject) : string => {
return getAssignmentTitle(assignment)
}
const getAssignmentDescriptionLocal = (assignment : UTSJSONObject) : string => {
return getAssignmentDescription(assignment)
}
const getProjectNameLocal = (assignment : UTSJSONObject) : string => {
return getProjectName(assignment)
}
const getAssignmentStatusLocal = (assignment : UTSJSONObject) : string => {
return getAssignmentStatus(assignment)
}
const getAssignmentStatusText = (assignment : UTSJSONObject) : string => {
const status = getAssignmentStatus(assignment)
const statusMap : UTSJSONObject = {
'pending': '待完成',
'in_progress': '进行中',
'completed': '已完成',
'overdue': '已逾期'
}
const statusText = statusMap.getString(status)
if (statusText != null && statusText !== '') {
return statusText
} else {
return '未知状态'
}
}
const getAssignmentProgress = (assignment : UTSJSONObject) : number => {
return assignment.getNumber('progress') ?? 0
}
const formatDueDate = (assignment : UTSJSONObject) : string => {
const dueDate = assignment.getString('due_date') ?? ''
if (dueDate != null && dueDate !== '') {
return '截止:' + formatDateTime(dueDate)
}
return '无截止时间'
}
// 刷新作业
const refreshAssignments = async () => {
isRefreshing.value = true
await loadAssignments()
isRefreshing.value = false
}
// 按状态筛选
const filterByStatus = (status : string) => {
selectedStatus.value = status
applyFilter()
}
// 获取空状态文本
const getEmptyStateText = () : string => {
const textMap : UTSJSONObject = {
'all': '暂时没有作业,等待老师分配新的训练任务',
'pending': '没有待完成的作业',
'completed': '还没有完成任何作业',
'overdue': '没有逾期的作业'
}
const text = textMap.getString(selectedStatus.value)
if (text != null && text !== '') {
return text
} else {
return '暂无数据'
}
}
// 查看作业详情
const viewAssignmentDetail = (assignment : UTSJSONObject) => {
const assignmentId = getAssignmentId(assignment)
uni.navigateTo({
url: `/pages/sport/student/assignment-detail?id=${assignmentId}`
})
}
// 开始训练
const startTraining = (assignment : UTSJSONObject) => {
const assignmentId = getAssignmentId(assignment)
uni.navigateTo({
url: `/pages/sport/student/training-record?assignmentId=${assignmentId}`
})
}
// 查看详情
const viewDetails = (assignment : UTSJSONObject) => {
viewAssignmentDetail(assignment)
}
// 快速筛选
const quickFilter = () => {
uni.showActionSheet({
itemList: ['全部作业', '待完成', '已完成', '已逾期'],
success: (res) => {
const statusMap = ['all', 'pending', 'completed', 'overdue']
filterByStatus(statusMap[res.tapIndex!])
}
})
} // 获取当前学生ID优先从页面参数然后从store中获取
const getCurrentStudentId = () : string => {
try {
// 优先使用页面参数传入的学生ID
if (studentId.value != null && studentId.value !== '') {
return studentId.value
}
// 其次从store获取当前用户ID
const userId = getCurrentUserId()
if (userId != null && userId !== '') {
return userId
}
// 备用方案:从本地存储获取
return uni.getStorageSync('current_student_id') || 'demo_student_id'
} catch (error) {
console.error('获取学生ID失败', error)
return 'demo_student_id'
}
}// 示例:直接使用 supaClient 创建训练记录
const createTrainingRecord = async (assignmentId : string, recordData : UTSJSONObject) => {
try {
const result = await supaClient
.from('ak_training_records')
.insert({
assignment_id: assignmentId,
student_id: getCurrentStudentId(),
...recordData,
created_at: new Date().toISOString()
})
.single()
.execute()
if (result.error == null) {
uni.showToast({
title: '训练记录保存成功',
icon: 'success'
})
return result.data
} else {
throw new Error(result.error?.message ?? '保存失败')
}
} catch (error) {
console.error('创建训练记录失败:', error)
uni.showToast({
title: '保存训练记录失败',
icon: 'none'
})
throw error
}
}
// 更新本地作业数据
const updateLocalAssignment = (updatedAssignment : UTSJSONObject) => {
const assignmentId = getAssignmentId(updatedAssignment)
const index = assignments.value.findIndex(assignment =>
getAssignmentId(assignment) === assignmentId)
if (index !== -1) {
assignments.value[index] = updatedAssignment
updateStatistics()
applyFilter()
}
}
</script>
<style>
.assignments-page {
display: flex;
flex: 1;
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
box-sizing: border-box;
}
/* 统计卡片 */
.stats-container {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 20rpx;
}
.stat-card {
flex: 1;
background: rgba(255, 255, 255, 0.9);
border-radius: 20rpx;
padding: 30rpx 20rpx;
text-align: center;
backdrop-filter: blur(10rpx);
border: 1px solid rgba(255, 255, 255, 0.2);
margin-right: 15rpx;
}
.stat-card:last-child {
margin-right: 0;
}
.stat-number {
font-size: 48rpx;
font-weight: bold;
color: #2c3e50;
line-height: 1;
}
.stat-label {
font-size: 24rpx;
color: #7f8c8d;
margin-top: 10rpx;
}
/* 筛选器 */
.filter-container {
margin-bottom: 20rpx;
}
.filter-scroll {
flex-direction: row;
white-space: nowrap;
}
.filter-item {
display: inline-block;
padding: 15rpx 30rpx;
margin-right: 15rpx;
background: rgba(255, 255, 255, 0.3);
color: white;
border-radius: 25rpx;
font-size: 28rpx;
transition: background-color 0.3s ease, color 0.3s ease, transform 0.3s ease;
backdrop-filter: blur(10rpx);
}
.filter-item.active {
background: rgba(255, 255, 255, 0.9);
color: #2c3e50;
transform: scale(1.05);
}
/* 作业列表 */
.assignments-list {
height: 70vh;
}
.assignment-item {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
margin-bottom: 20rpx;
padding: 30rpx;
backdrop-filter: blur(10rpx);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: transform 0.3s ease, background-color 0.3s ease;
}
.assignment-item:active {
transform: scale(0.98);
background: rgba(255, 255, 255, 0.9);
}
.assignment-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15rpx;
}
.assignment-title {
font-size: 32rpx;
font-weight: bold;
color: #2c3e50;
flex: 1;
margin-right: 20rpx;
}
.assignment-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: bold;
}
.assignment-status.pending {
background: #fff3cd;
color: #856404;
}
.assignment-status.completed {
background: #d1edff;
color: #0c5460;
}
.assignment-status.overdue {
background: #f8d7da;
color: #721c24;
}
.assignment-meta {
display: flex;
margin-bottom: 15rpx;
}
.meta-item {
display: flex;
align-items: center;
margin-right: 20rpx;
}
.meta-item:last-child {
margin-right: 0;
}
.meta-icon {
font-size: 24rpx;
margin-right: 8rpx;
}
.meta-text {
font-size: 26rpx;
color: #7f8c8d;
}
.assignment-description {
font-size: 28rpx;
color: #34495e;
line-height: 1.5;
margin-bottom: 20rpx;
}
.assignment-progress {
margin-bottom: 20rpx;
}
.progress-bar {
width: 100%;
height: 8rpx;
background: #ecf0f1;
border-radius: 4rpx;
overflow: hidden;
margin-bottom: 10rpx;
}
.progress-fill {
height: 100%;
background-image: linear-gradient(to bottom, #667eea, #764ba2);
transition: width 0.3s ease;
}
.progress-text {
font-size: 24rpx;
color: #7f8c8d;
}
.assignment-actions {
display: flex;
}
.action-btn {
flex: 1;
padding: 20rpx;
border-radius: 15rpx;
font-size: 28rpx;
font-weight: bold;
border: none;
transition: transform 0.3s ease;
margin-right: 15rpx;
}
.action-btn:last-child {
margin-right: 0;
}
.action-btn.primary {
background-image: linear-gradient(to top right, #667eea, #764ba2);
color: white;
}
.action-btn.secondary {
background: #ecf0f1;
color: #34495e;
}
.action-btn:active {
transform: scale(0.95);
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 100rpx 40rpx;
color: white;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.6;
}
.empty-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 15rpx;
}
.empty-description {
font-size: 28rpx;
opacity: 0.8;
line-height: 1.5;
}
/* 加载状态 */
.loading-state {
text-align: center;
padding: 50rpx;
color: white;
}
.loading-text {
font-size: 28rpx;
opacity: 0.8;
}
/* 浮动按钮 */
.fab-container {
position: fixed;
bottom: 40rpx;
right: 40rpx;
z-index: 999;
}
.fab {
width: 120rpx;
height: 120rpx;
background-image: linear-gradient(to top right, #667eea, #764ba2);
border-radius: 60rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.4);
transition: transform 0.3s ease;
}
.fab:active {
transform: scale(0.9);
}
.fab-icon {
font-size: 36rpx;
color: white;
}
</style>