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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,680 @@
<template>
<scroll-view direction="vertical" class="assignment-detail">
<!-- Header Card -->
<view class="header-card"> <view class="assignment-header">
<text class="assignment-title">{{ assignment.getString("title") }}</text>
<view class="status-badge" :class="`status-${getAssignmentStatusWrapper(assignment)}`">
<text class="status-text">{{ formatAssignmentStatusWrapper(getAssignmentStatusWrapper(assignment)) }}</text>
</view>
</view>
<text class="assignment-description">{{ getAssignmentDescriptionWrapper(assignment) }}</text>
<view class="meta-info">
<view class="meta-item">
<text class="meta-label">训练项目:</text>
<text class="meta-value">{{ getProjectNameWrapper(assignment) }}</text>
</view>
<view class="meta-item">
<text class="meta-label">截止时间:</text>
<text class="meta-value">{{ formatDateWrapper(getAssignmentDeadlineWrapper(assignment)) }}</text>
</view>
<view class="meta-item">
<text class="meta-label">目标分数:</text>
<text class="meta-value">{{ getAssignmentTargetScoreWrapper(assignment) }}分</text>
</view>
</view>
</view>
<!-- Progress Card -->
<view class="progress-card">
<view class="card-header">
<text class="card-title">完成进度</text>
</view>
<view class="progress-content">
<view class="progress-bar">
<view class="progress-fill" :style="`width: ${getProgressPercentage()}%`"></view>
</view>
<text class="progress-text">{{ getProgressPercentage() }}%</text>
</view>
<view class="progress-stats">
<view class="stat-item">
<text class="stat-value">{{ getCompletedCount() }}</text>
<text class="stat-label">已完成</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ getRemainingCount() }}</text>
<text class="stat-label">剩余</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ getTargetCount() }}</text>
<text class="stat-label">目标</text>
</view>
</view>
</view>
<!-- Requirements Card -->
<view class="requirements-card">
<view class="card-header">
<text class="card-title">训练要求</text>
</view> <view class="requirements-list">
<view class="requirement-item" v-for="(requirement, index) in getRequirements()" :key="index">
<view class="requirement-icon">
<text class="icon">{{ getRequirementIcon(requirement) }}</text>
</view>
<view class="requirement-content">
<text class="requirement-title">{{ getRequirementTitle(requirement) }}</text>
<text class="requirement-desc">{{ getRequirementDescription(requirement) }}</text>
</view>
</view>
</view>
</view>
<!-- Scoring Criteria Card -->
<view class="scoring-card">
<view class="card-header">
<text class="card-title">评分标准</text>
</view> <view class="scoring-list">
<view class="scoring-item" v-for="(criteria, index) in getScoringCriteria()" :key="index">
<view class="score-range">
<text class="score-text">{{ getScoringRange(criteria) }}</text>
</view>
<view class="criteria-content">
<text class="criteria-title">{{ getScoringTitle(criteria) }}</text>
<text class="criteria-desc">{{ getScoringDescription(criteria) }}</text>
</view>
</view>
</view>
</view>
<!-- Action Buttons -->
<view class="action-buttons">
<button
v-if="canStartTraining()"
class="action-btn primary-btn"
@click="startTraining"
>
开始训练
</button>
<button
v-if="canContinueTraining()"
class="action-btn primary-btn"
@click="continueTraining"
>
继续训练
</button>
<button
v-if="canViewRecords()"
class="action-btn secondary-btn"
@click="viewRecords"
>
查看记录
</button>
</view>
</scroll-view>
</template>
<script setup lang="uts"> import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onLoad, onResize } from '@dcloudio/uni-app'
import { state, getCurrentUserId } from '@/utils/store.uts'
import { getAssignmentId, getAssignmentDisplayName, getAssignmentDescription,
getAssignmentStatus, getAssignmentDeadline, getAssignmentTargetScore, getProjectName,
formatDate, formatAssignmentStatus } from '../types.uts' // Reactive data
const userId = ref('')
const assignment = ref<UTSJSONObject>({})
const assignmentId = ref('')
const loading = ref(true)
// 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
})
// Template-accessible wrapper functions for imported functions
const getAssignmentDisplayNameWrapper = (assignmentData: UTSJSONObject): string => {
return getAssignmentDisplayName(assignmentData)
}
const getAssignmentStatusWrapper = (assignmentData: UTSJSONObject): string => {
return getAssignmentStatus(assignmentData)
}
const getAssignmentDescriptionWrapper = (assignmentData: UTSJSONObject): string => {
return getAssignmentDescription(assignmentData)
}
const getProjectNameWrapper = (assignmentData: UTSJSONObject): string => {
return getProjectName(assignmentData)
}
const getAssignmentDeadlineWrapper = (assignmentData: UTSJSONObject): string => {
return getAssignmentDeadline(assignmentData)
}
const getAssignmentTargetScoreWrapper = (assignmentData: UTSJSONObject): number => {
return getAssignmentTargetScore(assignmentData)
}
const formatDateWrapper = (date: string): string => {
return formatDate(date)
}
const formatAssignmentStatusWrapper = (status: string): string => {
return formatAssignmentStatus(status)
}
// Wrapper functions for requirement and scoring criteria property access
const getRequirementIcon = (requirement: any): string => {
const req = requirement as UTSJSONObject
return req.getString('icon') ?? ''
}
const getRequirementTitle = (requirement: any): string => {
const req = requirement as UTSJSONObject
return req.getString('title') ?? ''
}
const getRequirementDescription = (requirement: any): string => {
const req = requirement as UTSJSONObject
return req.getString('description') ?? ''
}
const getScoringRange = (criteria: any): string => {
const crit = criteria as UTSJSONObject
return crit.getString('range') ?? ''
}
const getScoringTitle = (criteria: any): string => {
const crit = criteria as UTSJSONObject
return crit.getString('title') ?? ''
}
const getScoringDescription = (criteria: any): string => {
const crit = criteria as UTSJSONObject
return crit.getString('description') ?? ''
}
// Helper functions - defined before they are used
function getCompletedCount(): number {
return assignment.value.getNumber('completed_count') ?? 0
}
function getTargetCount(): number {
return assignment.value.getNumber('target_count') ?? 0
}
function getProgressPercentage(): number {
const completed = getCompletedCount()
const target = getTargetCount()
if (target <= 0) return 0
return Math.min(Math.round((completed / target) * 100), 100)
}
function getRemainingCount(): number {
return Math.max(getTargetCount() - getCompletedCount(), 0)
}
function getRequirements(): Array<any> {
const requirements = assignment.value.getAny('requirements') ?? ([] as Array<any>)
if (requirements instanceof Array) {
return requirements
}
return []
}
function getScoringCriteria(): Array<any> {
const criteriaData = assignment.value.getAny('scoring_criteria') ?? ([] as Array<any>)
// Handle new JSON format: {criteria: [{min_score, max_score, description}], ...}
if (criteriaData != null && typeof criteriaData === 'object' && !(criteriaData instanceof Array)) {
const criteria = (criteriaData as UTSJSONObject).getAny('criteria') ?? ([] as Array<any>)
if (criteria instanceof Array) {
return criteria.map((item: any) => {
const itemObj = item as UTSJSONObject
const minScore = itemObj.getNumber('min_score') ?? 0
const maxScore = itemObj.getNumber('max_score') ?? 100
const description = itemObj.getString('description') ?? ''
return {
range: `${minScore}-${maxScore}分`,
title: minScore >= 90 ? '优秀' : minScore >= 80 ? '良好' : minScore >= 70 ? '及格' : '不及格',
description: description
}
})
}
}
// Handle legacy array format
if (criteriaData instanceof Array) {
return criteriaData
}
return []
}
function canStartTraining(): boolean {
const status = getAssignmentStatusWrapper(assignment.value)
return status === 'pending' || status === 'not_started'
}
function canContinueTraining(): boolean {
const status = getAssignmentStatusWrapper(assignment.value)
return status === 'in_progress'
}
function canViewRecords(): boolean {
return getCompletedCount() > 0
}
function startTraining() {
uni.navigateTo({
url: `/pages/sport/student/training-record?assignmentId=${assignmentId.value}`
})
}
function continueTraining() {
uni.navigateTo({
url: `/pages/sport/student/training-record?assignmentId=${assignmentId.value}`
})
}
function viewRecords() {
uni.navigateTo({
url: `/pages/sport/student/records?assignmentId=${assignmentId.value}`
})
}
// Methods
function loadAssignmentDetail() {
loading.value = true
// Mock data - replace with actual API call
setTimeout(() => {
assignment.value = {
"id": assignmentId.value,
"title": "跳远技术训练",
"description": "完成跳远基础技术动作训练,重点练习助跑、起跳和落地技术",
"project_name": "跳远训练",
"status": "in_progress",
"deadline": "2024-01-20T23:59:59",
"target_score": 85,
"target_count": 10,
"completed_count": 6,
"requirements": [
{
"icon": "",
"title": "助跑距离",
"description": "助跑距离控制在12-16步"
},
{
"icon": "",
"title": "起跳技术",
"description": "单脚起跳,起跳点准确"
},
{
"icon": "",
"title": "落地姿势",
"description": "双脚并拢前伸落地"
}
],
"scoring_criteria": [
{
"range": "90-100分",
"title": "优秀",
"description": "动作标准,技术熟练,成绩优异"
},
{
"range": "80-89分",
"title": "良好",
"description": "动作较标准,技术较熟练"
},
{
"range": "70-79分",
"title": "及格",
"description": "动作基本标准,需要继续练习"
},
{
"range": "60-69分",
"title": "不及格",
"description": "动作不标准,需要重点改进"
}
]
} as UTSJSONObject
loading.value = false
}, 1000)
} // Lifecycle
onLoad((options: OnLoadOptions) => {
userId.value = options['userId'] ?? ''
if (userId.value.length === 0) {
userId.value = getCurrentUserId()
}
assignmentId.value = options['id'] ?? ''
loadAssignmentDetail()
})
onMounted(() => {
// Initialize screen width
screenWidth.value = uni.getSystemInfoSync().windowWidth
})
// Handle resize events for responsive design
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
</script>
<style>
.assignment-detail {
display: flex;
flex:1;
background-color: #f5f5f5;
min-height: 100vh;
padding: 20rpx;
}
/* Header Card */
.header-card {
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
color: white;
}
.assignment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.assignment-title {
font-size: 36rpx;
font-weight: bold;
flex: 1;
}
.status-badge {
padding: 8rpx 16rpx;
border-radius: 16rpx;
margin-left: 20rpx;
}
.status-pending {
background-color: rgba(255, 193, 7, 0.2);
border: 1px solid rgba(255, 193, 7, 0.5);
}
.status-in_progress {
background-color: rgba(0, 123, 255, 0.2);
border: 1px solid rgba(0, 123, 255, 0.5);
}
.status-completed {
background-color: rgba(40, 167, 69, 0.2);
border: 1px solid rgba(40, 167, 69, 0.5);
}
.status-text {
font-size: 24rpx;
color: white;
}
.assignment-description {
font-size: 28rpx;
line-height: 1.6;
margin-bottom: 20rpx;
opacity: 0.9;
}
.meta-info {
display: flex;
flex-direction: column;
}
.meta-item {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.meta-item:last-child {
margin-bottom: 0;
}
.meta-label {
font-size: 26rpx;
opacity: 0.8;
width: 140rpx;
}
.meta-value {
font-size: 26rpx;
font-weight: 400;
}
/* Progress Card */
.progress-card {
background-color: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-header {
margin-bottom: 25rpx;
}
.card-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.progress-content {
display: flex;
align-items: center;
margin-bottom: 25rpx;
}
.progress-bar {
flex: 1;
height: 12rpx;
background-color: #f0f0f0;
border-radius: 6rpx;
overflow: hidden;
margin-right: 20rpx;
}
.progress-fill {
height: 100%;
background-image: linear-gradient(to bottom, #667eea, #764ba2);
border-radius: 6rpx;
transition: width 0.3s ease;
}
.progress-text {
font-size: 28rpx;
font-weight: bold;
color: #667eea;
min-width: 80rpx;
text-align: right;
}
.progress-stats {
display: flex;
justify-content: space-around;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: #667eea;
margin-bottom: 5rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
/* Requirements Card */
.requirements-card {
background-color: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.requirements-list {
display: flex;
flex-direction: column;
}
.requirement-item {
display: flex;
align-items: flex-start;
padding: 20rpx;
background-color: #f8f9ff;
border-radius: 16rpx;
margin-bottom: 20rpx;
}
.requirement-item:last-child {
margin-bottom: 0;
}
.requirement-icon {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
background-color: #667eea;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
}
.icon {
font-size: 32rpx;
}
.requirement-content {
flex: 1;
}
.requirement-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
}
.requirement-desc {
font-size: 26rpx;
color: #666;
line-height: 1.5;
}
/* Scoring Card */
.scoring-card {
background-color: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.scoring-list {
display: flex;
flex-direction: column;
}
.scoring-item {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #fafafa;
border-radius: 12rpx;
margin-bottom: 15rpx;
}
.scoring-item:last-child {
margin-bottom: 0;
}
.score-range {
width: 150rpx;
text-align: center;
margin-right: 20rpx;
flex-shrink: 0;
}
.score-text {
font-size: 24rpx;
font-weight: bold;
color: #667eea;
padding: 8rpx 12rpx;
background-color: rgba(102, 126, 234, 0.1);
border-radius: 8rpx;
}
.criteria-content {
flex: 1;
}
.criteria-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
}
.criteria-desc {
font-size: 26rpx;
color: #666;
line-height: 1.4;
}
/* Action Buttons */
.action-buttons {
padding: 20rpx 0;
display: flex;
}
.action-btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: bold;
border: none;
margin-right: 20rpx;
}
.action-btn:last-child {
margin-right: 0;
}
.primary-btn {
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
color: white;
}
.secondary-btn {
background-color: white;
color: #667eea;
border: 2rpx solid #667eea;
}
.primary-btn:active {
transform: scale(0.98);
}
.secondary-btn:active {
transform: scale(0.98);
background-color: #f8f9ff;
}
</style>

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 {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,980 @@
<template>
<scroll-view direction="vertical" class="goal-settings-page">
<!-- Header -->
<view class="header">
<view class="header-left">
<button @click="goBack" class="back-btn">
<simple-icon type="arrow-left" :size="16" />
<text>返回</text>
</button>
<text class="title">训练目标</text>
</view>
<view class="header-actions">
<button @click="addGoal" class="add-btn">
<simple-icon type="plus" :size="16" />
<text>添加</text>
</button>
</view>
</view>
<!-- Loading State -->
<view v-if="loading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<!-- Content -->
<scroll-view v-else class="content" scroll-y="true" :style="{ height: contentHeight + 'px' }">
<!-- Current Goals -->
<view class="goals-section">
<view class="section-title">当前目标</view>
<view v-if="goals.length === 0" class="empty-state">
<simple-icon type="target" :size="48" color="#BDC3C7" />
<text class="empty-text">还没有设置训练目标</text>
<text class="empty-desc">设置目标让训练更有动力</text>
<button @click="addGoal" class="add-goal-btn">设置第一个目标</button>
</view>
<view v-else class="goals-list">
<view v-for="(goal,index) in goals" :key="goal.id" class="goal-item" @click="editGoal(goal)">
<view class="goal-header">
<view class="goal-icon">
<text class="goal-emoji">{{ goal["goal_type"] }}</text>
</view>
<view class="goal-info">
<text class="goal-name">{{ goal["goal_type"] }}</text>
<text class="goal-desc">{{ goal.getString("description") }}</text>
</view>
<view class="goal-status" :class="getGoalStatusClass(goal.getString('status')??'')">
<text class="status-text">{{ getGoalStatusText(goal.getString('status')??"") }}</text>
</view>
</view>
<view class="goal-progress">
<view class="progress-info">
<text class="progress-text">
{{ goal.current_value ?? 0 }} / {{ goal.target_value }} {{ goal.unit ?? '' }}
</text>
<text class="progress-percent">{{ getProgressPercent(goal) }}%</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: getProgressPercent(goal) + '%' }"></view>
</view>
</view>
<view class="goal-meta">
<text class="goal-date">目标日期: {{ getGoalTargetDate(goal) }}</text>
<text class="goal-priority">优先级: {{ getGoalPriorityText(goal) }}</text>
</view>
</view>
</view>
</view>
<!-- Goal Templates -->
<view class="templates-section">
<view class="section-title">目标模板</view>
<view class="templates-grid">
<view v-for="template in goalTemplates" :key="template.type"
class="template-item" @click="createFromTemplate(template)">
<view class="template-icon">
<text class="template-emoji">{{ template.icon }}</text>
</view>
<text class="template-name">{{ template.name }}</text>
<text class="template-desc">{{ template.description }}</text>
</view>
</view>
</view>
<!-- Statistics -->
<view class="stats-section">
<view class="section-title">目标统计</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-number">{{ completedGoals }}</text>
<text class="stat-label">已完成</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ activeGoals }}</text>
<text class="stat-label">进行中</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ getAverageProgress() }}%</text>
<text class="stat-label">平均进度</text>
</view>
</view>
</view>
</scroll-view>
<!-- Add/Edit Goal Modal -->
<view v-if="showGoalModal" class="modal-overlay" @click="closeGoalModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ editingGoal != null ? '编辑目标' : '添加目标' }}</text>
<button @click="closeGoalModal" class="modal-close" type="button">
<simple-icon type="x" :size="20" color="#666" />
</button>
</view>
<view class="modal-body">
<view class="form-group">
<text class="form-label">目标类型</text>
<view @click="showGoalTypePicker = true" class="picker-input">
<text>{{ goalTypeOptions[goalTypeIndex] ?? '请选择目标类型' }}</text>
<simple-icon type="chevron-down" :size="16" />
</view>
<view v-if="showGoalTypePicker" class="picker-view-modal">
<picker-view :value="[goalTypeIndex]" :indicator-style="'height: 40px;'" @change="onGoalTypePickerChange">
<picker-view-column>
<view v-for="(item, idx) in goalTypeOptions" :key="idx" class="picker-view-item">{{ item }}</view>
</picker-view-column>
</picker-view>
<view class="picker-view-actions">
<button @click="showGoalTypePicker = false">取消</button>
<button @click="confirmGoalTypePicker">确定</button>
</view>
</view>
</view>
<view class="form-group">
<text class="form-label">目标数值</text>
<input :value="goalForm.target_value" type="number"
class="form-input" placeholder="请输入目标数值" />
</view>
<view class="form-group">
<text class="form-label">单位</text>
<input :value="goalForm.unit" type="text"
class="form-input" placeholder="如: kg, 次, 分钟" />
</view>
<view class="form-group">
<text class="form-label">目标日期</text>
<input :value="goalForm.target_date" type="date" class="form-input" placeholder="请选择目标日期" />
</view>
<view class="form-group">
<text class="form-label">优先级</text>
<view @click="showPriorityPicker = true" class="picker-input">
<text>{{ priorityOptions[priorityIndex] ?? '请选择优先级' }}</text>
<simple-icon type="chevron-down" :size="16" color="#999" />
</view>
<view v-if="showPriorityPicker" class="picker-view-modal">
<picker-view :value="[priorityIndex]" :indicator-style="'height: 40px;'" @change="onPriorityPickerChange">
<picker-view-column>
<view v-for="(item, idx) in priorityOptions" :key="idx" class="picker-view-item">{{ item }}</view>
</picker-view-column>
</picker-view>
<view class="picker-view-actions">
<button @click="showPriorityPicker = false">取消</button>
<button @click="confirmPriorityPicker">确定</button>
</view>
</view>
</view>
<view class="form-group">
<text class="form-label">描述</text>
<textarea :value="goalForm.description" class="form-textarea"
placeholder="描述你的目标..." maxlength="200"></textarea>
</view>
</view>
<view class="modal-footer">
<button @click="closeGoalModal" class="cancel-btn">取消</button>
<button @click="saveGoal" class="save-btn" :disabled="!isFormValid">保存</button>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { onLoad,onResize } from '@dcloudio/uni-app'
import { formatDate } from '../types.uts'
import { getCurrentUserId } from '@/utils/store.uts'
import supaClient from '@/components/supadb/aksupainstance.uts'
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 loading = ref(true)
const goals = ref<Array<UTSJSONObject>>([])
const showGoalModal = ref(false)
const editingGoal = ref<UTSJSONObject | null>(null)
const contentHeight = ref(0)
// 表单数据
const goalForm = ref<UTSJSONObject>({
goal_type: '',
target_value: '',
unit: '',
target_date: '',
priority: 1,
description: ''
})
// 选择器数据
const goalTypeIndex = ref(0)
const priorityIndex = ref(0)
// Add missing picker-view state refs for Android compatibility
const tempGoalTypeIndex = ref(0)
const tempPriorityIndex = ref(0)
const showGoalTypePicker = ref(false)
const showPriorityPicker = ref(false)
const goalTypeOptions = ['减肥', '增肌', '耐力提升', '柔韧性', '力量增强', '技能提升']
const goalTypes = ['weight_loss', 'muscle_gain', 'endurance', 'flexibility', 'strength', 'skill']
const priorityOptions = ['低', '一般', '中等', '较高', '最高']
// 目标模板
const goalTemplates = ref<Array<UTSJSONObject>>([
{
type: 'weight_loss',
name: '减重目标',
description: '设定理想体重目标',
icon: '⚖️',
defaultValue: 5,
unit: 'kg'
},
{
type: 'muscle_gain',
name: '增肌目标',
description: '增加肌肉量',
icon: '',
defaultValue: 3,
unit: 'kg'
},
{
type: 'endurance',
name: '耐力提升',
description: '提高有氧耐力',
icon: '',
defaultValue: 30,
unit: '分钟'
},
{
type: 'strength',
name: '力量增强',
description: '提升最大力量',
icon: '',
defaultValue: 20,
unit: 'kg'
}
])
// 计算属性
const completedGoals = computed(() => {
return goals.value.filter(goal => goal.get('status') === 'completed').length
})
const activeGoals = computed(() => {
return goals.value.filter(goal => goal.get('status') === 'active').length
})
const isFormValid = computed(() => {
return (goalForm.value['goal_type'] as string) !== '' &&
(goalForm.value['target_value'] as string) !== '' &&
(goalForm.value['unit'] as string) !== '' &&
(goalForm.value['target_date'] as string) !== ''
})
// 计算内容高度
const calculateContentHeight = () => {
const systemInfo = uni.getSystemInfoSync()
const windowHeight = systemInfo.windowHeight
const headerHeight = 60
contentHeight.value = windowHeight - headerHeight
}
// 加载目标数据
const loadGoals = async () => {
try {
loading.value = true
if ((userId.value as string) === '') {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
const result = await supaClient
.from('ak_user_training_goals')
.select('*', {})
.eq('user_id', userId.value)
.order('created_at', { ascending: false })
.execute()
if (result.error == null && result.data != null) {
goals.value = result.data as UTSJSONObject[]
}
} catch (error) {
console.error('加载目标失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 生命周期
onMounted(() => {
calculateContentHeight()
loadGoals()
})
onLoad((options: OnLoadOptions) => {
userId.value = options['id'] ?? getCurrentUserId()
loadGoals()
})
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
// 重置表单
function resetForm() {
goalForm.value = {
goal_type: '',
target_value: '',
unit: '',
target_date: '',
priority: 1,
description: ''
}
goalTypeIndex.value = 0
priorityIndex.value = 0
}
// 填充表单
function populateForm(goal: UTSJSONObject) {
goalForm.value = {
goal_type: goal.get('goal_type') as string,
target_value: goal.get('target_value') as string,
unit: goal.get('unit') as string,
target_date: goal.get('target_date') as string,
priority: goal.get('priority') != null ? goal.get('priority') as number : 1,
description: goal.get('description') as string
}
const form = goalForm.value
const typeIndex = goalTypes.indexOf(form['goal_type'])
goalTypeIndex.value = typeIndex >= 0 ? typeIndex : 0
const priorityNum = parseInt(form['priority'] != null ? form['priority'].toString() : '1')
priorityIndex.value = isNaN(priorityNum) ? 0 : priorityNum - 1
}
// 添加目标
const addGoal = () => {
editingGoal.value = null
resetForm()
showGoalModal.value = true
}
// 编辑目标
const editGoal = (goal: UTSJSONObject) => {
editingGoal.value = goal
populateForm(goal)
showGoalModal.value = true
}
// 关闭模态框
const closeGoalModal = () => {
showGoalModal.value = false
editingGoal.value = null
}
// 保存目标
const saveGoal = async () => {
try {
const goalData = {
user_id: userId.value,
goal_type: goalTypes[goalTypeIndex.value],
target_value: parseFloat(goalForm.value['target_value'] != null ? goalForm.value['target_value'].toString() : '0'),
unit: goalForm.value['unit'],
target_date: goalForm.value['target_date'],
priority: priorityIndex.value + 1,
description: goalForm.value['description'],
status: 'active'
}
let result: any
const currentEditingGoal = editingGoal.value
if (currentEditingGoal != null) {
// 更新
result = await supaClient
.from('ak_user_training_goals')
.update(goalData)
.eq('id', (currentEditingGoal.get('id') ?? '').toString())
.execute()
} else {
// 创建
result = await supaClient
.from('ak_user_training_goals')
.insert(goalData)
.execute()
}
if (result.error != null) {
throw new Error(result.error?.message ?? '未知错误')
}
uni.showToast({
title: editingGoal.value != null ? '更新成功' : '创建成功',
icon: 'success'
})
closeGoalModal()
loadGoals()
} catch (error) {
console.error('保存目标失败:', error)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
// 从模板创建目标
const createFromTemplate = (template: UTSJSONObject) => {
editingGoal.value = null
resetForm()
// Use bracket notation for UTS compatibility
goalForm.value['goal_type'] = template.get('type') as string
goalForm.value['target_value'] = template.get('defaultValue') as string
goalForm.value['unit'] = template.get('unit') as string
const typeIndex = goalTypes.indexOf(goalForm.value['goal_type'])
goalTypeIndex.value = typeIndex >= 0 ? typeIndex : 0
// 设置默认目标日期为3个月后
const targetDate = new Date()
targetDate.setMonth(targetDate.getMonth() + 3)
goalForm.value['target_date'] = targetDate.toISOString().split('T')[0]
showGoalModal.value = true
}
// 事件处理
const onGoalTypePickerChange = (e: UniPickerViewChangeEvent) => {
tempGoalTypeIndex.value = e.detail.value[0]
}
const confirmGoalTypePicker = () => {
goalTypeIndex.value = tempGoalTypeIndex.value
goalForm.value['goal_type'] = goalTypes[goalTypeIndex.value]
showGoalTypePicker.value = false
}
const onPriorityPickerChange = (e: UniPickerViewChangeEvent) => {
tempPriorityIndex.value = e.detail.value[0]
}
const confirmPriorityPicker = () => {
priorityIndex.value = tempPriorityIndex.value
goalForm.value['priority'] = priorityIndex.value + 1
showPriorityPicker.value = false
}
// 工具函数
const getGoalIcon = (goalType: string): string => {
const icons = {
'weight_loss': '⚖️',
'muscle_gain': '',
'endurance': '',
'flexibility': '',
'strength': '',
'skill': ''
}
return icons[goalType] ?? ''
}
const getGoalTypeName = (goalType: string): string => {
const names = {
'weight_loss': '减肥目标',
'muscle_gain': '增肌目标',
'endurance': '耐力提升',
'flexibility': '柔韧性',
'strength': '力量增强',
'skill': '技能提升'
}
return names[goalType] ?? '未知目标'
}
const getGoalStatusText = (status: string): string => {
const statusTexts = {
'active': '进行中',
'paused': '已暂停',
'completed': '已完成',
'cancelled': '已取消'
}
const result =statusTexts[status]
return result!=null ? result.toString() :'未知'
}
const getGoalStatusClass = (status: string): string => {
return `status-${status}`
}
const getPriorityText = (priority: number): string => {
const priorities = ['低', '一般', '中等', '较高', '最高']
return priorities[priority - 1] ?? '一般'
}
const getProgressPercent = (goal: UTSJSONObject): number => {
const current = goal.getNumber('current_value') ?? 0
const target = goal.getNumber('target_value') ?? 1
return Math.min(Math.round((current / target) * 100), 100)
}
const getAverageProgress = (): number => {
if (goals.value.length === 0) return 0
const totalProgress = goals.value.reduce((sum, goal) => sum + getProgressPercent(goal), 0)
return Math.round(totalProgress / goals.value.length)
}
// Add a helper to safely get description as string
function getGoalDescription(desc: string): string {
if (desc == null) return '暂无描述'
if (typeof desc === 'string') {
const trimmed = desc.trim()
return trimmed !== '' ? trimmed : '暂无描述'
}
return '暂无描述'
}
// Helper to safely get priority text from goal object
function getGoalPriorityText(goal: UTSJSONObject): string {
const raw = goal.get('priority')
let num = 1
if (typeof raw === 'number') {
num = raw
} else if (typeof raw === 'string') {
const parsed = parseInt(raw)
num = isNaN(parsed) ? 1 : parsed
}
return getPriorityText(num)
}
// Helper to safely get formatted date from goal object
function getGoalTargetDate(goal: UTSJSONObject): string {
const raw = goal.get('target_date')
const dateStr = (raw != null) ? raw.toString() : ''
// UTS: formatDate is always a function, call directly
return formatDate(dateStr, 'YYYY-MM-DD')
}
// Lifecycle hooks
onMounted(() => {
screenWidth.value = uni.getSystemInfoSync().windowWidth
userId.value = getCurrentUserId()
loadGoals()
})
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
</script>
<style scoped>
.goal-settings-page {
flex: 1;
background-color: #f5f5f5;
}
.header { height: 60px;
background-image: linear-gradient(to top right, #4CAF50, #45a049);
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.header-left {
flex-direction: row;
align-items: center;
flex: 1;
}
.back-btn, .add-btn {
background-color: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 20px;
padding: 8px 12px;
flex-direction: row;
align-items: center;
gap: 4px;
}
.back-btn {
margin-right: 12px;
}
.back-btn text, .add-btn text {
color: #FFFFFF;
font-size: 14px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #FFFFFF;
}
.content {
flex: 1;
padding: 16px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
.goals-section {
margin-bottom: 24px;
}
.empty-state {
background-color: #FFFFFF;
border-radius: 12px;
padding: 40px 20px;
align-items: center;
text-align: center;
}
.empty-text {
font-size: 16px;
color: #666;
margin: 12px 0 4px;
}
.empty-desc {
font-size: 14px;
color: #999;
margin-bottom: 20px;
}
.add-goal-btn {
background-color: #4CAF50;
color: #FFFFFF;
border: none;
border-radius: 20px;
padding: 12px 24px;
font-size: 14px;
}
.goals-list {
gap: 12px;
}
.goal-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
}
.goal-header {
flex-direction: row;
align-items: center;
margin-bottom: 12px;
}
.goal-icon {
width: 40px;
height: 40px;
background-color: #f0f0f0;
border-radius: 20px;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.goal-emoji {
font-size: 20px;
}
.goal-info {
flex: 1;
}
.goal-name {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.goal-desc {
font-size: 12px;
color: #666;
}
.goal-status {
padding: 4px 8px;
border-radius: 12px;
align-items: center;
}
.status-active {
background-color: #E8F5E8;
}
.status-completed {
background-color: #E3F2FD;
}
.status-paused {
background-color: #FFF3E0;
}
.status-cancelled {
background-color: #FFEBEE;
}
.status-text {
font-size: 12px;
color: #333;
}
.goal-progress {
margin-bottom: 12px;
}
.progress-info {
flex-direction: row;
justify-content: space-between;
margin-bottom: 8px;
}
.progress-text {
font-size: 14px;
color: #666;
}
.progress-percent {
font-size: 14px;
font-weight: bold;
color: #4CAF50;
}
.progress-bar {
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #4CAF50;
border-radius: 4px;
}
.goal-meta {
flex-direction: row;
justify-content: space-between;
}
.goal-date, .goal-priority {
font-size: 12px;
color: #999;
}
.templates-section {
margin-bottom: 24px;
}
.templates-grid {
flex-direction: row;
flex-wrap: wrap;
gap: 12px;
}
.template-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
width: calc(50% - 6px);
align-items: center;
text-align: center;
}
.template-icon {
width: 40px;
height: 40px;
justify-content: center;
align-items: center;
margin-bottom: 8px;
}
.template-emoji {
font-size: 24px;
}
.template-name {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.template-desc {
font-size: 12px;
color: #666;
}
.stats-section {
margin-bottom: 24px;
}
.stats-grid {
flex-direction: row;
gap: 12px;
}
.stat-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
flex: 1;
align-items: center;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #4CAF50;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: #FFFFFF;
border-radius: 12px;
width: 80%;
max-height: 80%;
}
.modal-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.modal-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.modal-close {
background: transparent;
border: none;
padding: 4px;
}
.modal-body {
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
font-size: 14px;
color: #333;
margin-bottom: 8px;
display: block;
}
.form-input, .form-textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
}
.form-textarea {
height: 80px;
text-align: start;
}
.picker-input {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #FFFFFF;
}
.modal-footer {
flex-direction: row;
justify-content: flex-end;
gap: 12px;
padding: 16px;
border-top: 1px solid #f0f0f0;
}
.cancel-btn, .save-btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
border: none;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
}
.save-btn:disabled {
background-color: #cccccc;
color: #999999;
}
.loading-container {
flex: 1;
justify-content: center;
align-items: center;
}
.loading-text {
font-size: 16px;
color: #666;
}
</style>

View File

@@ -0,0 +1,151 @@
<!-- 手环位置功能测试页面 -->
<template>
<view class="test-page">
<text class="test-title">手环位置功能测试</text>
<view class="test-section">
<text class="section-title">基本功能测试</text>
<button class="test-btn" @click="testGetCurrentLocation">测试获取当前位置</button>
<button class="test-btn" @click="testGetBaseStations">测试获取基站列表</button>
<button class="test-btn" @click="testGetFences">测试获取围栏列表</button>
<button class="test-btn" @click="testGetLocationHistory">测试获取位置历史</button>
<button class="test-btn" @click="testGetFenceEvents">测试获取围栏事件</button>
</view>
<view class="test-results">
<text class="results-title">测试结果:</text>
<scroll-view class="results-scroll" scroll-y="true">
<text class="result-text">{{ testResults }}</text>
</scroll-view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { LocationService } from '@/utils/locationService.uts'
const testResults = ref<string>('等待测试...\n')
const addResult = (text: string) => {
const now = new Date()
const h = now.getHours().toString().padStart(2, '0')
const m = now.getMinutes().toString().padStart(2, '0')
const s = now.getSeconds().toString().padStart(2, '0')
const timestamp = `${h}:${m}:${s}`
testResults.value += `[${timestamp}] ${text}\n`
}
const testGetCurrentLocation = async () => {
addResult('开始测试获取当前位置...')
try {
const result = await LocationService.getCurrentLocation('device_test')
addResult(`获取位置结果: ${JSON.stringify(result, null, 2)}`)
} catch (error) {
addResult(`获取位置失败: ${error}`)
}
}
const testGetBaseStations = async () => {
addResult('开始测试获取基站列表...')
try {
const result = await LocationService.getBaseStations()
addResult(`获取基站结果: ${JSON.stringify(result, null, 2)}`)
} catch (error) {
addResult(`获取基站失败: ${error}`)
}
}
const testGetFences = async () => {
addResult('开始测试获取围栏列表...')
try {
const result = await LocationService.getFences('device_test')
addResult(`获取围栏结果: ${JSON.stringify(result, null, 2)}`)
} catch (error) {
addResult(`获取围栏失败: ${error}`)
}
}
const testGetLocationHistory = async () => {
addResult('开始测试获取位置历史...')
try {
const endDate = new Date().toISOString()
const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
const result = await LocationService.getLocationHistory('device_test', startDate, endDate)
addResult(`获取历史结果: ${JSON.stringify(result, null, 2)}`)
} catch (error) {
addResult(`获取历史失败: ${error}`)
}
}
const testGetFenceEvents = async () => {
addResult('开始测试获取围栏事件...')
try {
const result = await LocationService.getFenceEvents('device_test', 10)
addResult(`获取事件结果: ${JSON.stringify(result, null, 2)}`)
} catch (error) {
addResult(`获取事件失败: ${error}`)
}
}
</script>
<style>
.test-page {
padding: 32rpx;
}
.test-title {
font-size: 48rpx;
font-weight: bold;
color: #333333;
margin-bottom: 32rpx;
text-align: center;
}
.test-section {
margin-bottom: 32rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 24rpx;
}
.test-btn {
width: 100%;
height: 80rpx;
background: #007aff;
color: #ffffff;
border: none;
border-radius: 12rpx;
margin-bottom: 16rpx;
font-size: 28rpx;
}
.test-results {
flex: 1;
}
.results-title {
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin-bottom: 16rpx;
}
.results-scroll {
height: 400rpx;
background: #f5f5f5;
border-radius: 12rpx;
padding: 16rpx;
}
.result-text {
font-size: 24rpx;
color: #666666;
line-height: 1.5;
white-space: pre-wrap;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,900 @@
<template>
<scroll-view class="profile-page" direction="vertical">
<!-- 头部背景 -->
<view class="profile-header">
<view class="header-bg"></view>
<!-- 用户信息 -->
<view class="user-info">
<view class="avatar-container">
<image class="avatar" :src="userAvatar" mode="aspectFill" @click="changeAvatar"></image>
<view class="avatar-edit">
<text class="edit-icon"></text>
</view>
</view>
<view class="user-details">
<text class="username">{{ getUserName() }}</text>
<text class="user-id">学号:{{ getUserId() }}</text>
<text class="join-date">加入时间:{{ getJoinDate() }}</text>
</view>
</view>
<!-- 等级信息 -->
<view class="level-info">
<view class="level-badge">
<text class="level-text">Lv.{{ getUserLevel() }}</text>
</view>
<view class="xp-info">
<text class="xp-text">{{ getCurrentXP() }} / {{ getNextLevelXP() }} XP</text>
<view class="xp-bar">
<view class="xp-progress" :style="{ width: getXPProgress() + '%' }"></view>
</view>
</view>
</view>
</view>
<!-- 统计卡片 -->
<view class="stats-grid">
<view class="stat-item">
<text class="stat-number">{{ totalTrainingDays }}</text>
<text class="stat-label">训练天数</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ completedAssignments }}</text>
<text class="stat-label">完成作业</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ totalTrainingTime }}</text>
<text class="stat-label">训练时长(小时)</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ achievementCount }}</text>
<text class="stat-label">获得成就</text>
</view>
</view>
<!-- 功能菜单 */
<view class="menu-section">
<view class="section-title">个人设置</view>
<view class="menu-list">
<view class="menu-item" @click="editProfile">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">编辑资料</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="changePassword">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">修改密码</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="notificationSettings">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">通知设置</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="privacySettings">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">隐私设置</text>
</view>
<text class="menu-arrow">></text>
</view>
</view>
</view>
<!-- 训练偏好 -->
<view class="menu-section">
<view class="section-title">训练偏好</view>
<view class="menu-list">
<view class="menu-item" @click="goalSettings">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">训练目标</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="reminderSettings">
<view class="menu-left">
<text class="menu-icon">⏰</text>
<text class="menu-title">训练提醒</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="favoriteExercises">
<view class="menu-left">
<text class="menu-icon">❤️</text>
<text class="menu-title">喜欢的运动</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="preferencesAnalytics">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">偏好分析</text>
</view>
<text class="menu-arrow">></text>
</view>
</view>
</view>
<!-- 数据管理 -->
<view class="menu-section">
<view class="section-title">数据管理</view>
<view class="menu-list">
<view class="menu-item" @click="deviceManagement">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">设备管理</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="exportData">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">导出数据</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="backupData">
<view class="menu-left">
<text class="menu-icon">☁️</text>
<text class="menu-title">数据备份</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="clearCache">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">清除缓存</text>
</view>
<text class="menu-arrow">></text>
</view>
</view>
</view>
<!-- 关于 -->
<view class="menu-section">
<view class="section-title">关于</view>
<view class="menu-list">
<view class="menu-item" @click="helpCenter">
<view class="menu-left">
<text class="menu-icon">❓</text>
<text class="menu-title">帮助中心</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="feedback">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">意见反馈</text>
</view>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="aboutApp">
<view class="menu-left">
<text class="menu-icon"></text>
<text class="menu-title">关于应用</text>
</view>
<text class="menu-arrow">></text>
</view>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-section">
<button class="logout-btn" @click="logout">退出登录</button>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed, onUnmounted } from 'vue'
import { onLoad, onResize } from '@dcloudio/uni-app'
import { formatDateTime } from '../types.uts'
import supaClient from '@/components/supadb/aksupainstance.uts'
import { state, getCurrentUserId, setUserProfile } from '@/utils/store.uts'
import type { UserProfile } from '@/pages/user/types.uts'
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 userInfo = ref<UTSJSONObject | null>(null)
const userAvatar = ref<string>('/static/default-avatar.png')
const totalTrainingDays = ref<number>(0)
const completedAssignments = ref<number>(0)
const totalTrainingTime = ref<number>(0)
const achievementCount = ref<number>(0)
const getUserName = () : string => {
const profile = state.userProfile
if (profile != null) {
const username = profile.username
if (typeof username === 'string' && username.length > 0) {
return username
}
}
if (userInfo.value == null) {
return '未登录'
}
return (userInfo.value as UTSJSONObject).getString('username') ?? '未设置'
}
const getUserId = () : string => {
if (typeof userId.value === 'string' && userId.value.length > 0) {
return userId.value
}
if (userInfo.value == null) {
return '---'
}
return (userInfo.value as UTSJSONObject).getString('id') ?? '未设置'
}
const getJoinDate = () : string => {
if (userInfo.value == null) return '---'
const joinDate = (userInfo.value as UTSJSONObject).getString('created_at') ?? ''
return joinDate.length > 0 ? formatDateTime(joinDate).split(' ')[0] : '未知'
}
const getUserLevel = () : number => {
if (userInfo.value == null) return 1
return (userInfo.value as UTSJSONObject).getNumber('level') ?? 1
}
const getCurrentXP = () : number => {
if (userInfo.value == null) return 0
return (userInfo.value as UTSJSONObject).getNumber('current_xp') ?? 0
}
const getNextLevelXP = () : number => {
const level = getUserLevel()
return level * 1000 // 每级需要1000XP
}
const getXPProgress = () : number => {
const current = getCurrentXP()
const next = getNextLevelXP()
const levelBase = (getUserLevel() - 1) * 1000
const levelCurrent = current - levelBase
const levelRequired = next - levelBase
return levelRequired > 0 ? (levelCurrent / levelRequired) * 100 : 0
} // 加载用户资料 - 优先使用 state.userProfile然后同步数据库
// 加载用户统计数据
const loadUserStats = async () => {
try { // 获取训练记录统计
const recordsResult = await supaClient
.from('ak_training_records')
.select('*', {})
.eq('user_id', userId.value)
.execute()
if (recordsResult != null && recordsResult.error == null && recordsResult.status == 200) {
console.log(recordsResult)
const records = recordsResult.data as UTSJSONObject[]
// 计算训练天数(去重日期)
const trainingDates = new Set<string>()
let totalMinutes = 0
records.forEach(record => {
const createdAt = (record as UTSJSONObject).getString('created_at') ?? ''
if (createdAt.length > 0) {
const date = new Date(createdAt).toDateString()
trainingDates.add(date)
}
const duration = (record as UTSJSONObject).getNumber('duration') ?? 0
totalMinutes += duration
})
totalTrainingDays.value = trainingDates.size
totalTrainingTime.value = Math.round(totalMinutes / 60)
}
else
{
console.log(recordsResult)
}
// 获取完成的作业数量
const assignmentsResult = await supaClient
.from('ak_assignments')
.select('*', {})
.eq('status', 'completed')
.execute()
if (assignmentsResult != null && assignmentsResult.error == null) {
const data = assignmentsResult.data as UTSJSONObject[]
completedAssignments.value = data.length
}
// 获取成就数量 - 使用高分作业作为成就代理
const achievementsResult = await supaClient
.from('ak_assignment_submissions')
.select('*', {})
.eq('student_id', userId.value)
.gte('final_score', 90) // 90分以上的作业算作成就
.execute()
if (achievementsResult != null && achievementsResult.error == null) {
const data = achievementsResult.data as UTSJSONObject[]
achievementCount.value = data.length
}
} catch (error) {
console.log(userId.value)
console.error('加载用户统计失败:', error)
// 使用模拟数据
totalTrainingDays.value = 45
completedAssignments.value = 28
totalTrainingTime.value = 67
achievementCount.value = 12
}
} // 更换头像 - 同时更新 state 和数据库
const changeAvatar = () => {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: function (res) {
const tempFilePath = res.tempFilePaths[0]
// 这里应该上传头像到服务器
userAvatar.value = tempFilePath // 更新 state.userProfile 中的头像 - 使用本地变量避免smart cast问题
const currentProfile = state.userProfile
if (currentProfile != null && typeof currentProfile.id === 'string' && (currentProfile.id?.length ?? 0) > 0) {
const updatedProfile : UserProfile = {
id: currentProfile.id,
username: currentProfile.username,
email: currentProfile.email,
gender: currentProfile.gender,
birthday: currentProfile.birthday,
height_cm: currentProfile.height_cm,
weight_kg: currentProfile.weight_kg,
bio: currentProfile.bio,
avatar_url: tempFilePath,
preferred_language: currentProfile.preferred_language,
role: currentProfile.role
}
setUserProfile(updatedProfile)
}// 更新数据库如果用户ID有效
const currentUserId = getCurrentUserId()
if (currentUserId.length > 0 && currentUserId !== 'demo_user_id') {
// 使用异步调用但不等待结果
supaClient
.from('ak_users')
.update({ avatar_url: tempFilePath })
.eq('id', currentUserId)
.execute()
.then(() => {
console.log('头像更新成功')
})
.catch((error) => {
console.error('更新头像失败:', error)
})
}
uni.showToast({
title: '头像更新成功',
icon: 'success'
})
}
})
}
// 编辑资料
const editProfile = () => {
uni.showModal({
title: '编辑资料',
content: '功能开发中...',
showCancel: false
})
}
// 修改密码
const changePassword = () => {
uni.showModal({
title: '修改密码',
content: '功能开发中...',
showCancel: false
})
}
// 通知设置
const notificationSettings = () => {
uni.showModal({
title: '通知设置',
content: '功能开发中...',
showCancel: false
})
}
// 隐私设置
const privacySettings = () => {
uni.showModal({
title: '隐私设置',
content: '功能开发中...',
showCancel: false
})
}
// 目标设置
const goalSettings = () => {
uni.navigateTo({
url: '/pages/sport/student/goal-settings'
})
}
// 提醒设置
const reminderSettings = () => {
uni.navigateTo({
url: '/pages/sport/student/reminder-settings'
})
}
// 喜欢的运动
const favoriteExercises = () => {
uni.navigateTo({
url: '/pages/sport/student/favorite-exercises'
})
}
// 偏好分析
const preferencesAnalytics = () => {
uni.navigateTo({
url: '/pages/sport/student/preferences-analytics'
})
}
// 设备管理
const deviceManagement = () => {
uni.navigateTo({
url: '/pages/sport/student/device-management'
})
}
// 导出数据
const exportData = () => {
uni.showLoading({
title: '导出中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '导出完成',
icon: 'success'
})
}, 2000)
}
// 备份数据
const backupData = () => {
uni.showLoading({
title: '备份中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '备份完成',
icon: 'success'
})
}, 1500)
}
// 清除缓存
const clearCache = () => {
uni.showModal({
title: '清除缓存',
content: '确定要清除所有缓存数据吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '清除中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '缓存已清除',
icon: 'success'
})
}, 1000)
}
}
})
}
// 帮助中心
const helpCenter = () => {
uni.showModal({
title: '帮助中心',
content: '功能开发中...',
showCancel: false
})
}
// 意见反馈
const feedback = () => {
uni.showModal({
title: '意见反馈',
content: '功能开发中...',
showCancel: false
})
}
// 关于应用
const aboutApp = () => {
uni.showModal({
title: '关于应用',
content: 'AI监测系统 v1.0.0\n\n 高性能AI监测管理平台',
showCancel: false
})
}
const loadUserProfile = async () => {
try {
// 如果 state.userProfile 存在,先使用它填充界面
const profile = state.userProfile
if (profile != null && typeof profile.id === 'string' && (profile.id?.length ?? 0) > 0) {
userInfo.value = {
id: profile.id,
username: profile.username != null ? profile.username : '',
email: profile.email != null ? profile.email : '',
avatar_url: profile.avatar_url != null ? profile.avatar_url : '',
created_at: new Date().toISOString() // 临时值
} as UTSJSONObject
const avatarUrl = profile.avatar_url
if (typeof avatarUrl === 'string' && (avatarUrl?.length ?? 0) > 0) {
userAvatar.value = avatarUrl as string
}
}
// 然后从数据库获取最新数据并更新 state
if (typeof userId.value === 'string' && userId.value.length > 0 && userId.value !== 'demo_user_id') {
const result = await supaClient
.from('ak_users')
.select('*', {})
.eq('id', userId.value)
.single()
.execute()
if (result != null && result.error == null && result.data != null) {
const res_data = result.data as UTSJSONObject[]
const resultData = res_data[0]
userInfo.value = resultData
const avatar = (userInfo.value as UTSJSONObject).getString('avatar_url') ?? ''
if (avatar.length > 0) {
userAvatar.value = avatar
}
// 更新 state.userProfile
const updatedProfile : UserProfile = {
id: resultData.getString('id') ?? '',
username: resultData.getString('username') ?? '',
email: resultData.getString('email') ?? '',
gender: resultData.getString('gender'),
birthday: resultData.getString('birthday'),
height_cm: resultData.getNumber('height_cm'),
weight_kg: resultData.getNumber('weight_kg'),
bio: resultData.getString('bio'),
avatar_url: resultData.getString('avatar_url'),
preferred_language: resultData.getString('preferred_language'),
role: resultData.getString('role')
}
setUserProfile(updatedProfile)
}
}
loadUserStats()
} catch (error) {
console.error('加载用户资料失败:', error)
}
}
// 退出登录 - 清空 state 和重定向
const logout = () => {
uni.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
success: function (res) {
if (res.confirm) {
// 使用 Promise 包装异步操作
supaClient.signOut()
.then(() => {
// 清空 state
const emptyProfile : UserProfile = { username: '', email: '' }
setUserProfile(emptyProfile)
uni.reLaunch({
url: '/pages/index/index'
})
})
.catch((error) => {
console.error('退出登录失败:', error)
uni.showToast({
title: '退出失败',
icon: 'none'
})
})
}
}
})
}
// 生命周期
onLoad((options : OnLoadOptions) => {
userId.value = options['id'] ?? ''
if (userId.value.length === 0) {
userId.value = getCurrentUserId()
}
loadUserProfile()
})
onMounted(() => {
// Initialize screen width
screenWidth.value = uni.getSystemInfoSync().windowWidth
})
// Handle resize events for responsive design
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
</script>
<style>
.profile-page {
flex: 1;
background: #f5f6fa;
}
/* 头部区域 */
.profile-header {
position: relative;
padding: 60rpx 40rpx 40rpx;
margin-bottom: 20rpx;
}
.header-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: linear-gradient(to right, #667eea, #764ba2);
border-radius: 0 0 40rpx 40rpx;
}
.user-info {
position: relative;
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.avatar-container {
position: relative;
margin-right: 30rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
}
.avatar-edit {
position: absolute;
bottom: 0;
right: 0;
width: 40rpx;
height: 40rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.edit-icon {
font-size: 20rpx;
}
.user-details {
flex: 1;
}
.username {
font-size: 36rpx;
font-weight: bold;
color: white;
margin-bottom: 8rpx;
}
.user-id,
.join-date {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 4rpx;
}
.level-info {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
}
.level-badge {
background: rgba(255, 255, 255, 0.2);
padding: 10rpx 20rpx;
border-radius: 25rpx;
backdrop-filter: blur(10rpx);
margin-left: 20rpx;
}
.level-text {
color: white;
font-size: 24rpx;
font-weight: bold;
}
.xp-info {
flex: 1;
}
.xp-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 8rpx;
}
.xp-bar {
height: 8rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 4rpx;
overflow: hidden;
}
.xp-progress {
height: 100%;
background: white;
transition: width 0.3s ease;
}
/* 统计网格 */
.stats-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 20rpx 30rpx;
}
.stat-item {
background: white;
border-radius: 20rpx;
padding: 40rpx 30rpx;
text-align: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
margin-right: 20rpx;
margin-bottom: 20rpx;
width: 46%;
flex: 0 0 46%;
}
.stat-number {
font-size: 48rpx;
font-weight: bold;
color: #2c3e50;
line-height: 1;
margin-bottom: 10rpx;
}
.stat-label {
font-size: 24rpx;
color: #7f8c8d;
}
/* 菜单区域 */
.menu-section {
margin: 0 20rpx 30rpx;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #2c3e50;
margin-bottom: 15rpx;
padding-left: 10rpx;
}
.menu-list {
background: white;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.menu-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1px solid #f8f9fa;
transition: background-color 0.2s;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background-color: #f8f9fa;
}
.menu-left {
display: flex;
flex-direction: row;
align-items: center;
}
.menu-icon {
font-size: 32rpx;
width: 40rpx;
text-align: center;
margin-right: 20rpx;
}
.menu-title {
font-size: 30rpx;
color: #2c3e50;
}
.menu-arrow {
font-size: 28rpx;
color: #bdc3c7;
}
/* 退出登录 */
.logout-section {
margin: 40rpx 20rpx;
}
.logout-btn {
width: 100%;
padding: 30rpx;
background: #e74c3c;
color: white;
border: none;
border-radius: 20rpx;
font-size: 32rpx;
font-weight: bold;
transition: transform 0.3s ease, background-color 0.3s ease;
}
.logout-btn:active {
transform: scale(0.98);
background: #c0392b;
}
</style>

