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

835 lines
19 KiB
Plaintext

<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>