diff --git a/docs/更新说明.md b/docs/更新说明.md index ea74477..e95ad08 100644 --- a/docs/更新说明.md +++ b/docs/更新说明.md @@ -5,6 +5,15 @@ --- +## 2026-07-27 — 交易记录详情改为弹窗 + +### 变更 + +1. 点击成交组弹出对话框查看开平仓时间、持仓周期、成交明细与盈亏。 +2. 支持点遮罩 / 关闭按钮 / Esc 关闭;手机端底部抽屉式布局。 + +--- + ## 2026-07-27 — 修复交易记录 500(记录未丢) ### 变更 diff --git a/frontend/src/pages/Trades.tsx b/frontend/src/pages/Trades.tsx index 7e8fade..f475524 100644 --- a/frontend/src/pages/Trades.tsx +++ b/frontend/src/pages/Trades.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useId, useState } from "react"; import { apiFetch } from "../api/client"; import { closeReasonZh, @@ -82,11 +82,14 @@ function fmtHold(ms: number | null | undefined) { } export default function TradesPage() { + const titleId = useId(); const [groups, setGroups] = useState([]); const [selected, setSelected] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null); const [fills, setFills] = useState([]); const [summary, setSummary] = useState(null); + const [detailLoading, setDetailLoading] = useState(false); + const [detailErr, setDetailErr] = useState(""); const [err, setErr] = useState(""); useEffect(() => { @@ -95,10 +98,36 @@ export default function TradesPage() { .catch((e) => setErr(e instanceof Error ? e.message : String(e))); }, []); + useEffect(() => { + if (!selected) return; + const onKey = (ev: KeyboardEvent) => { + if (ev.key === "Escape") closeDetail(); + }; + window.addEventListener("keydown", onKey); + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + window.removeEventListener("keydown", onKey); + document.body.style.overflow = prev; + }; + }, [selected]); + + function closeDetail() { + setSelected(null); + setSelectedGroup(null); + setFills([]); + setSummary(null); + setDetailErr(""); + setDetailLoading(false); + } + async function openGroup(id: string) { setSelected(id); setSummary(null); setSelectedGroup(null); + setFills([]); + setDetailErr(""); + setDetailLoading(true); try { const r = await apiFetch<{ group: Group; @@ -109,7 +138,9 @@ export default function TradesPage() { setSummary(r.pnl_summary); setSelectedGroup(r.group); } catch (e) { - setErr(e instanceof Error ? e.message : String(e)); + setDetailErr(e instanceof Error ? e.message : String(e)); + } finally { + setDetailLoading(false); } } @@ -117,7 +148,7 @@ export default function TradesPage() {

交易记录

- 持仓周期按目标平仓计时;净盈利达标且只平永续时,以永续平仓时间为准(不含期权残留至到期)。 + 持仓周期按目标平仓计时;净盈利达标且只平永续时,以永续平仓时间为准(不含期权残留至到期)。点击一条可查看明细。