View File

@@ -0,0 +1,637 @@
<template>
<scroll-view direction="vertical" class="progress-container"> <!-- Header -->
<view class="header">
<view class="header-left">
<button v-if="fromStats" @click="goBack" class="back-btn">
<simple-icon type="arrow-left" :size="16" color="#FFFFFF" />
<text>返回</text>
</button>
<text class="title">{{ fromStats ? (studentName!='' ? `${studentName} - 学习进度` : '学生学习进度') : '学习进度' }}</text>
</view>
<view class="header-actions">
<button @click="refreshData" class="refresh-btn">
<simple-icon type="refresh" :size="16" color="#FFFFFF" />
<text>刷新</text>
</button>
</view>
</view>
<!-- Loading State -->
<view v-if="loading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<!-- Content -->
<scroll-view v-else class="content" direction="vertical" :style="{ height: contentHeight + 'px' }">
<!-- Overall Progress -->
<view class="progress-overview">
<text class="section-title">总体进度</text>
<view class="overall-stats">
<view class="stat-card">
<text class="stat-value">{{ overallProgress }}%</text>
<text class="stat-label">完成率</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ completedAssignments }}</text>
<text class="stat-label">已完成作业</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ totalTrainingTime }}</text>
<text class="stat-label">训练时长(分钟)</text>
</view>
</view>
</view> <!-- Progress by Subject -->
<view class="progress-by-subject">
<text class="section-title">各科目进度</text>
<view v-for="subject in subjectProgress" :key="subject.getString('id')" class="subject-item">
<view class="subject-header">
<text class="subject-name">{{ subject.getString('name') }}</text>
<text class="subject-percentage">{{ subject.getNumber('progress') }}%</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: (subject.getNumber('progress') ?? 0) + '%' }"></view>
</view>
<view class="subject-details">
<text class="detail-text">已完成:
{{ subject.getNumber('completed') }}/{{ subject.getNumber('total') }}</text>
<text class="detail-text">最近训练: {{ formatDate(subject.getString('lastTraining') ?? '') }}</text>
</view>
</view>
</view> <!-- Recent Activities -->
<view class="recent-activities">
<text class="section-title">最近活动</text>
<view v-for="activity in recentActivities" :key="activity.getString('id')" class="activity-item">
<view class="activity-icon">
<simple-icon :type="getActivityIcon(activity.getString('type') ?? 'info')" :size="20"
color="#4CAF50" />
</view>
<view class="activity-content">
<text class="activity-title">{{ activity.getString('title') }}</text>
<text class="activity-desc">{{ activity.getString('description') }}</text>
<text class="activity-time">{{ formatDateTime(activity.getString('time') ?? '') }}</text>
</view>
</view>
</view> <!-- Weekly Goals -->
<view class="weekly-goals">
<text class="section-title">本周目标</text>
<view v-for="goal in weeklyGoals" :key="goal.getString('id')" class="goal-item">
<view class="goal-header">
<text class="goal-name">{{ goal.getString('name') }}</text>
<text class="goal-status" :class="{ 'completed': isGoalCompleted(goal) }">
{{ isGoalCompleted(goal) ? '已完成' : '进行中' }}
</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: (goal.getNumber('progress') ?? 0) + '%' }"></view>
</view>
<text class="goal-desc">{{ goal.getString('description') }}</text>
</view>
</view>
</scroll-view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { onLoad, onResize } from '@dcloudio/uni-app'
import { state, getCurrentUserId } from '@/utils/store.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// 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 loading = ref(true)
const overallProgress = ref(0)
const completedAssignments = ref(0)
const totalTrainingTime = ref(0)
const subjectProgress = ref<UTSJSONObject[]>([])
const recentActivities = ref<UTSJSONObject[]>([])
const weeklyGoals = ref<UTSJSONObject[]>([])
const contentHeight = ref(0)
// Page parameters
const userId = ref('')
const studentName = ref('')
const fromStats = ref(false)// 生命周期
// Forward declaration for loadProgressData
let loadProgressData : () => Promise<void> = async () => {
}
// Handle page load with parameters
onLoad((options : OnLoadOptions) => {
// Get student ID from page parameters if provided
userId.value = options['id'] ?? ''
if (userId.value.length > 0) {
fromStats.value = true
}
studentName.value = decodeURIComponent(options['studentName'] ?? '') ?? ''
// Use setTimeout to avoid async function type mismatch
setTimeout(() => {
loadProgressData().then((res) => {
// Data loaded successfully
console.log(res)
})
}, 100)
})
// 计算内容高度
const calculateContentHeight = () => {
const systemInfo = uni.getSystemInfoSync()
const windowHeight = systemInfo.windowHeight
const headerHeight = 60
contentHeight.value = windowHeight - headerHeight
} // 加载进度数据
// 加载总体进度
const loadOverallProgress = async (studentId : string) => {
try {
// 获取作业完成情况
const assignmentsResult = await supa
.from('ak_student_assignments')
.select('status', {})
.eq('student_id', studentId)
.execute()
if (assignmentsResult.data != null) {
const assignments = assignmentsResult.data as UTSJSONObject[]
const completed = assignments.filter(a => ((a as UTSJSONObject).getString('status') ?? 'pending') === 'completed').length
completedAssignments.value = completed
overallProgress.value = assignments.length > 0 ? Math.round((completed / assignments.length) * 100) : 0
}// 获取训练时长
const trainingResult = await supa
.from('ak_training_records')
.select('duration', {})
.eq('student_id', studentId)
.execute()
if (trainingResult.data != null) {
const records = trainingResult.data as UTSJSONObject[]
totalTrainingTime.value = records.reduce((total, record) => {
return total + ((record as UTSJSONObject).getNumber('duration') ?? 0)
}, 0)
}
} catch (error) {
console.error('加载总体进度失败:', error)
}
}
// 加载各科目进度
const loadSubjectProgress = async (studentId : string) => {
try {
const result = await supa
.from('ak_training_projects')
.select(`
id, name,
training_records:ak_training_records(status),
assignments:ak_student_assignments(status)
`, {})
.execute()
if (result.data != null) {
const projects = result.data as UTSJSONObject[]
subjectProgress.value = projects.map(project => {
const records = (project as UTSJSONObject).getArray('training_records') as UTSJSONObject[] ?? []
const assignments = (project as UTSJSONObject).getArray('assignments') as UTSJSONObject[] ?? []
const completedRecords = records.filter(r => ((r as UTSJSONObject).getString('status') ?? 'pending') === 'completed').length
const completedAssignments = assignments.filter(a => ((a as UTSJSONObject).getString('status') ?? 'pending') === 'completed').length
const total = records.length + assignments.length
const completed = completedRecords + completedAssignments
const progressData = new UTSJSONObject()
progressData.set('id', (project as UTSJSONObject).getString('id') ?? '')
progressData.set('name', (project as UTSJSONObject).getString('name') ?? '')
progressData.set('progress', total > 0 ? Math.round((completed / total) * 100) : 0)
progressData.set('completed', completed)
progressData.set('total', total)
progressData.set('lastTraining', new Date().toISOString())
return progressData
})
}
} catch (error) {
console.error('加载科目进度失败:', error)
}
}
// 加载最近活动
const loadRecentActivities = async (studentId : string) => {
try {
const result = await supa
.from('ak_training_records')
.select('*, ak_training_projects(name)', {})
.eq('student_id', studentId)
.order('created_at', { ascending: false })
.limit(10)
.execute()
if (result.data != null) {
const records = result.data as UTSJSONObject[]
recentActivities.value = records.map(record => {
const activityData = new UTSJSONObject()
activityData.set('id', (record as UTSJSONObject).getString('id') ?? '')
activityData.set('type', 'training')
activityData.set('title', `完成训练: ${((record as UTSJSONObject).getAny('ak_training_projects') as UTSJSONObject ?? new UTSJSONObject()).getString('name') ?? '未知项目'}`)
activityData.set('description', `耗时: ${(record as UTSJSONObject).getNumber('duration') ?? 0}分钟`)
activityData.set('time', (record as UTSJSONObject).getString('created_at') ?? '')
return activityData
})
}
} catch (error) {
console.error('加载最近活动失败:', error)
}
}
// 加载周目标
const loadWeeklyGoals = async (studentId : string) => {
// 创建示例周目标
const goal1 = new UTSJSONObject()
goal1.set('id', '1')
goal1.set('name', '完成3次训练')
goal1.set('description', '本周完成至少3次训练课程')
goal1.set('progress', 67)
goal1.set('completed', false)
const goal2 = new UTSJSONObject()
goal2.set('id', '2')
goal2.set('name', '提交2份作业')
goal2.set('description', '按时提交本周的训练作业')
goal2.set('progress', 100)
goal2.set('completed', true)
const goal3 = new UTSJSONObject()
goal3.set('id', '3')
goal3.set('name', '训练时长达到120分钟')
goal3.set('description', '累计训练时长达到目标')
goal3.set('progress', 45)
goal3.set('completed', false)
weeklyGoals.value = [goal1, goal2, goal3]
} // 刷新数据
const refreshData = () => {
loadProgressData().then(() => {
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}).catch((error) => {
console.error('Refresh failed:', error)
})
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
// 格式化日期
const formatDate = (dateString : string) : string => {
try {
const date = new Date(dateString)
return `${date.getMonth() + 1}月${date.getDate()}日`
} catch {
return '暂无'
}
}
// 格式化日期时间
const formatDateTime = (dateString : string) : string => {
try {
const date = new Date(dateString)
return `${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
} catch {
return '暂无'
}
}
loadProgressData = async () => {
try {
loading.value = true
if (userId.value.length === 0) {
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none',
duration: 2000
})
setTimeout(() => {
uni.redirectTo({ url: '/pages/user/login' })
}, 1000)
return
}
// 序列化加载各种数据以避免Promise.all兼容性问题
await loadOverallProgress(userId.value)
await loadSubjectProgress(userId.value)
await loadRecentActivities(userId.value)
await loadWeeklyGoals(userId.value)
} catch (error) {
console.error('加载进度数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 获取活动图标
const getActivityIcon = (type : string) : string => {
switch (type) {
case 'training': return 'play'
case 'assignment': return 'edit'
case 'achievement': return 'trophy'
default: return 'info'
}
}
// 安全解包目标完成状态
const isGoalCompleted = (goal: UTSJSONObject): boolean => {
return goal.getBoolean('completed') === true
}
onMounted(() => {
screenWidth.value = uni.getSystemInfoSync().windowWidth
calculateContentHeight()
// Use setTimeout to avoid async function type mismatch
setTimeout(() => {
loadProgressData().then(() => {
// Data loaded successfully
}).catch((error) => {
console.error('Failed to load data:', error)
})
}, 100)
})
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
</script>
<style scoped>
.progress-container {
flex: 1;
background-color: #f5f5f5;
}
.header {
height: 60px;
background-image: linear-gradient(to top right, #4CAF50, #45a049);
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.header-left {
flex-direction: row;
align-items: center;
flex: 1;
}
.back-btn {
background-color: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 20px;
padding: 8px 12px;
flex-direction: row;
align-items: center;
margin-right: 12px;
}
.back-btn simple-icon {
margin-right: 4px;
}
.back-btn text {
color: #FFFFFF;
font-size: 14px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #FFFFFF;
}
.header-actions {
flex-direction: row;
align-items: center;
}
.refresh-btn {
background-color: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 20px;
padding: 8px 12px;
flex-direction: row;
align-items: center;
}
.refresh-btn simple-icon {
margin-right: 4px;
}
.refresh-btn text {
color: #FFFFFF;
font-size: 14px;
}
.loading-container {
flex: 1;
justify-content: center;
align-items: center;
}
.loading-text {
font-size: 16px;
color: #666;
}
.content {
flex: 1;
padding: 16px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
.progress-overview {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.overall-stats {
flex-direction: row;
justify-content: space-between;
}
.stat-card {
flex: 1;
align-items: center;
padding: 12px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #4CAF50;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
.progress-by-subject {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.subject-item {
margin-bottom: 16px;
}
.subject-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.subject-name {
font-size: 16px;
font-weight: bold;
color: #333;
}
.subject-percentage {
font-size: 14px;
color: #4CAF50;
font-weight: bold;
}
.progress-bar {
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
margin-bottom: 8px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #4CAF50;
border-radius: 4px;
}
.subject-details {
flex-direction: row;
justify-content: space-between;
}
.detail-text {
font-size: 12px;
color: #666;
}
.recent-activities {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.activity-item {
flex-direction: row;
align-items: flex-start;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.activity-icon {
width: 40px;
height: 40px;
background-color: #f0f0f0;
border-radius: 20px;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.activity-content {
flex: 1;
}
.activity-title {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.activity-desc {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.activity-time {
font-size: 11px;
color: #999;
}
.weekly-goals {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
}
.goal-item {
margin-bottom: 16px;
}
.goal-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.goal-name {
font-size: 16px;
font-weight: bold;
color: #333;
}
.goal-status {
font-size: 12px;
color: #FF9800;
background-color: #FFF3E0;
padding: 4px 8px;
border-radius: 12px;
}
.goal-status.completed {
color: #4CAF50;
background-color: #E8F5E8;
}
.goal-desc {
font-size: 12px;
color: #666;
margin-top: 8px;
}
</style>

View File

View File

@@ -0,0 +1,834 @@
<template>
<scroll-view direction="vertical" class="record-detail">
<!-- Header Card -->
<view class="header-card">
<view class="record-header">
<text class="record-title">{{ getRecordTitle() }}</text>
<view class="score-badge" :class="`score-${getScoreLevel()}`">
<text class="score-text">{{ getRecordScore() }}分</text>
</view>
</view>
<text class="record-project">{{ getProjectNameWrapper() }}</text>
<view class="record-meta">
<view class="meta-item">
<text class="meta-icon"></text>
<text class="meta-text">{{ formatDateWrapper(getRecordDate()) }}</text>
</view>
<view class="meta-item">
<text class="meta-icon">⏱️</text>
<text class="meta-text">{{ formatDuration(getRecordDuration()) }}</text>
</view>
</view>
</view>
<!-- Performance Data Card -->
<view class="performance-card">
<view class="card-header">
<text class="card-title">训练数据</text>
</view>
<view class="performance-grid"> <view
class="performance-item"
v-for="(metric, index) in getPerformanceMetrics()"
:key="index"
>
<view class="metric-icon">
<text class="icon">{{ getMetricIcon(metric) }}</text>
</view>
<view class="metric-content">
<text class="metric-value">{{ getMetricValue(metric) }}</text>
<text class="metric-label">{{ getMetricLabel(metric) }}</text>
<text class="metric-unit">{{ getMetricUnit(metric) }}</text>
</view>
</view>
</view>
</view>
<!-- Technique Analysis Card -->
<view class="technique-card">
<view class="card-header">
<text class="card-title">技术分析</text>
</view>
<view class="technique-list"> <view
class="technique-item"
v-for="(technique, index) in getTechniqueAnalysis()"
:key="index"
>
<view class="technique-header">
<text class="technique-name">{{ getTechniqueName(technique) }}</text>
<view class="rating-stars">
<text
class="star"
v-for="(star, starIndex) in 5"
:key="starIndex"
:class="starIndex < getTechniqueRating(technique) ? 'star-filled' : 'star-empty'"
>
</text>
</view>
</view>
<text class="technique-comment">{{ getTechniqueComment(technique) }}</text>
</view>
</view>
</view>
<!-- Improvement Suggestions Card -->
<view class="suggestions-card">
<view class="card-header">
<text class="card-title">改进建议</text>
</view>
<view class="suggestions-list"> <view
class="suggestion-item"
v-for="(suggestion, index) in getImprovementSuggestions()"
:key="index"
>
<view class="suggestion-icon">
<text class="icon"></text>
</view>
<view class="suggestion-content">
<text class="suggestion-title">{{ getSuggestionTitle(suggestion) }}</text>
<text class="suggestion-desc">{{ getSuggestionDescription(suggestion) }}</text>
<view class="priority-tag" :class="`priority-${getSuggestionPriority(suggestion)}`">
<text class="priority-text">{{ formatPriority(getSuggestionPriority(suggestion)) }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- Teacher Comments Card -->
<view class="comments-card" v-if="hasTeacherComments()">
<view class="card-header">
<text class="card-title">教师点评</text>
</view>
<view class="comment-content"> <view class="comment-header">
<text class="teacher-name">{{ getTeacherName() }}</text>
<text class="comment-date">{{ formatDateWrapper(getCommentDate()) }}</text>
</view>
<text class="comment-text">{{ getTeacherComment() }}</text>
<view class="comment-rating">
<text class="rating-label">综合评价:</text>
<view class="rating-stars">
<text
class="star"
v-for="(star, index) in 5"
:key="index"
:class="index < getTeacherRating() ? 'star-filled' : 'star-empty'"
>
</text>
</view>
</view>
</view>
</view>
<!-- Action Buttons -->
<view class="action-buttons">
<button class="action-btn secondary-btn" @click="viewAssignment">
查看作业
</button>
<button class="action-btn primary-btn" @click="retryTraining">
重新训练
</button>
</view>
</scroll-view>
</template>
<script setup lang="uts"> import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onLoad, onResize } from '@dcloudio/uni-app'
import { getProjectName, formatDate } from '../types.uts'
// Reactive data
const record = ref<UTSJSONObject>({})
const recordId = ref('')
const loading = ref(true)
// 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
})
// Methods
function loadRecordDetail() {
loading.value = true
// Mock data - replace with actual API call
setTimeout(() => {
record.value = {
"id": recordId.value,
"assignment_id": "1",
"project_name": "跳远训练",
"date": "2024-01-15T10:30:00",
"duration": 1800, // 30 minutes in seconds
"score": 85,
"performance_data": {
"distance": { "value": 4.2, "unit": "米", "icon": "" },
"attempts": { "value": 8, "unit": "次", "icon": "" },
"best_distance": { "value": 4.5, "unit": "米", "icon": "" },
"average_distance": { "value": 4.1, "unit": "米", "icon": "" }
},
"technique_analysis": [
{
"name": "助跑技术",
"rating": 4,
"comment": "助跑节奏良好,加速明显,起跳点控制较准确"
},
{
"name": "起跳技术",
"rating": 3,
"comment": "起跳时机把握较好,但起跳角度需要调整"
},
{
"name": "空中姿态",
"rating": 4,
"comment": "空中姿态保持良好,腿部动作协调"
},
{
"name": "落地技术",
"rating": 5,
"comment": "落地缓冲充分,姿态标准"
}
],
"improvement_suggestions": [
{
"title": "起跳角度调整",
"description": "适当增加起跳角度,有助于提高跳跃距离",
"priority": "high"
},
{
"title": "助跑速度控制",
"description": "在保证准确性的前提下,可以适当提高助跑速度",
"priority": "medium"
},
{
"title": "力量训练加强",
"description": "建议增加腿部力量训练,提高爆发力",
"priority": "low"
}
],
"teacher_comment": {
"teacher_name": "张老师",
"comment": "整体表现不错,技术动作比较标准。建议在起跳技术上多加练习,同时注意助跑与起跳的衔接。继续保持!",
"rating": 4,
"date": "2024-01-15T15:30:00"
}
} as UTSJSONObject
loading.value = false
}, 1000)
}
function getRecordDate(): string {
return record.value.getString('date') ?? ''
}
function getRecordDuration(): number {
return record.value.getNumber('duration') ?? 0
}
function getRecordScore(): number {
return record.value.getNumber('score') ?? 0
}
function getScoreLevel(): string {
const score = getRecordScore()
if (score >= 90) return 'excellent'
if (score >= 80) return 'good'
if (score >= 70) return 'fair'
return 'poor'
}
function formatDuration(seconds: number): string {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
return `${minutes}分${remainingSeconds}秒`
}
function getPerformanceMetrics(): Array<any> {
const data = record.value.getAny('performance_data') as UTSJSONObject ?? {}
const metrics: Array<any> = []
const keys = ['distance', 'attempts', 'best_distance', 'average_distance']
const labels = ['最终距离', '训练次数', '最好成绩', '平均距离']
keys.forEach((key, index) => {
const metric = (data as UTSJSONObject).getAny(key) as UTSJSONObject
if (metric !== null) {
metrics.push({
icon: metric.getString('icon') ?? '',
value: metric.getNumber('value') ?? 0,
unit: metric.getString('unit') ?? '',
label: labels[index]
})
}
})
return metrics
}
function getTechniqueAnalysis(): Array<any> {
const analysis = record.value.getArray('technique_analysis') ?? ([] as Array<any>)
if (analysis instanceof Array) {
return analysis as Array<any>
}
return []
}
function getImprovementSuggestions(): Array<any> {
const suggestions = record.value.getArray('improvement_suggestions') ?? ([] as Array<any>)
if (suggestions instanceof Array) {
return suggestions as Array<any>
}
return []
}
function formatPriority(priority: string): string {
const priorityMap = {
'high': '高优先级',
'medium': '中优先级',
'low': '低优先级'
} as UTSJSONObject
return priorityMap.getString(priority) ?? priority
}
function hasTeacherComments(): boolean {
const comment = record.value.getAny('teacher_comment')
return comment !== null
}
function getTeacherName(): string {
const comment = record.value.getAny('teacher_comment') as UTSJSONObject ?? {}
return (comment as UTSJSONObject).getString('teacher_name') ?? ''
}
function getTeacherComment(): string {
const comment = record.value.getAny('teacher_comment') as UTSJSONObject ?? {}
return (comment as UTSJSONObject).getString('comment') ?? ''
}
function getTeacherRating(): number {
const comment = record.value.getAny('teacher_comment') as UTSJSONObject ?? {}
return (comment as UTSJSONObject).getNumber('rating') ?? 0
}
function getCommentDate(): string {
const comment = record.value.getAny('teacher_comment') as UTSJSONObject ?? {}
return (comment as UTSJSONObject).getString('date') ?? ''
} function viewAssignment() {
const assignmentId = record.value.getString('assignment_id') ?? ''
if (assignmentId.length > 0) {
uni.navigateTo({
url: `/pages/sport/student/assignment-detail?id=${assignmentId}`
})
}
}
function retryTraining() {
const assignmentId = record.value.getString('assignment_id') ?? ''
if (assignmentId.length > 0) {
uni.navigateTo({
url: `/pages/sport/student/training-record?assignmentId=${assignmentId}`
})
}
} // Template wrapper functions for safe property access
function getMetricIcon(metric: any): string {
if (metric != null && typeof metric === 'object') {
const obj = metric as UTSJSONObject
return obj.getString('icon') ?? ''
}
return ''
}
function getMetricValue(metric: any): number {
if (metric != null && typeof metric === 'object') {
const obj = metric as UTSJSONObject
return obj.getNumber('value') ?? 0
}
return 0
}
function getMetricLabel(metric: any): string {
if (metric != null && typeof metric === 'object') {
const obj = metric as UTSJSONObject
return obj.getString('label') ?? ''
}
return ''
}
function getMetricUnit(metric: any): string {
if (metric != null && typeof metric === 'object') {
const obj = metric as UTSJSONObject
return obj.getString('unit') ?? ''
}
return ''
}
function getTechniqueName(technique: any): string {
if (technique != null && typeof technique === 'object') {
const obj = technique as UTSJSONObject
return obj.getString('name') ?? ''
}
return ''
}
function getTechniqueRating(technique: any): number {
if (technique != null && typeof technique === 'object') {
const obj = technique as UTSJSONObject
return obj.getNumber('rating') ?? 0
}
return 0
}
function getTechniqueComment(technique: any): string {
if (technique != null && typeof technique === 'object') {
const obj = technique as UTSJSONObject
return obj.getString('comment') ?? ''
}
return ''
}
function getSuggestionTitle(suggestion: any): string {
if (suggestion != null && typeof suggestion === 'object') {
const obj = suggestion as UTSJSONObject
return obj.getString('title') ?? ''
}
return ''
}
function getSuggestionDescription(suggestion: any): string {
if (suggestion != null && typeof suggestion === 'object') {
const obj = suggestion as UTSJSONObject
return obj.getString('description') ?? ''
}
return ''
}
function getSuggestionPriority(suggestion: any): string {
if (suggestion != null && typeof suggestion === 'object') {
const obj = suggestion as UTSJSONObject
return obj.getString('priority') ?? ''
}
return ''
}
function getProjectNameWrapper(): string {
return getProjectName(record.value)
}
function formatDateWrapper(dateStr: string): string {
return formatDate(dateStr)
}
function getRecordTitle(): string {
const date = getRecordDate()
return `训练记录 - ${formatDateWrapper(date)}`
} // Lifecycle
onLoad((options: OnLoadOptions) => {
recordId.value = options['id'] ?? ''
loadRecordDetail()
})
onMounted(() => {
screenWidth.value = uni.getSystemInfoSync().windowWidth
})
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
</script>
<style>
.record-detail {
display: flex;
flex:1;
background-color: #f5f5f5;
min-height: 100vh;
padding: 20rpx;
}
/* Header Card */
.header-card {
background: linear-gradient(to bottom right, #667eea, #764ba2);
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
color: white;
}
.record-header {
display: flex;
flex-direction:row;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.record-title {
font-size: 32rpx;
font-weight: bold;
flex: 1;
}
.score-badge {
padding: 8rpx 16rpx;
border-radius: 16rpx;
margin-left: 20rpx;
}
.score-excellent {
background-color: rgba(40, 167, 69, 0.3);
border: 1px solid rgba(40, 167, 69, 0.6);
}
.score-good {
background-color: rgba(0, 123, 255, 0.3);
border: 1px solid rgba(0, 123, 255, 0.6);
}
.score-fair {
background-color: rgba(255, 193, 7, 0.3);
border: 1px solid rgba(255, 193, 7, 0.6);
}
.score-poor {
background-color: rgba(220, 53, 69, 0.3);
border: 1px solid rgba(220, 53, 69, 0.6);
}
.score-text {
font-size: 28rpx;
font-weight: bold;
color: white;
}
.record-project {
font-size: 28rpx;
opacity: 0.9;
margin-bottom: 20rpx;
}
.record-meta {
display: flex;
flex-direction:row;
gap: 30rpx;
}
.meta-item {
display: flex;
flex-direction:row;
align-items: center;
}
.meta-icon {
font-size: 24rpx;
margin-right: 8rpx;
}
.meta-text {
font-size: 26rpx;
opacity: 0.9;
}
/* Performance Card */
.performance-card {
background-color: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-header {
margin-bottom: 25rpx;
}
.card-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.performance-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -10rpx;
} .performance-item {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #f8f9ff;
border-radius: 16rpx;
width: 44%;
flex: 0 0 44%;
margin: 0 10rpx 20rpx;
}
.metric-icon {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
background-color: #667eea;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15rpx;
flex-shrink: 0;
}
.icon {
font-size: 28rpx;
color: white;
}
.metric-content {
flex: 1;
}
.metric-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 2rpx;
}
.metric-label {
font-size: 24rpx;
color: #666;
display: block;
}
.metric-unit {
font-size: 20rpx;
color: #999;
}
/* Technique Card */
.technique-card {
background-color: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.technique-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.technique-item {
padding: 20rpx;
background-color: #fafafa;
border-radius: 12rpx;
}
.technique-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.technique-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.rating-stars {
display: flex;
flex-direction:row;
}
.star {
font-size: 24rpx;
margin-left: 2rpx;
}
.star-filled {
color: #ffc107;
}
.star-empty {
color: #ddd;
}
.technique-comment {
font-size: 26rpx;
color: #666;
line-height: 1.5;
}
/* Suggestions Card */
.suggestions-card {
background-color: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.suggestions-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.suggestion-item {
display: flex;
align-items: flex-start;
padding: 20rpx;
background-color: #fff8e1;
border-radius: 12rpx;
border-left: 4rpx solid #ffc107;
}
.suggestion-icon {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15rpx;
flex-shrink: 0;
}
.suggestion-content {
flex: 1;
position: relative;
}
.suggestion-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
display: block;
}
.suggestion-desc {
font-size: 26rpx;
color: #666;
line-height: 1.5;
margin-bottom: 10rpx;
}
.priority-tag {
display: inline-block;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-size: 22rpx;
}
.priority-high {
background-color: rgba(220, 53, 69, 0.1);
color: #dc3545;
}
.priority-medium {
background-color: rgba(255, 193, 7, 0.1);
color: #ffc107;
}
.priority-low {
background-color: rgba(40, 167, 69, 0.1);
color: #28a745;
}
.priority-text {
font-weight: 400;
}
/* Comments Card */
.comments-card {
background-color: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.comment-content {
background-color: #f8f9ff;
padding: 25rpx;
border-radius: 16rpx;
border-left: 4rpx solid #667eea;
}
.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.teacher-name {
font-size: 26rpx;
font-weight: bold;
color: #667eea;
}
.comment-date {
font-size: 24rpx;
color: #999;
}
.comment-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
margin-bottom: 15rpx;
}
.comment-rating {
display: flex;
align-items: center;
}
.rating-label {
font-size: 26rpx;
color: #666;
margin-right: 15rpx;
}
/* Action Buttons */
.action-buttons {
padding: 20rpx 0;
display: flex;
gap: 20rpx;
}
.action-btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: bold;
border: none;
}
.primary-btn {
background: linear-gradient(to bottom right, #667eea, #764ba2);
color: white;
}
.secondary-btn {
background-color: white;
color: #667eea;
border: 2rpx solid #667eea;
}
.primary-btn:active {
transform: scale(0.98);
}
.secondary-btn:active {
transform: scale(0.98);
background-color: #f8f9ff;
}
</style>

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,469 @@
<!-- 训练记录管理 - 直接使用 supaClient 示例 -->
<template>
<scroll-view direction="vertical" class="records-page">
<view class="header">
<text class="title">我的监测记录</text>
<button class="add-btn" @click="showAddRecord">新增记录</button>
</view>
<!-- 记录列表 -->
<scroll-view class="records-list" scroll-y="true">
<view class="record-item" v-for="record in records" :key="getRecordId(record)">
<view class="record-header">
<text class="record-title">{{ getRecordTitle(record) }}</text>
<text class="record-date">{{ formatDate(getRecordDate(record)) }}</text>
</view>
<view class="record-stats">
<text class="stat">时长: {{ getRecordDuration(record) }}分钟</text>
<text class="stat">消耗: {{ getRecordCalories(record) }}卡路里</text>
</view>
<view class="record-actions">
<button class="edit-btn" @click="editRecord(record)">编辑</button>
<button class="delete-btn" @click="deleteRecord(record)">删除</button>
</view>
</view>
</scroll-view>
<!-- 新增/编辑记录弹窗 -->
<view class="modal" v-if="showModal" @click="hideModal">
<view class="modal-content" @click.stop>
<text class="modal-title">{{ isEditing ? '编辑' : '新增' }}训练记录</text>
<input class="input" v-model="formData.title" placeholder="训练项目" />
<input class="input" v-model="formData.duration" type="number" placeholder="训练时长(分钟)" />
<input class="input" v-model="formData.calories" type="number" placeholder="消耗卡路里" />
<textarea class="textarea" v-model="formData.notes" placeholder="训练备注"></textarea>
<view class="modal-actions">
<button class="cancel-btn" @click="hideModal">取消</button>
<button class="save-btn" @click="saveRecord">保存</button>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts"> import { ref, onMounted, onUnmounted, computed } from 'vue'
import { onLoad, onResize } from '@dcloudio/uni-app'
import supaClient from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId } from '@/utils/store.uts'
// 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 userId = ref<string>('')
const records = ref<UTSJSONObject[]>([])
const showModal = ref<boolean>(false)
const isEditing = ref<boolean>(false)
const editingRecordId = ref<string>('')
const formData = ref({
title: '',
duration: '',
calories: '',
notes: ''
}) const loading = ref<boolean>(false)
const subscription = ref<any>(null)
// 页面加载时获取用户ID
onLoad((options: OnLoadOptions) => {
userId.value = options['id'] ?? getCurrentUserId()
})
// 生命周期
onMounted(() => {
screenWidth.value = uni.getSystemInfoSync().windowWidth
loadRecords()
setupRealtimeSubscription()
})
onResize((size: any) => {
screenWidth.value = size.size.windowWidth
})
onUnmounted(() => {
// 清理实时订阅
if (subscription.value) {
subscription.value.unsubscribe()
}
})// 获取记录相关信息
const getRecordId = (record: UTSJSONObject): string => {
return record.getString('id') ?? ''
}
const getRecordTitle = (record: UTSJSONObject): string => {
return record.getString('title') ?? '未命名训练'
}
const getRecordDate = (record: UTSJSONObject): string => {
return record.getString('created_at') ?? ''
}
const getRecordDuration = (record: UTSJSONObject): number => {
return record.getNumber('duration') ?? 0
}
const getRecordCalories = (record: UTSJSONObject): number => {
return record.getNumber('calories') ?? 0
}
// 格式化日期
const formatDate = (dateString: string): string => {
if (!dateString) return ''
try {
const date = new Date(dateString)
return `${date.getMonth() + 1}/${date.getDate()}`
} catch {
return ''
}
}
// 获取当前用户ID // 加载训练记录 - 直接使用 supaClient
const loadRecords = async () => {
try {
loading.value = true
const result = await supaClient
.from('ak_training_records')
.select('*', {})
.eq('student_id', userId.value)
.order('created_at', { ascending: false })
.limit(50)
.execute()
if (result.success) {
records.value = result.data as UTSJSONObject[]
} else {
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
} catch (error) {
console.error('加载记录失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 设置实时订阅 - 直接使用 supaClient
const setupRealtimeSubscription = () => {
try { subscription.value = supaClient
.from('ak_training_records') .on('INSERT', (payload) => {
console.log('新记录:', payload)
if ((payload.new as UTSJSONObject).getString('student_id') === userId.value) {
records.value.unshift(payload.new as UTSJSONObject)
}
})
.on('UPDATE', (payload) => {
console.log('记录更新:', payload)
const recordId = (payload.new as UTSJSONObject).getString('id') ?? ''
const index = records.value.findIndex(r => getRecordId(r) === recordId)
if (index !== -1) {
records.value[index] = payload.new as UTSJSONObject
}
})
.on('DELETE', (payload) => {
console.log('记录删除:', payload)
const recordId = (payload.old as UTSJSONObject).getString('id') ?? ''
records.value = records.value.filter(r => getRecordId(r) !== recordId)
})
.subscribe()
} catch (error) {
console.error('设置实时订阅失败:', error)
}
}
// 保存记录 - 直接使用 supaClient
const saveRecord = async () => {
if (!formData.value.title.trim()) {
uni.showToast({
title: '请输入训练项目',
icon: 'none'
})
return
}
try { const recordData = {
title: formData.value.title,
duration: parseInt(formData.value.duration) || 0,
calories: parseInt(formData.value.calories) || 0,
notes: formData.value.notes,
student_id: userId.value
}
let result
if (isEditing.value) {
// 更新记录
result = await supaClient
.from('ak_training_records')
.update(recordData)
.eq('id', editingRecordId.value)
.single()
.execute()
} else {
// 创建新记录
result = await supaClient
.from('ak_training_records')
.insert(recordData)
.single()
.execute()
}
if (result.success) {
uni.showToast({
title: isEditing.value ? '更新成功' : '保存成功',
icon: 'success'
})
hideModal()
loadRecords() // 重新加载数据
} else {
throw new Error(result.message || '保存失败')
}
} catch (error) {
console.error('保存记录失败:', error)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
// 删除记录 - 直接使用 supaClient
const deleteRecord = async (record: UTSJSONObject) => {
const recordId = getRecordId(record)
const title = getRecordTitle(record)
uni.showModal({
title: '确认删除',
content: `确定要删除训练记录"${title}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
const result = await supaClient
.from('ak_training_records')
.delete()
.eq('id', recordId)
.execute()
if (result.success) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
loadRecords()
} else {
throw new Error(result.message || '删除失败')
}
} catch (error) {
console.error('删除记录失败:', error)
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
}
}
})
}
// 显示新增记录弹窗
const showAddRecord = () => {
isEditing.value = false
editingRecordId.value = ''
formData.value = {
title: '',
duration: '',
calories: '',
notes: ''
}
showModal.value = true
}
// 编辑记录
const editRecord = (record: UTSJSONObject) => {
isEditing.value = true
editingRecordId.value = getRecordId(record) formData.value = {
title: getRecordTitle(record),
duration: getRecordDuration(record).toString(),
calories: getRecordCalories(record).toString(),
notes: record.getString('notes') ?? ''
}
showModal.value = true
}
// 隐藏弹窗
const hideModal = () => {
showModal.value = false
}
</script>
<style>
.records-page {
display: flex;
flex:1;
padding: 20rpx;
background: #f5f5f5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding: 20rpx;
background: white;
border-radius: 15rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.add-btn {
background-image: linear-gradient(to top right, #667eea, #764ba2);
color: white;
border: none;
padding: 15rpx 30rpx;
border-radius: 25rpx;
font-size: 28rpx;
}
.records-list {
height: calc(100vh - 200rpx);
}
.record-item {
background: white;
margin-bottom: 20rpx;
padding: 30rpx;
border-radius: 15rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.record-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.record-date {
font-size: 24rpx;
color: #999;
}
.record-stats {
display: flex;
gap: 30rpx;
margin-bottom: 20rpx;
}
.stat {
font-size: 26rpx;
color: #666;
}
.record-actions {
display: flex;
gap: 20rpx;
}
.edit-btn, .delete-btn {
flex: 1;
padding: 15rpx;
border: none;
border-radius: 10rpx;
font-size: 26rpx;
}
.edit-btn {
background: #3498db;
color: white;
}
.delete-btn {
background: #e74c3c;
color: white;
}
/* 弹窗样式 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 20rpx;
padding: 40rpx;
width: 90%;
max-width: 600rpx;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.input, .textarea {
width: 100%;
padding: 20rpx;
border: 1px solid #ddd;
border-radius: 10rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
}
.textarea {
height: 120rpx;
resize: none;
}
.modal-actions {
display: flex;
gap: 20rpx;
margin-top: 30rpx;
}
.cancel-btn, .save-btn {
flex: 1;
padding: 25rpx;
border: none;
border-radius: 15rpx;
font-size: 28rpx;
}
.cancel-btn {
background: #95a5a6;
color: white;
}
.save-btn {
background-image: linear-gradient(to top right, #667eea, #764ba2);
color: white;
}
</style>

File diff suppressed because it is too large Load Diff