Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,686 @@
<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 {