{err ?
{err}
: null}
@@ -160,83 +191,139 @@ export default function TradesPage() { }) )}
+ {selected ? ( -
-

{selected} 成交明细

- {selectedGroup ? ( -
-
- 开仓时间 - - {fmtTime(selectedGroup.hold_open_at_ms ?? selectedGroup.open_at_ms)} - -
-
- 平仓时间 - - {fmtTime( - selectedGroup.hold_close_at_ms ?? selectedGroup.close_at_ms, - )} - {selectedGroup.hold_basis === "perp" ? ( - (永续) +
+
ev.stopPropagation()} + > +
+

+ {selected} +

+ +
+ +
+ {detailLoading ? ( +

加载明细…

+ ) : null} + {detailErr ?
{detailErr}
: null} + + {!detailLoading && !detailErr && selectedGroup ? ( + <> +
+
+ 状态 + + {statusZh(selectedGroup.status)} + {selectedGroup.close_reason + ? ` · ${closeReasonZh(selectedGroup.close_reason)}` + : ""} + +
+
+ 方向 + + {positionSidesZh( + selectedGroup.perp_side, + selectedGroup.option_side, + )} + +
+
+ 开仓时间 + + {fmtTime( + selectedGroup.hold_open_at_ms ?? + selectedGroup.open_at_ms, + )} + +
+
+ 平仓时间 + + {fmtTime( + selectedGroup.hold_close_at_ms ?? + selectedGroup.close_at_ms, + )} + {selectedGroup.hold_basis === "perp" ? ( + (永续) + ) : null} + +
+
+ 持仓周期 + + {fmtHold(selectedGroup.hold_ms)} + +
+
+ +

+ 成交价为成交均价(未预先扣费);手续费单独列出。净盈亏 = 期权盈亏 + + 永续盈亏 − 全部手续费。 +

+ + {fills.map((f) => ( +
+ + {fillDescZh(f.leg, f.action, f.side)} + + + 价 {f.fill_px.toFixed(4)} · 数量 {f.qty_eth} · 手续费{" "} + {f.fee.toFixed(4)} + +
+ ))} + + {summary ? ( +
+
+ 期权盈亏 + + {fmt(summary.option_pnl)} + +
+
+ 永续盈亏 + + {fmt(summary.perp_pnl)} + +
+
+ 手续费合计 + {fmt(summary.fees_total, 4)} +
+
+ + 净盈亏(扣费后) + + + {fmt(summary.net_pnl)} + +
+
) : null} - -
-
- 持仓周期 - {fmtHold(selectedGroup.hold_ms)} -
+ + ) : null}
- ) : null} -

- 成交价为成交均价(未预先扣费);手续费单独列出。净盈亏 = 期权盈亏 + 永续盈亏 − - 全部手续费。 -

- {fills.map((f) => ( -
- - {fillDescZh(f.leg, f.action, f.side)} - - - 价 {f.fill_px.toFixed(4)} · 数量 {f.qty_eth} · 手续费{" "} - {f.fee.toFixed(4)} - -
- ))} - {summary ? ( -
-
- 期权盈亏 - - {fmt(summary.option_pnl)} - -
-
- 永续盈亏 - - {fmt(summary.perp_pnl)} - -
-
- 手续费合计 - {fmt(summary.fees_total, 4)} -
-
- - 净盈亏(扣费后) - - - {fmt(summary.net_pnl)} - -
-
- ) : null} +
) : null}
diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index e277abf..2d78a74 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -584,6 +584,96 @@ input { font-size: 12px; } +.trade-detail-hint { + color: var(--muted); + font-size: 13px; + margin: 0 0 12px; + line-height: 1.45; +} + +.trade-pnl-block { + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid var(--line); +} + +.modal-backdrop { + position: fixed; + inset: 0; + z-index: 80; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); + background: rgba(0, 0, 0, 0.62); + backdrop-filter: blur(4px); +} + +.modal-dialog { + width: min(560px, 100%); + max-height: min(86vh, 820px); + display: flex; + flex-direction: column; + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: 12px; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45); + overflow: hidden; +} + +.modal-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 16px; + border-bottom: 1px solid var(--line); + flex: 0 0 auto; +} + +.modal-title { + margin: 0; + font-size: 16px; + font-weight: 700; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.modal-close { + flex: 0 0 auto; + min-height: 40px; +} + +.modal-body { + padding: 14px 16px 18px; + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +@media (max-width: 900px) { + .modal-backdrop { + align-items: flex-end; + padding: 0; + } + + .modal-dialog { + width: 100%; + max-height: min(88vh, 900px); + border-radius: 14px 14px 0 0; + } + + .modal-head { + padding: 12px 14px; + } + + .modal-body { + padding: 12px 14px calc(18px + env(safe-area-inset-bottom, 0px)); + } +} + .trade-row-pnl { display: flex; flex-direction: column;