Add delete for closed trade records.
Only closed groups can be removed from local history; equity is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+130
-42
@@ -157,14 +157,24 @@ export default function TradesPage() {
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detailErr, setDetailErr] = useState("");
|
||||
const [err, setErr] = useState("");
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<{ groups: Group[] }>("/api/trades/groups")
|
||||
function loadGroups() {
|
||||
return apiFetch<{ groups: Group[] }>("/api/trades/groups")
|
||||
.then((r) => {
|
||||
setGroups(r.groups);
|
||||
setPage(1);
|
||||
return r.groups;
|
||||
})
|
||||
.catch((e) => setErr(e instanceof Error ? e.message : String(e)));
|
||||
.catch((e) => {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadGroups().then((list) => {
|
||||
if (list) setPage(1);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -220,41 +230,82 @@ export default function TradesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteGroup(id: string, status: string) {
|
||||
if (String(status).toLowerCase() !== "closed") {
|
||||
window.alert("只能删除已平仓记录");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!window.confirm(
|
||||
`确认删除交易记录 ${id}?\n仅从本系统移除记录,不会回滚资金,也不会动交易所仓位。`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDeletingId(id);
|
||||
setErr("");
|
||||
try {
|
||||
await apiFetch<{ ok: boolean }>(`/api/trades/groups/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (selected === id) closeDetail();
|
||||
setGroups((prev) => prev.filter((g) => g.group_id !== id));
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMobileRow(g: Group) {
|
||||
const listPnl = groupPnl(g);
|
||||
const openMs = g.hold_open_at_ms ?? g.open_at_ms;
|
||||
const closeMs = g.hold_close_at_ms ?? g.close_at_ms;
|
||||
const canDelete = String(g.status).toLowerCase() === "closed";
|
||||
return (
|
||||
<button
|
||||
key={g.group_id}
|
||||
type="button"
|
||||
className="trade-row"
|
||||
onClick={() => openGroup(g.group_id)}
|
||||
>
|
||||
<div className="trade-row-main mono">
|
||||
<span className="trade-row-id">{g.group_id}</span>
|
||||
<span className="trade-row-meta">
|
||||
{statusZh(g.status)} ·{" "}
|
||||
{positionSidesZh(g.perp_side, g.option_side)}
|
||||
</span>
|
||||
<span className="trade-row-times">
|
||||
开 {fmtTime(openMs)} · 平 {fmtTime(closeMs)} · 周期{" "}
|
||||
{fmtHold(g.hold_ms)}
|
||||
{g.move_points != null ? ` · 波动 ${fmtMovePoints(g.move_points)}` : ""}
|
||||
{g.option_leverage != null
|
||||
? ` · 期权杠杆 ${fmt(g.option_leverage, 0)}x`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`trade-row-pnl mono ${pnlClass(listPnl)}`}>
|
||||
<span>净盈亏 {fmt(listPnl)}</span>
|
||||
{g.close_reason ? (
|
||||
<span className="trade-row-reason">
|
||||
{closeReasonZh(g.close_reason)}
|
||||
<div key={g.group_id} className="trade-row-wrap">
|
||||
<button
|
||||
type="button"
|
||||
className="trade-row"
|
||||
onClick={() => openGroup(g.group_id)}
|
||||
>
|
||||
<div className="trade-row-main mono">
|
||||
<span className="trade-row-id">{g.group_id}</span>
|
||||
<span className="trade-row-meta">
|
||||
{statusZh(g.status)} ·{" "}
|
||||
{positionSidesZh(g.perp_side, g.option_side)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
<span className="trade-row-times">
|
||||
开 {fmtTime(openMs)} · 平 {fmtTime(closeMs)} · 周期{" "}
|
||||
{fmtHold(g.hold_ms)}
|
||||
{g.move_points != null
|
||||
? ` · 波动 ${fmtMovePoints(g.move_points)}`
|
||||
: ""}
|
||||
{g.option_leverage != null
|
||||
? ` · 期权杠杆 ${fmt(g.option_leverage, 0)}x`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`trade-row-pnl mono ${pnlClass(listPnl)}`}>
|
||||
<span>净盈亏 {fmt(listPnl)}</span>
|
||||
{g.close_reason ? (
|
||||
<span className="trade-row-reason">
|
||||
{closeReasonZh(g.close_reason)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
{canDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost trade-row-delete"
|
||||
disabled={deletingId === g.group_id}
|
||||
onClick={() => void deleteGroup(g.group_id, g.status)}
|
||||
>
|
||||
{deletingId === g.group_id ? "删除中…" : "删除"}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -262,7 +313,7 @@ export default function TradesPage() {
|
||||
<div className="trades-page">
|
||||
<h2 style={{ marginTop: 0 }}>交易记录</h2>
|
||||
<p className="trade-hold-note">
|
||||
持仓周期按目标平仓计时;净盈利达标且只平永续时,以永续平仓时间为准(不含期权残留至到期)。点击一条可查看明细。
|
||||
持仓周期按目标平仓计时;净盈利达标且只平永续时,以永续平仓时间为准(不含期权残留至到期)。点击一条可查看明细。已平仓记录可删除(仅清本地记录,不回滚资金)。
|
||||
</p>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
|
||||
@@ -296,6 +347,7 @@ export default function TradesPage() {
|
||||
<th>波动点数</th>
|
||||
<th>盈亏金额</th>
|
||||
<th>平仓方式</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -304,6 +356,8 @@ export default function TradesPage() {
|
||||
const openMs = g.hold_open_at_ms ?? g.open_at_ms;
|
||||
const closeMs = g.hold_close_at_ms ?? g.close_at_ms;
|
||||
const seq = (safePage - 1) * PAGE_SIZE + i + 1;
|
||||
const canDelete =
|
||||
String(g.status).toLowerCase() === "closed";
|
||||
return (
|
||||
<tr
|
||||
key={g.group_id}
|
||||
@@ -333,6 +387,25 @@ export default function TradesPage() {
|
||||
? closeReasonZh(g.close_reason)
|
||||
: "—"}
|
||||
</td>
|
||||
<td
|
||||
className="trade-table-actions"
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
>
|
||||
{canDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost trade-delete-btn"
|
||||
disabled={deletingId === g.group_id}
|
||||
onClick={() =>
|
||||
void deleteGroup(g.group_id, g.status)
|
||||
}
|
||||
>
|
||||
{deletingId === g.group_id ? "删除中…" : "删除"}
|
||||
</button>
|
||||
) : (
|
||||
<span className="meta">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -381,14 +454,29 @@ export default function TradesPage() {
|
||||
<h3 id={titleId} className="modal-title mono">
|
||||
{selected}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost modal-close"
|
||||
onClick={closeDetail}
|
||||
aria-label="关闭"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<div className="modal-head-actions">
|
||||
{selectedGroup &&
|
||||
String(selectedGroup.status).toLowerCase() === "closed" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn danger"
|
||||
disabled={deletingId === selected}
|
||||
onClick={() =>
|
||||
void deleteGroup(selected, selectedGroup.status)
|
||||
}
|
||||
>
|
||||
{deletingId === selected ? "删除中…" : "删除记录"}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost modal-close"
|
||||
onClick={closeDetail}
|
||||
aria-label="关闭"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
|
||||
@@ -849,6 +849,46 @@ input {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.trade-table-actions {
|
||||
width: 1%;
|
||||
}
|
||||
|
||||
.trade-delete-btn {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trade-row-wrap {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.trade-row-wrap:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.trade-row-wrap .trade-row {
|
||||
flex: 1;
|
||||
border-bottom: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.trade-row-delete {
|
||||
align-self: center;
|
||||
flex-shrink: 0;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
color: var(--danger, #e85d5d);
|
||||
}
|
||||
|
||||
.modal-head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.trade-pager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user