История: изменение рейтинга на карточках партий профиля
GET /users/{id}/matches отдаёт у каждой партии rating_delta — изменение
общего рейтинга владельца истории за неё (один знак после запятой), в обоих
режимах, включая «лучшую партию»; история и рейтинг считаются одним
проигрыванием. В списке партий группы поле пустое: там непонятно, чья это
дельта.
Фронт: компактная карточка истории — второй строкой «Рейтинг: +12.3»,
подробная (MatchListView) — последней строкой, только когда дельта есть.
Рост — зелёным, падение — красным, минус типографский (formatRatingDelta).
schema.d.ts перегенерирован.
Тест: дуэль новичков ±32 в обеих историях, реванш с обратными знаками,
сумма изменений сходится с рейтингом, в списке группы дельты нет, режим
«лучшая» её отдаёт. #77
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
This commit is contained in:
Vendored
+2
@@ -1888,6 +1888,8 @@ export interface components {
|
||||
* @default []
|
||||
*/
|
||||
participants: components["schemas"]["MatchListParticipant"][];
|
||||
/** Rating Delta */
|
||||
rating_delta?: number | null;
|
||||
};
|
||||
/** MatchListParticipant */
|
||||
MatchListParticipant: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { formatDate, formatDuration } from "../domain/format";
|
||||
import type { MatchListItem } from "../domain/types";
|
||||
import { MatchListView } from "./MatchList";
|
||||
import { RatingDelta } from "./RatingDelta";
|
||||
|
||||
/**
|
||||
* История партий игрока в профиле. Подробный режим — тот же список, что у группы
|
||||
@@ -53,29 +54,33 @@ export function MatchHistory({
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
className="card row-between"
|
||||
className="card"
|
||||
style={{ margin: 0, width: "100%", textAlign: "left" }}
|
||||
onClick={() => navigate(`/match/${m.id}`)}
|
||||
>
|
||||
<div className="row" style={{ gap: 10, minWidth: 0 }}>
|
||||
<span
|
||||
className={
|
||||
"place-badge" +
|
||||
(mine?.place === 1 && !mine?.eliminated ? " first" : "") +
|
||||
(mine?.eliminated ? " eliminated" : "")
|
||||
}
|
||||
>
|
||||
{mine?.eliminated ? "✕" : (mine?.place ?? "—")}
|
||||
</span>
|
||||
<span
|
||||
style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||
>
|
||||
{mine?.faction_name ?? "—"}
|
||||
<div className="row-between">
|
||||
<div className="row" style={{ gap: 10, minWidth: 0 }}>
|
||||
<span
|
||||
className={
|
||||
"place-badge" +
|
||||
(mine?.place === 1 && !mine?.eliminated ? " first" : "") +
|
||||
(mine?.eliminated ? " eliminated" : "")
|
||||
}
|
||||
>
|
||||
{mine?.eliminated ? "✕" : (mine?.place ?? "—")}
|
||||
</span>
|
||||
<span
|
||||
style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||
>
|
||||
{mine?.faction_name ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<span className="muted small" style={{ flexShrink: 0 }}>
|
||||
{formatDate(m.played_at)} · {formatDuration(m.duration_minutes)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="muted small" style={{ flexShrink: 0 }}>
|
||||
{formatDate(m.played_at)} · {formatDuration(m.duration_minutes)}
|
||||
</span>
|
||||
{/* Второй строкой — изменение рейтинга владельца профиля за партию. */}
|
||||
{m.rating_delta != null && <RatingDelta delta={m.rating_delta} />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,10 +3,12 @@ import { useNavigate } from "react-router-dom";
|
||||
import { formatDate, formatDuration } from "../domain/format";
|
||||
import { winReasonLabel } from "../domain/winReasons";
|
||||
import type { MatchListItem } from "../domain/types";
|
||||
import { RatingDelta } from "./RatingDelta";
|
||||
|
||||
/** Список ЗАВЕРШЁННЫХ партий. Незавершённые сюда не попадают: группа отдаёт их
|
||||
* отдельным блоком (InProgressMatches), а история профиля приходит с бэкенда уже
|
||||
* отфильтрованной по status="finished". */
|
||||
* отфильтрованной по status="finished". В истории профиля у партии есть rating_delta
|
||||
* владельца — она идёт последней строкой; у списка группы её нет. */
|
||||
export function MatchListView({ items }: { items: MatchListItem[] }) {
|
||||
const navigate = useNavigate();
|
||||
if (items.length === 0) return <div className="muted small">Партий пока нет.</div>;
|
||||
@@ -48,6 +50,7 @@ export function MatchListView({ items }: { items: MatchListItem[] }) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{m.rating_delta != null && <RatingDelta delta={m.rating_delta} />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { formatRatingDelta } from "../domain/format";
|
||||
|
||||
/** Строка карточки истории: сколько общего рейтинга владелец профиля получил или
|
||||
* потерял за партию. Рост — зелёным, падение — красным, ноль — без цвета. */
|
||||
export function RatingDelta({ delta }: { delta: number }) {
|
||||
const text = formatRatingDelta(delta);
|
||||
const tone = text.startsWith("+") ? "rating-up" : text.startsWith("−") ? "rating-down" : undefined;
|
||||
return (
|
||||
<div className="small rating-delta">
|
||||
Рейтинг: <b className={tone}>{text}</b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,14 @@ export function plural(n: number, one: string, few: string, many: string): strin
|
||||
return many;
|
||||
}
|
||||
|
||||
// Изменение рейтинга за партию: всегда со знаком и одним знаком после запятой
|
||||
// («+12.3», «−4.1», «0.0»); минус — типографский, как в справке.
|
||||
export function formatRatingDelta(delta: number): string {
|
||||
const abs = Math.abs(delta).toFixed(1);
|
||||
if (abs === "0.0") return "0.0";
|
||||
return `${delta > 0 ? "+" : "−"}${abs}`;
|
||||
}
|
||||
|
||||
export function formatDuration(minutes: number | null | undefined): string {
|
||||
if (minutes == null) return "—";
|
||||
const h = Math.floor(minutes / 60);
|
||||
|
||||
@@ -103,6 +103,11 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
||||
|
||||
.error-text { color: var(--danger); font-size: 13px; }
|
||||
|
||||
/* Изменение рейтинга за партию в истории профиля */
|
||||
.rating-delta { color: var(--text-muted); margin-top: 6px; }
|
||||
.rating-up { color: var(--success); }
|
||||
.rating-down { color: var(--danger); }
|
||||
|
||||
/* Формула в справке */
|
||||
.formula {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
|
||||
Reference in New Issue
Block a user