Sprint 6: report review workflow, CI/K8s, and client tabs.
CI / api-test (push) Has been cancelled
CI / web-build (push) Has been cancelled

Add coach review APIs, pose calibration thresholds, Gitea CI, Kubernetes skeleton, H5 practice page, and mini program tab bar.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-23 11:44:11 +08:00
parent 1aaef71f52
commit a400130c67
27 changed files with 636 additions and 27 deletions
+5 -1
View File
@@ -2,7 +2,7 @@
<div class="app-shell">
<header class="topbar">
<strong>Happy Up</strong>
<span class="stage">Sprint 5 H5</span>
<span class="stage">Sprint 6 H5</span>
</header>
<div class="page">
<RouterView />
@@ -20,6 +20,10 @@
<span class="dot"></span>
报告
</RouterLink>
<RouterLink to="/practice" :class="{ active: route.path === '/practice' }">
<span class="dot"></span>
跟练
</RouterLink>
<RouterLink to="/training" :class="{ active: route.path === '/training' }">
<span class="dot"></span>
训练
+2
View File
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import PracticeView from '../views/PracticeView.vue'
import ReportView from '../views/ReportView.vue'
import ScreeningView from '../views/ScreeningView.vue'
import TrainingView from '../views/TrainingView.vue'
@@ -14,6 +15,7 @@ const router = createRouter({
{ path: '/screening', component: ScreeningView },
{ path: '/reports', component: ReportView },
{ path: '/training', component: TrainingView },
{ path: '/practice', component: PracticeView },
],
})
+5
View File
@@ -9,6 +9,7 @@
<div class="muted">{{ child.birthday }} · {{ child.gender }}</div>
</div>
<button class="btn" style="margin-top: 16px" @click="goScreening">开始筛查</button>
<button class="btn secondary" style="margin-top: 8px" @click="goPractice">跟练打卡</button>
</div>
</section>
</template>
@@ -41,4 +42,8 @@ function goScreening() {
}
router.push('/screening')
}
function goPractice() {
router.push('/practice')
}
</script>
+82
View File
@@ -0,0 +1,82 @@
<template>
<section class="card">
<h2>跟练打卡</h2>
<p class="muted">{{ planGoal || '加载训练计划…' }}</p>
<div v-if="exercise" class="list-item">
<strong>{{ exercise.name }}</strong>
<p class="muted">目标 {{ durationMinutes }} 分钟 · 得分 {{ score }}</p>
</div>
<div class="progress"><span :style="{ width: progress + '%' }"></span></div>
<p class="muted">{{ statusText }}</p>
<button class="btn" :disabled="running || !planId" @click="startPractice">开始跟练</button>
<button v-if="planId" class="btn secondary" style="margin-top: 8px" :disabled="running" @click="submitRecord">
提交打卡
</button>
</section>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { apiFetch, listChildren, listTrainingPlans } from '../api'
const planId = ref<number | null>(null)
const planGoal = ref('')
const exercise = ref<{ name: string } | null>(null)
const durationMinutes = ref(5)
const progress = ref(0)
const score = ref(80)
const statusText = ref('准备就绪')
const running = ref(false)
onMounted(async () => {
try {
const { list } = await listChildren()
const child = list[0]
if (!child) return
const plans = await listTrainingPlans(child.id)
const plan = plans.list[0]
if (!plan) return
planId.value = plan.id
planGoal.value = plan.detail.goal
exercise.value = plan.detail.exercises[0] || null
durationMinutes.value = plan.detail.exercises[0]?.durationMinutes || 5
} catch {
statusText.value = '暂无训练计划'
}
})
async function startPractice() {
running.value = true
progress.value = 0
statusText.value = '跟练中…'
for (let step = 1; step <= 5; step += 1) {
await new Promise((resolve) => setTimeout(resolve, 400))
progress.value = step * 20
}
score.value = 78 + Math.floor(Math.random() * 15)
statusText.value = '跟练完成,可提交打卡'
running.value = false
}
async function submitRecord() {
if (!planId.value || !exercise.value) return
running.value = true
try {
await apiFetch(`/api/training/plans/${planId.value}/records`, {
method: 'POST',
body: JSON.stringify({
exerciseId: 1,
completed: true,
score: score.value,
durationSeconds: durationMinutes.value * 60,
note: 'H5 跟练打卡',
}),
})
statusText.value = '打卡成功 ✓'
} catch (err) {
statusText.value = err instanceof Error ? err.message : '打卡失败'
} finally {
running.value = false
}
}
</script>