686 lines
18 KiB
Plaintext
686 lines
18 KiB
Plaintext
<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> |