Initial commit of akmon project

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

View File

@@ -0,0 +1,928 @@
<template>
<scroll-view direction="vertical" class="project-detail" :class="{ 'small-screen': !isLargeScreen }" :scroll-y="true" :enable-back-to-top="true">
<!-- Header Card -->
<view class="header-card">
<view class="project-header">
<text class="project-title">{{ getProjectDisplayNameWrapper(project) }}</text>
<view class="status-badge" :class="`status-${project.getString('status') ?? 'active'}`">
<text class="status-text">{{ formatProjectStatusLocal(project.getString('status') ?? 'active') }}</text>
</view>
</view>
<text class="project-description">{{ getProjectDescriptionWrapper(project) }}</text>
<view class="project-meta">
<view class="meta-row">
<view class="meta-item">
<text class="meta-icon"></text>
<text class="meta-text">{{ getProjectCategoryWrapper(project) }}</text>
</view>
<view class="meta-item">
<text class="meta-icon">⭐</text>
<text class="meta-text">{{ formatDifficultyWrapper(getProjectDifficultyWrapper(project)) }}</text>
</view>
</view>
<view class="meta-row">
<view class="meta-item">
<text class="meta-icon"></text>
<text class="meta-text">{{ getAssignmentCount() }}个作业</text>
</view>
<view class="meta-item">
<text class="meta-icon"></text>
<text class="meta-text">{{ getRecordCount() }}条记录</text>
</view>
</view>
</view>
</view>
<!-- Statistics Card -->
<view class="stats-card">
<view class="card-header">
<text class="card-title">统计概览</text>
</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-value">{{ getTotalStudents() }}</text>
<text class="stat-label">参与学生</text>
<text class="stat-icon"></text>
</view>
<view class="stat-item">
<text class="stat-value">{{ getAverageScore() }}</text>
<text class="stat-label">平均分数</text>
<text class="stat-icon"></text>
</view>
<view class="stat-item">
<text class="stat-value">{{ getCompletionRate() }}%</text>
<text class="stat-label">完成率</text>
<text class="stat-icon">✅</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ getImprovementRate() }}%</text>
<text class="stat-label">进步率</text>
<text class="stat-icon"></text>
</view>
</view>
</view>
<!-- Training Requirements Card -->
<view class="requirements-card">
<view class="card-header">
<text class="card-title">训练要求</text>
</view>
<view class="requirements-content">
<view class="requirement-section">
<text class="section-title">基础要求</text>
<view class="requirement-list"> <view
class="requirement-item"
v-for="(req, index) in getBasicRequirements()"
:key="'req-' + index"
>
<text class="requirement-icon">{{ getRequirementIcon(req) }}</text>
<text class="requirement-text">{{ getRequirementText(req) }}</text>
</view>
</view>
</view>
<view class="requirement-section">
<text class="section-title">评分标准</text>
<view class="scoring-table"> <view
class="scoring-row"
v-for="(criteria, index) in getScoringCriteria()"
:key="'criteria-' + index"
>
<view class="score-range">{{ getCriteriaRange(criteria) }}</view>
<view class="score-desc">{{ getCriteriaDescription(criteria) }}</view>
</view>
</view>
</view>
</view>
</view>
<!-- Recent Assignments Card -->
<view class="assignments-card">
<view class="card-header">
<text class="card-title">近期作业</text>
<text class="view-all-btn" @click="viewAllAssignments">查看全部</text>
</view>
<view class="assignments-list"> <view
class="assignment-item"
v-for="(assignment, index) in getRecentAssignments()"
:key="'assignment-' + index"
@click="viewAssignmentDetail(assignment)"
>
<view class="assignment-content">
<text class="assignment-title">{{ getAssignmentDisplayNameWrapper(assignment) }}</text>
<text class="assignment-date">{{ formatDateWrapper(getAssignmentCreatedAtLocal(assignment)) }}</text>
</view>
<view class="assignment-stats">
<text class="participants">{{ getAssignmentParticipants(assignment) }}人参与</text>
<view class="status-dot" :class="`status-${getAssignmentStatusWrapper(assignment)}`"></view>
</view>
</view>
</view>
</view>
<!-- Performance Trends Card -->
<view class="trends-card">
<view class="card-header">
<text class="card-title">成绩趋势</text>
</view>
<view class="trends-content">
<view class="chart-placeholder">
<text class="chart-text">成绩趋势图</text>
<text class="chart-desc">显示最近30天的平均成绩变化</text>
</view>
<view class="trend-summary">
<view class="trend-item">
<text class="trend-label">本周平均:</text>
<text class="trend-value">{{ getWeeklyAverage() }}分</text>
<text class="trend-change positive">+{{ getWeeklyChange() }}%</text>
</view>
<view class="trend-item">
<text class="trend-label">本月平均:</text>
<text class="trend-value">{{ getMonthlyAverage() }}分</text>
<text class="trend-change negative">{{ getMonthlyChange() }}%</text>
</view>
</view>
</view>
</view>
<!-- Action Buttons -->
<view class="action-buttons">
<button class="action-btn secondary-btn" @click="editProject">
编辑项目
</button>
<button class="action-btn primary-btn" @click="createAssignment">
创建作业
</button>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import { getProjectId, getProjectDisplayName, getProjectDescription,
getProjectCategory, getProjectDifficulty, getAssignmentDisplayName,
getAssignmentStatus, getAssignmentId, formatDifficulty,
formatDate, formatProjectStatus, getAssignmentCreatedAt } from '../types.uts'
// Reactive data
const project = ref<UTSJSONObject>({})
const projectId = ref('')
const assignments = ref<Array<UTSJSONObject>>([])
const statistics = ref<UTSJSONObject>({})
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 loadProjectDetail() {
loading.value = true
// Mock data - replace with actual API calls
setTimeout(() => {
project.value = {
"id": projectId.value,
"title": "跳远技术训练",
"description": "全面训练跳远技术,包括助跑、起跳、空中姿态和落地等各个环节",
"category": "田径运动",
"difficulty": "intermediate",
"status": "active",
"basic_requirements": [
{ "icon": "", "text": "助跑距离12-16步节奏均匀" },
{ "icon": "", "text": "单脚起跳,起跳点准确" },
{ "icon": "✈️", "text": "空中保持良好姿态" },
{ "icon": "", "text": "双脚并拢前伸落地" }
],
"scoring_criteria": [
{ "range": "90-100分", "description": "动作标准,技术熟练,成绩优异" },
{ "range": "80-89分", "description": "动作较标准,技术较熟练" },
{ "range": "70-79分", "description": "动作基本标准,需要继续练习" },
{ "range": "60-69分", "description": "动作不标准,需要重点改进" }
]
} as UTSJSONObject
assignments.value = [
{
"id": "1",
"title": "跳远基础训练",
"created_at": "2024-01-15T10:00:00",
"participants": 25,
"status": "active"
},
{
"id": "2",
"title": "跳远技术提升",
"created_at": "2024-01-10T14:30:00",
"participants": 22,
"status": "completed"
},
{
"id": "3",
"title": "跳远考核测试",
"created_at": "2024-01-08T09:15:00",
"participants": 28,
"status": "completed"
}
]
statistics.value = {
"total_students": 28,
"average_score": 82.5,
"completion_rate": 85,
"improvement_rate": 12,
"weekly_average": 84.2,
"weekly_change": 3.5,
"monthly_average": 81.8,
"monthly_change": -1.2,
"assignment_count": 8,
"record_count": 156
} as UTSJSONObject
loading.value = false
}, 1000)
}
// 监听屏幕尺寸变化
onMounted(() => {
screenWidth.value = uni.getSystemInfoSync().windowWidth
})
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
// Lifecycle
onLoad((options: OnLoadOptions) => {
const id = options['id']
if (id !== null) {
projectId.value = id as string
} else {
projectId.value = ''
}
loadProjectDetail()
})
// Helper functions for data access
function getAssignmentCount(): number {
return (statistics.value.get('assignment_count') as number) ?? 0
}
function getRecordCount(): number {
return (statistics.value.get('record_count') as number) ?? 0
}
function getTotalStudents(): number {
return (statistics.value.get('total_students') as number) ?? 0
}
function getAverageScore(): string {
const score = (statistics.value.get('average_score') as number) ?? 0
return score.toFixed(1)
}
function getCompletionRate(): number {
return (statistics.value.get('completion_rate') as number) ?? 0
}
function getImprovementRate(): number {
return (statistics.value.get('improvement_rate') as number) ?? 0
} function getBasicRequirements(): Array<UTSJSONObject> {
const requirements = project.value.get('basic_requirements') as Array<any> ?? []
if (requirements instanceof Array) {
return requirements.map((item: any) => item as UTSJSONObject)
}
return []
} function getScoringCriteria(): Array<UTSJSONObject> {
const criteriaData = project.value.get('scoring_criteria') ?? null
if (criteriaData != null && typeof criteriaData === 'object') {
// New JSON format: {criteria: [{min_score, max_score, description}], ...}
const criteriaObj = criteriaData as UTSJSONObject
const criteria = criteriaObj.get('criteria') as Array<any> ?? []
if (criteria instanceof Array) {
return criteria.map((item: any) => {
const itemObj = item as UTSJSONObject
const minScore = itemObj.get('min_score') ?? 0
const maxScore = itemObj.get('max_score') ?? 100
const description = itemObj.get('description') ?? ''
return {
range: `${minScore}-${maxScore}分`,
description: description.toString()
} as UTSJSONObject
})
}
}
// Fallback: Legacy format or hardcoded data
const legacyCriteria = project.value.get('scoring_criteria') as Array<any> ?? []
if (legacyCriteria instanceof Array) {
return legacyCriteria.map((item: any) => item as UTSJSONObject)
}
return []
}
function getRecentAssignments(): Array<UTSJSONObject> {
return assignments.value.slice(0, 3)
}
function getAssignmentParticipants(assignment: UTSJSONObject): number {
return (assignment.get('participants') as number) ?? 0
}
function getWeeklyAverage(): string {
const score = (statistics.value.get('weekly_average') as number) ?? 0
return score.toFixed(1)
}
function getWeeklyChange(): string {
const change = (statistics.value.get('weekly_change') as number) ?? 0
return Math.abs(change).toFixed(1)
}
function getMonthlyAverage(): string {
const score = (statistics.value.get('monthly_average') as number) ?? 0
return score.toFixed(1)
}
function getMonthlyChange(): string {
const change = (statistics.value.get('monthly_change') as number) ?? 0
return change.toFixed(1)
}
function viewAllAssignments() {
uni.navigateTo({
url: `/pages/sport/teacher/assignments?projectId=${projectId.value}`
})
}
function viewAssignmentDetail(assignment: UTSJSONObject) {
const assignmentId = getAssignmentId(assignment)
uni.navigateTo({
url: `/pages/sport/teacher/assignment-detail?id=${assignmentId}`
})
}
function editProject() {
uni.navigateTo({
url: `/pages/sport/teacher/project-edit?id=${projectId.value}`
})
}
function createAssignment() {
uni.navigateTo({
url: `/pages/sport/teacher/create-assignment?projectId=${projectId.value}`
})
}
// Template helper functions
const getProjectDisplayNameWrapper = (project: UTSJSONObject): string => getProjectDisplayName(project)
const getProjectDescriptionWrapper = (project: UTSJSONObject): string => getProjectDescription(project)
const getProjectCategoryWrapper = (project: UTSJSONObject): string => getProjectCategory(project)
const getProjectDifficultyWrapper = (project: UTSJSONObject): number => getProjectDifficulty(project)
const getAssignmentDisplayNameWrapper = (assignment: UTSJSONObject): string => getAssignmentDisplayName(assignment)
const getAssignmentStatusWrapper = (assignment: UTSJSONObject): string => getAssignmentStatus(assignment)
const formatDifficultyWrapper = (difficulty: number): string => formatDifficulty(difficulty)
const formatDateWrapper = (date: string): string => formatDate(date)
const formatProjectStatusLocal = (status: string): string => formatProjectStatus(status)
const getAssignmentCreatedAtLocal = (assignment: UTSJSONObject): string => getAssignmentCreatedAt(assignment)
// Scoring criteria helpers for template
function getCriteriaRange(criteria: UTSJSONObject): string {
// Handles both legacy and new format
if (criteria.getString('range')!=null) {
return criteria.getString('range')?? ''
}
const min = criteria.get('min_score') ?? ''
const max = criteria.get('max_score') ?? ''
if (min !== '' && max !== '') {
return `${min}-${max}分`
}
return ''
}
function getCriteriaDescription(criteria: UTSJSONObject): string {
return criteria.getString('description') ?? ''
}
// Requirement helpers
function getRequirementIcon(req: UTSJSONObject): string {
return req.getString('icon') ?? ''
}
function getRequirementText(req: UTSJSONObject): string {
return req.getString('text') ?? ''
}
</script>
<style>
.project-detail {
background-color: #f5f5f5;
height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
box-sizing: border-box;
}
/* Header Card */
.header-card {
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
color: white;
}
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.project-title {
font-size: 36rpx;
font-weight: bold;
flex: 1;
}
.status-badge {
padding: 8rpx 16rpx;
border-radius: 16rpx;
margin-left: 20rpx;
}
.status-active {
background-color: rgba(40, 167, 69, 0.3);
border: 1px solid rgba(40, 167, 69, 0.6);
}
.status-inactive {
background-color: rgba(108, 117, 125, 0.3);
border: 1px solid rgba(108, 117, 125, 0.6);
}
.status-text {
font-size: 24rpx;
color: white;
}
.project-description {
font-size: 28rpx;
line-height: 1.6;
margin-bottom: 25rpx;
opacity: 0.9;
}
.project-meta {
display: flex;
flex-direction: column;
}
.project-meta .meta-row {
margin-bottom: 15rpx;
}
.project-meta .meta-row:last-child {
margin-bottom: 0;
}
.meta-row {
display: flex;
justify-content: space-between;
}
.meta-item {
display: flex;
align-items: center;
flex: 1;
}
.meta-icon {
font-size: 24rpx;
margin-right: 8rpx;
}
.meta-text {
font-size: 26rpx;
opacity: 0.9;
}
/* Statistics Card */
.stats-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 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
}
.card-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.stats-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.stat-item {
width: 300rpx;
margin-right: 20rpx;
margin-bottom: 20rpx;
text-align: center;
padding: 25rpx;
background-color: #f8f9ff;
border-radius: 16rpx;
position: relative;
}
.stat-item:nth-child(2n) {
margin-right: 0;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: #667eea;
display: block;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 26rpx;
color: #666;
display: block;
}
.stat-icon {
position: absolute;
top: 15rpx;
right: 15rpx;
font-size: 32rpx;
opacity: 0.3;
}
/* 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);
}
.requirement-section {
margin-bottom: 30rpx;
}
.requirement-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
display: block;
}
.requirement-list {
display: flex;
flex-direction: column;
}
.requirement-list .requirement-item {
margin-bottom: 12rpx;
}
.requirement-list .requirement-item:last-child {
margin-bottom: 0;
}
.requirement-item {
display: flex;
align-items: center;
padding: 15rpx;
background-color: #f8f9ff;
border-radius: 12rpx;
}
.requirement-icon {
font-size: 24rpx;
margin-right: 12rpx;
width: 32rpx;
text-align: center;
}
.requirement-text {
font-size: 26rpx;
color: #333;
flex: 1;
}
.scoring-table {
border-radius: 12rpx;
overflow: hidden;
border: 1rpx solid #eee;
}
.scoring-row {
display: flex;
border-bottom: 1rpx solid #eee;
}
.scoring-row:nth-child(4) {
border-bottom: none;
}
.score-range {
width: 160rpx;
padding: 15rpx;
background-color: #f8f9ff;
font-size: 26rpx;
font-weight: bold;
color: #667eea;
text-align: center;
border-right: 1rpx solid #eee;
}
.score-desc {
flex: 1;
padding: 15rpx;
font-size: 26rpx;
color: #333;
background-color: white;
}
/* Assignments Card */
.assignments-card {
background-color: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.view-all-btn {
font-size: 26rpx;
color: #667eea;
padding: 8rpx 16rpx;
border-radius: 12rpx;
background-color: rgba(102, 126, 234, 0.1);
}
.assignments-list {
display: flex;
flex-direction: column;
}
.assignment-item {
display: flex;
justify-content: space-between;
margin-bottom: 15rpx;
align-items: center;
padding: 20rpx;
background-color: #fafafa;
border-radius: 12rpx;
}
.assignment-content {
flex: 1;
}
.assignment-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
display: block;
}
.assignment-date {
font-size: 24rpx;
color: #666;
}
.assignment-stats {
display: flex;
align-items: center;
}
.assignment-stats .participants {
margin-right: 15rpx;
}
.participants {
font-size: 24rpx;
color: #666;
}
.status-dot {
width: 12rpx;
height: 12rpx;
border-radius: 6rpx;
}
.status-dot.status-active {
background-color: #28a745;
}
.status-dot.status-completed {
background-color: #6c757d;
}
/* Trends Card */
.trends-card {
background-color: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.chart-placeholder {
height: 200rpx;
background-image: linear-gradient(to bottom right, #f8f9ff, #e3f2fd);
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 25rpx;
border: 2rpx dashed #667eea;
}
.chart-text {
font-size: 32rpx;
font-weight: bold;
color: #667eea;
margin-bottom: 8rpx;
}
.chart-desc {
font-size: 24rpx;
color: #666;
}
.trend-summary {
display: flex;
justify-content: space-around;
}
.trend-item {
text-align: center;
}
.trend-label {
font-size: 24rpx;
color: #666;
display: block;
margin-bottom: 5rpx;
}
.trend-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 5rpx;
}
.trend-change {
font-size: 24rpx;
font-weight: bold;
padding: 4rpx 8rpx;
border-radius: 8rpx;
}
.trend-change.positive {
color: #28a745;
background-color: rgba(40, 167, 69, 0.1);
}
.trend-change.negative {
color: #dc3545;
background-color: rgba(220, 53, 69, 0.1);
}
/* 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-of-type {
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;
}
/* 小屏幕专用样式 */
.small-screen .stats-grid {
flex-direction: column;
}
.small-screen .stat-item {
width: 100%;
margin-right: 0;
}
.small-screen .action-buttons {
flex-direction: column;
}
.small-screen .action-btn {
margin-right: 0;
margin-bottom: 15rpx;
}
.small-screen .action-btn:last-of-type {
margin-bottom: 0;
}
/* 响应式布局 - 小屏幕优化 */
@media (max-width: 768px) {
.stats-grid {
flex-direction: column;
}
.stat-item {
width: 100%;
margin-right: 0;
}
.meta-row {
flex-direction: column;
}
.meta-item {
margin-bottom: 10rpx;
}
.action-buttons {
flex-direction: column;
}
.action-btn {
margin-right: 0;
margin-bottom: 15rpx;
}
.action-btn:last-of-type {
margin-bottom: 0;
}
.project-detail {
padding: 15rpx;
}
.header-card, .stats-card, .requirements-card, .assignments-card, .trends-card {
padding: 20rpx;
margin-bottom: 15rpx;
}
}
/* Android 兼容性优化 */
.project-detail {
display: flex;
flex:1;
/* 确保滚动容器正确 */
overflow-y: auto;
/* 移除可能不支持的属性 */
-webkit-overflow-scrolling: touch;
}
/* 修复可能的渲染问题 */
.header-card, .stats-card, .requirements-card, .assignments-card, .trends-card {
/* 强制硬件加速 */
transform: translateZ(0);
/* 确保背景正确渲染 */
background-clip: padding-box;
}
</style>