Sprint 6: report review workflow, CI/K8s, and client tabs.
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:
@@ -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>
|
||||
训练
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user