Add multi-session auth and changelog tracking
这个提交包含在:
@@ -4,7 +4,9 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Activity, Calendar, CheckCircle2, Clock, TrendingUp, Target, Sparkles } from "lucide-react";
|
||||
import { Activity, Calendar, CheckCircle2, ChevronDown, ChevronUp, Clock, TrendingUp, Target, Sparkles } from "lucide-react";
|
||||
import { formatDateTimeShanghai, formatMonthDayShanghai } from "@/lib/time";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend
|
||||
@@ -17,6 +19,7 @@ export default function Progress() {
|
||||
const { data: analyses } = trpc.analysis.list.useQuery();
|
||||
const { data: stats } = trpc.profile.stats.useQuery();
|
||||
const [, setLocation] = useLocation();
|
||||
const [expandedRecordId, setExpandedRecordId] = useState<number | null>(null);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -29,7 +32,7 @@ export default function Progress() {
|
||||
// Aggregate data by date for charts
|
||||
const dateMap = new Map<string, { date: string; sessions: number; minutes: number; avgScore: number; scores: number[] }>();
|
||||
(records || []).forEach((r: any) => {
|
||||
const date = new Date(r.trainingDate || r.createdAt).toLocaleDateString("zh-CN", { month: "short", day: "numeric" });
|
||||
const date = formatMonthDayShanghai(r.trainingDate || r.createdAt);
|
||||
const existing = dateMap.get(date) || { date, sessions: 0, minutes: 0, avgScore: 0, scores: [] };
|
||||
existing.sessions++;
|
||||
existing.minutes += r.durationMinutes || 0;
|
||||
@@ -44,7 +47,7 @@ export default function Progress() {
|
||||
|
||||
// Analysis score trend
|
||||
const scoreTrend = (analyses || []).map((a: any) => ({
|
||||
date: new Date(a.createdAt).toLocaleDateString("zh-CN", { month: "short", day: "numeric" }),
|
||||
date: formatMonthDayShanghai(a.createdAt),
|
||||
overall: Math.round(a.overallScore || 0),
|
||||
consistency: Math.round(a.strokeConsistency || 0),
|
||||
footwork: Math.round(a.footworkScore || 0),
|
||||
@@ -179,32 +182,104 @@ export default function Progress() {
|
||||
{(records?.length || 0) > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{(records || []).slice(0, 20).map((record: any) => (
|
||||
<div key={record.id} className="flex items-center justify-between py-2 border-b last:border-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`h-8 w-8 rounded-lg flex items-center justify-center ${
|
||||
record.completed ? "bg-green-50 text-green-600" : "bg-muted text-muted-foreground"
|
||||
}`}>
|
||||
{record.completed ? <CheckCircle2 className="h-4 w-4" /> : <Activity className="h-4 w-4" />}
|
||||
<div key={record.id} className="border-b py-2 last:border-0">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`mt-0.5 h-8 w-8 rounded-lg flex items-center justify-center ${
|
||||
record.completed ? "bg-green-50 text-green-600" : "bg-muted text-muted-foreground"
|
||||
}`}>
|
||||
{record.completed ? <CheckCircle2 className="h-4 w-4" /> : <Activity className="h-4 w-4" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{record.exerciseName}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatDateTimeShanghai(record.trainingDate || record.createdAt)}
|
||||
{record.durationMinutes ? ` · ${record.durationMinutes}分钟` : ""}
|
||||
{record.sourceType ? ` · ${record.sourceType}` : ""}
|
||||
</p>
|
||||
{record.actionCount ? (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
动作数 {record.actionCount}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{record.exerciseName}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(record.trainingDate || record.createdAt).toLocaleDateString("zh-CN")}
|
||||
{record.durationMinutes ? ` · ${record.durationMinutes}分钟` : ""}
|
||||
{record.sourceType ? ` · ${record.sourceType}` : ""}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{record.poseScore && (
|
||||
<Badge variant="secondary" className="text-xs">{Math.round(record.poseScore)}分</Badge>
|
||||
)}
|
||||
{record.completed ? (
|
||||
<Badge className="bg-green-100 text-green-700 text-xs">已完成</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs">进行中</Badge>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setExpandedRecordId((current) => current === record.id ? null : record.id)}
|
||||
>
|
||||
{expandedRecordId === record.id ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{record.poseScore && (
|
||||
<Badge variant="secondary" className="text-xs">{Math.round(record.poseScore)}分</Badge>
|
||||
)}
|
||||
{record.completed ? (
|
||||
<Badge className="bg-green-100 text-green-700 text-xs">已完成</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs">进行中</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expandedRecordId === record.id ? (
|
||||
<div className="mt-3 rounded-2xl border border-border/60 bg-muted/20 p-4 text-sm">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">记录时间</div>
|
||||
<div className="mt-1 font-medium">{formatDateTimeShanghai(record.trainingDate || record.createdAt, { second: "2-digit" })}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">动作数据</div>
|
||||
<div className="mt-1 font-medium">动作数 {record.actionCount || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{record.metadata ? (
|
||||
<div className="mt-4 space-y-3">
|
||||
{record.metadata.dominantAction ? (
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">主动作</div>
|
||||
<div className="mt-1 font-medium">{String(record.metadata.dominantAction)}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{record.metadata.actionSummary && Object.keys(record.metadata.actionSummary).length > 0 ? (
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">动作明细</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{Object.entries(record.metadata.actionSummary as Record<string, number>)
|
||||
.filter(([, count]) => Number(count) > 0)
|
||||
.map(([actionType, count]) => (
|
||||
<Badge key={actionType} variant="secondary">
|
||||
{actionType} {count} 次
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{record.metadata.validityStatus ? (
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">录制有效性</div>
|
||||
<div className="mt-1 font-medium">{String(record.metadata.validityStatus)}</div>
|
||||
{record.metadata.invalidReason ? (
|
||||
<div className="mt-1 text-xs text-muted-foreground">{String(record.metadata.invalidReason)}</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{record.notes ? (
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">备注</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">{record.notes}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
在新工单中引用
屏蔽一个用户