From 6a578cc5985bacfc886aa5bd8bdd87ab2ec7fdaa Mon Sep 17 00:00:00 2001 From: AdamGao Date: Tue, 8 Sep 2026 11:13:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(UI):=20=E4=BA=A4=E6=98=93=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=BD=92=E5=B1=9E=E5=88=86=E9=85=8D=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=20Modal=20=E5=BC=B9=E7=AA=97=EF=BC=8C=E8=A1=8C=E5=86=85?= =?UTF-8?q?=E5=8F=AA=E7=95=99=E7=B2=BE=E7=AE=80=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/views/TradeRecordsTab.jsx | 268 +++++++++++++++++---------- 1 file changed, 165 insertions(+), 103 deletions(-) diff --git a/src/client/views/TradeRecordsTab.jsx b/src/client/views/TradeRecordsTab.jsx index c7c602e..8c5a2c0 100644 --- a/src/client/views/TradeRecordsTab.jsx +++ b/src/client/views/TradeRecordsTab.jsx @@ -505,43 +505,23 @@ function OrderRows({ order, expanded, onToggle, targets, loadTargets, saveSegmen } /** - * R-018 归属单元格:展示段 chips;点开进入份额分配器(AttributionEditor)。 - * - 已关联段:策略名 + 量(段 chip);未关联(余额 > 0 且无段)显示「未关联」 - * - 点「分配」→ 打开编辑器(加载 targets 惰性) + * R-018 归属单元格(行内精简态):只显示段 chips / 未关联 + 剩余提示 + 一个入口按钮。 + * 份额分配操作在 AttributionModal(弹窗)中完成 —— 不在表格行内堆大量控件。 */ function AttributionCell({ order, segments, totalSeg, targets, loadTargets, saveSegments, onNotice }) { - const [editing, setEditing] = useState(false); + const [open, setOpen] = useState(false); const hasSeg = (segments ?? []).length > 0; const aggVolume = Number(order.aggVolume ?? order.tradedVolume ?? 0); - const leftover = Math.max(0, aggVolume - totalSeg); // 未分配余额(今日实时 aggVolume=已成交量;历史 aggVolume 由 fills 聚合并) - - const open = () => { - loadTargets(); - setEditing(true); - }; - - if (editing) { - return ( - setEditing(false)} - onNotice={onNotice} - /> - ); - } + const leftover = Math.max(0, aggVolume - totalSeg); // 未分配余额 return ( - + {hasSeg ? ( - + {(segments ?? []).map((s) => ( - {(s.strategyName || s.strategyId)}·{fmtNum(s.volume, 0)} + {s.strategyName || s.strategyId} ))} @@ -557,14 +537,17 @@ function AttributionCell({ order, segments, totalSeg, targets, loadTargets, save 未关联 )} {leftover > 1e-9 && ( - - 未分配剩余 {fmtNum(leftover, 0)} 股 + + 剩{fmtNum(leftover, 0)} )} + {open && ( + setOpen(false)} + onNotice={onNotice} + /> + )} ); } /** - * R-018 份额分配器(编辑器):策略下拉 + 段量输入 + 段列表(可撤)。 - * - 余额 = 已成交量 − 已分配段和(默认输入 = 余额全量,可改小) - * - 每步操作(加段/撤段)即时 saveSegments(全量替换,原子) + * R-018 归属分配弹窗(Modal):居中遮罩 + 面板。 + * - 面板:委托摘要(code/名称/方向/已成交量)+ 已分配段列表(每段可撤)+ 新增段(策略下拉 + 量输入,默认余额全量) + * - 操作即时保存(setSegments 全量替换原子);顶部余额与状态实时反馈 */ -function AttributionEditor({ order, segments, leftover, targets, saveSegments, onDone, onNotice }) { +function AttributionModal({ order, segments, aggVolume, totalSeg, leftover, targets, saveSegments, onClose, onNotice }) { const list = (targets ?? []).length > 0 ? targets : null; const [selStrategy, setSelStrategy] = useState(''); const [amount, setAmount] = useState(''); const [busy, setBusy] = useState(false); - const aggVolume = Number(order.aggVolume ?? order.tradedVolume ?? 0); const segs = Array.isArray(segments) ? segments : []; - const totalSeg = segs.reduce((s, x) => s + Number(x?.volume ?? 0), 0); - const remaining = Math.max(0, aggVolume - totalSeg); + const total = segs.reduce((s, x) => s + Number(x?.volume ?? 0), 0); + const remaining = Math.max(0, aggVolume - total); + const fullyAllocated = remaining <= 1e-9; + const amountHint = remaining > 0 ? String(remaining) : ''; const notice = (msg, isErr) => { onNotice?.(msg); - if (isErr) setTimeout(() => onNotice?.(null), 3000); - else setTimeout(() => onNotice?.(null), 2000); + setTimeout(() => onNotice?.(null), isErr ? 3000 : 2000); }; const run = async (nextSegments) => { @@ -614,14 +610,10 @@ function AttributionEditor({ order, segments, leftover, targets, saveSegments, o const v = Number(amount); if (!Number.isFinite(v) || v <= 0) { notice('请输入有效份额量', true); return; } if (v > remaining + 1e-9) { notice('份额量超过未分配余额 ' + fmtNum(remaining, 0), true); return; } - // 卖出场景:若该策略无活动仓(activityShares=0 且方向 sell)→ 服务端会拒绝;这里给前端提示 const t = (targets ?? []).find((x) => x.strategyId === selStrategy); const isSell = String(order.direction ?? '') === 'sell'; - if (isSell && t && !t.hasActiveHolding) { - notice('该策略无此持仓可减(关联失败)', true); - return; - } - // sell 且 activityShares < amount → 自动截断为可容纳量(R4 老师拍板:自动截断续分) + if (isSell && t && !t.hasActiveHolding) { notice('该策略无此持仓可减(关联失败)', true); return; } + // sell 且 activityShares < 输入量 → 自动截断为可容纳量(R4:自动截断续分) let vol = v; if (isSell && t && t.hasActiveHolding && Number(t.activityShares) + 1e-9 < v) { vol = Number(t.activityShares); @@ -629,75 +621,145 @@ function AttributionEditor({ order, segments, leftover, targets, saveSegments, o } const merged = [...segs.filter((s) => s.strategyId !== selStrategy), { strategyId: selStrategy, volume: vol }]; await run(merged); + setAmount(''); }; const removeSegment = async (strategyId) => { - const merged = segs.filter((s) => s.strategyId !== strategyId); - await run(merged); + await run(segs.filter((s) => s.strategyId !== strategyId)); }; const clearAll = async () => { await run([]); }; - const amountDefaultHint = remaining > 0 ? String(remaining) : ''; - return ( - -
- 已成交量 {fmtNum(aggVolume, 0)} | 已分配 {fmtNum(totalSeg, 0)} | 余额 1e-9 ? 'var(--dsw-alias-state-error-primary, #d32f2f)' : 'var(--dsw-alias-state-success-primary, #1b5e20)', fontWeight: 'bold' }}>{fmtNum(remaining, 0)} -
- {(segs.length > 0) && ( - - {segs.map((s) => ( - - {s.strategyName || s.strategyId}·{fmtNum(s.volume, 0)} - !busy && removeSegment(s.strategyId)} - style={{ cursor: busy ? 'wait' : 'pointer', color: 'var(--dsw-alias-state-error-primary, #c62828)' }} - >✕ +
!busy && onClose()}> +
e.stopPropagation()} + style={{ + background: 'var(--dsw-alias-bg-layer-1, #fff)', borderRadius: 8, + maxWidth: 480, width: '92%', maxHeight: '80vh', overflow: 'auto', + boxShadow: '0 6px 24px rgba(0,0,0,0.18)', + }} + > + {/* 头部:委托摘要 + 关闭 */} +
+

+ {directionText(order.direction)} + {' '}{order.code} {order.name || ''} + + 委托 {order.orderId} - ))} - - )} - {list && list.length > 0 && remaining > 1e-9 && ( - - - setAmount(e.target.value)} - style={{ width: 70, padding: '1px 3px', fontSize: 11 }} - title="分配份额量(默认 = 未分配余额全量,可改小)" - /> - - - )} - {remaining <= 1e-9 && ( - 已全部分配 - )} - - {segs.length > 0 && ( -

+ +
+ + {/* 余额统计行 */} +
+ 已成交量 {fmtNum(aggVolume, 0)} + 已分配 {fmtNum(total, 0)} + + 待分配余额{' '} + + {fmtNum(remaining, 0)} + + + {fullyAllocated && 已全部分配} +
+ + {/* 已分配段列表 */} +
+ {segs.length === 0 ? ( +
+ 尚未关联任何策略(可在下方选择策略并分配份额;关联会同步调整对应策略持仓) +
+ ) : ( +
+ {segs.map((s) => ( +
+ {s.strategyName || s.strategyId} + {fmtNum(s.volume, 0)} 股 + +
+ ))} +
+ )} +
+ + {/* 新增段 */} +
+
+ 分配给策略{fullyAllocated ? '(已全部分配,如需调整请先撤除某段)' : ''} +
+
+ + setAmount(e.target.value)} + disabled={fullyAllocated || busy} + style={{ width: 110, padding: '4px 6px', fontSize: 13, border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4 }} + title={'分配份额量(默认 = 待分配余额 ' + fmtNum(remaining, 0) + ',可改小)'} + /> + +
+ {list === null && ( +
策略加载中…
+ )} +
+ + {/* 底部操作 */} +
+ - )} - - - + +
+
+
); }