比较提交
2 次代码提交
67b27e3551
...
669497e625
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
669497e625 | ||
|
|
71caf0de19 |
@@ -8,6 +8,22 @@ export type ChangeLogEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGE_LOG_ENTRIES: ChangeLogEntry[] = [
|
export const CHANGE_LOG_ENTRIES: ChangeLogEntry[] = [
|
||||||
|
{
|
||||||
|
version: "2026.03.15-progress-time-actions",
|
||||||
|
releaseDate: "2026-03-15",
|
||||||
|
repoVersion: "71caf0d",
|
||||||
|
summary: "最近训练记录默认显示具体上海时间,并直接展示录制动作数据摘要。",
|
||||||
|
features: [
|
||||||
|
"最近训练记录摘要行默认显示到秒的 Asia/Shanghai 时间",
|
||||||
|
"录制记录列表直接展示主动作和前 3 个动作统计,无需先展开",
|
||||||
|
"展开态动作明细统一用中文动作标签展示",
|
||||||
|
"提醒页通知时间统一切换为 Asia/Shanghai",
|
||||||
|
],
|
||||||
|
tests: [
|
||||||
|
"pnpm check",
|
||||||
|
"pnpm build",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: "2026.03.15-session-changelog",
|
version: "2026.03.15-session-changelog",
|
||||||
releaseDate: "2026-03-15",
|
releaseDate: "2026-03-15",
|
||||||
|
|||||||
@@ -13,6 +13,28 @@ import {
|
|||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
|
|
||||||
|
const ACTION_LABEL_MAP: Record<string, string> = {
|
||||||
|
forehand: "正手挥拍",
|
||||||
|
backhand: "反手挥拍",
|
||||||
|
serve: "发球",
|
||||||
|
volley: "截击",
|
||||||
|
overhead: "高压",
|
||||||
|
slice: "切削",
|
||||||
|
lob: "挑高球",
|
||||||
|
unknown: "未知动作",
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRecordMetadata(record: any) {
|
||||||
|
if (!record?.metadata || typeof record.metadata !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return record.metadata as Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActionLabel(actionType: string) {
|
||||||
|
return ACTION_LABEL_MAP[actionType] || actionType;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Progress() {
|
export default function Progress() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { data: records, isLoading } = trpc.record.list.useQuery({ limit: 100 });
|
const { data: records, isLoading } = trpc.record.list.useQuery({ limit: 100 });
|
||||||
@@ -181,7 +203,16 @@ export default function Progress() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
{(records?.length || 0) > 0 ? (
|
{(records?.length || 0) > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{(records || []).slice(0, 20).map((record: any) => (
|
{(records || []).slice(0, 20).map((record: any) => {
|
||||||
|
const metadata = getRecordMetadata(record);
|
||||||
|
const actionSummary = metadata?.actionSummary && typeof metadata.actionSummary === "object"
|
||||||
|
? Object.entries(metadata.actionSummary as Record<string, number>).filter(([, count]) => Number(count) > 0)
|
||||||
|
: [];
|
||||||
|
const topActions = actionSummary
|
||||||
|
.sort((left, right) => Number(right[1]) - Number(left[1]))
|
||||||
|
.slice(0, 3);
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={record.id} className="border-b py-2 last:border-0">
|
<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 justify-between gap-3">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
@@ -193,15 +224,27 @@ export default function Progress() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">{record.exerciseName}</p>
|
<p className="text-sm font-medium">{record.exerciseName}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{formatDateTimeShanghai(record.trainingDate || record.createdAt)}
|
{formatDateTimeShanghai(record.trainingDate || record.createdAt, { second: "2-digit" })}
|
||||||
{record.durationMinutes ? ` · ${record.durationMinutes}分钟` : ""}
|
{record.durationMinutes ? ` · ${record.durationMinutes}分钟` : ""}
|
||||||
{record.sourceType ? ` · ${record.sourceType}` : ""}
|
{record.sourceType ? ` · ${record.sourceType}` : ""}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||||
{record.actionCount ? (
|
{record.actionCount ? (
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<Badge variant="outline" className="text-[11px]">
|
||||||
动作数 {record.actionCount}
|
动作数 {record.actionCount}
|
||||||
</p>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
|
{metadata?.dominantAction ? (
|
||||||
|
<Badge variant="secondary" className="text-[11px]">
|
||||||
|
主动作 {getActionLabel(String(metadata.dominantAction))}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
{topActions.map(([actionType, count]) => (
|
||||||
|
<Badge key={`${record.id}-${actionType}`} variant="secondary" className="text-[11px]">
|
||||||
|
{getActionLabel(actionType)} {count} 次
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -236,36 +279,36 @@ export default function Progress() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{record.metadata ? (
|
{metadata ? (
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4 space-y-3">
|
||||||
{record.metadata.dominantAction ? (
|
{metadata.dominantAction ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">主动作</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 className="mt-1 font-medium">{getActionLabel(String(metadata.dominantAction))}</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{record.metadata.actionSummary && Object.keys(record.metadata.actionSummary).length > 0 ? (
|
{metadata.actionSummary && Object.keys(metadata.actionSummary).length > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">动作明细</div>
|
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">动作明细</div>
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
{Object.entries(record.metadata.actionSummary as Record<string, number>)
|
{Object.entries(metadata.actionSummary as Record<string, number>)
|
||||||
.filter(([, count]) => Number(count) > 0)
|
.filter(([, count]) => Number(count) > 0)
|
||||||
.map(([actionType, count]) => (
|
.map(([actionType, count]) => (
|
||||||
<Badge key={actionType} variant="secondary">
|
<Badge key={actionType} variant="secondary">
|
||||||
{actionType} {count} 次
|
{getActionLabel(actionType)} {count} 次
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{record.metadata.validityStatus ? (
|
{metadata.validityStatus ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">录制有效性</div>
|
<div className="text-xs uppercase tracking-[0.16em] text-muted-foreground">录制有效性</div>
|
||||||
<div className="mt-1 font-medium">{String(record.metadata.validityStatus)}</div>
|
<div className="mt-1 font-medium">{String(metadata.validityStatus)}</div>
|
||||||
{record.metadata.invalidReason ? (
|
{metadata.invalidReason ? (
|
||||||
<div className="mt-1 text-xs text-muted-foreground">{String(record.metadata.invalidReason)}</div>
|
<div className="mt-1 text-xs text-muted-foreground">{String(metadata.invalidReason)}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -281,7 +324,8 @@ export default function Progress() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-8 text-center text-muted-foreground text-sm">
|
<div className="py-8 text-center text-muted-foreground text-sm">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useAuth } from "@/_core/hooks/useAuth";
|
import { useAuth } from "@/_core/hooks/useAuth";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
|
import { formatDateTimeShanghai } from "@/lib/time";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -458,7 +459,12 @@ export default function Reminders() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap ml-2">
|
<span className="text-xs text-muted-foreground whitespace-nowrap ml-2">
|
||||||
{new Date(notif.createdAt).toLocaleString("zh-CN", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}
|
{formatDateTimeShanghai(notif.createdAt, {
|
||||||
|
year: undefined,
|
||||||
|
second: undefined,
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# Tennis Training Hub - 变更日志
|
# Tennis Training Hub - 变更日志
|
||||||
|
|
||||||
|
## 2026.03.15-progress-time-actions (2026-03-15)
|
||||||
|
|
||||||
|
### 功能更新
|
||||||
|
|
||||||
|
- 最近训练记录摘要行默认显示到秒的具体时间,统一按 `Asia/Shanghai` 展示
|
||||||
|
- 录制类训练记录在列表中直接显示动作数、主动作和前 3 个动作统计
|
||||||
|
- 训练记录展开态中的动作明细改为中文动作标签,便于直接阅读
|
||||||
|
- 提醒页通知时间统一切换为 `Asia/Shanghai`
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
- `pnpm check`
|
||||||
|
- `pnpm build`
|
||||||
|
|
||||||
|
### 仓库版本
|
||||||
|
|
||||||
|
- `71caf0d`
|
||||||
|
|
||||||
## 2026.03.15-session-changelog (2026-03-15)
|
## 2026.03.15-session-changelog (2026-03-15)
|
||||||
|
|
||||||
### 功能更新
|
### 功能更新
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户