feat(策略持仓): 表格加昨收/涨幅列(涨幅支持升降排序)+ 去掉总持仓列

- 昨收列:行情缓存 lastClose
- 涨幅列:(现价-昨收)/昨收,红涨绿跌着色
- 涨幅排序:点击表头降序/升序切换(▼/▲指示)
- 移除总持仓列
This commit is contained in:
2026-09-02 09:23:02 +08:00
parent 2aacd8a25d
commit d122d88bce
+60 -5
View File
@@ -15,7 +15,7 @@
* - 窄屏(<480px):每元素一行,两端对齐
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useRpc } from './connection.jsx';
import { LoadState } from './LoadState.jsx';
import { ToastProvider, useToast } from './Toast.jsx';
@@ -28,6 +28,30 @@ function fmtPrice(v) {
return Number(v).toFixed(2);
}
/** 涨幅计算:(现价 - 昨收) / 昨收 × 100%;缺数据返回 null */
function calcPctChange(snap) {
if (!snap) return null;
const last = Number(snap.lastPrice);
const close = Number(snap.lastClose);
if (!isFinite(last) || !isFinite(close) || close === 0) return null;
return ((last - close) / close) * 100;
}
/** 涨幅着色:涨红跌绿(A股习惯) */
function pctStyle(pct) {
if (pct == null) return { color: '#999' };
if (pct > 0) return { color: '#d32f2f', fontWeight: 'bold' };
if (pct < 0) return { color: '#2e7d32', fontWeight: 'bold' };
return { color: '#999' };
}
/** 涨幅文本:+x.xx% / -x.xx% / — */
function fmtPct(pct) {
if (pct == null || !isFinite(pct)) return '—';
const sign = pct > 0 ? '+' : '';
return sign + pct.toFixed(2) + '%';
}
/** 添加持仓区域响应式样式(内联注入,含媒体查询) */
const ADD_FORM_CSS = `
.odl-add-form {
@@ -120,6 +144,9 @@ function StrategyTabInner({ strategyId, strategyName }) {
const [expandedCode, setExpandedCode] = useState(null); // 当前展开的 code
const [holdingTrades, setHoldingTrades] = useState(null); // { code: [委托汇总] }
const [holdingTradesLoading, setHoldingTradesLoading] = useState(null); // 展开加载中的 code
// 涨幅排序(2026-09-02):sortKey='pctChange' 时按涨幅排序;null=默认顺序
const [sortKey, setSortKey] = useState(null); // 'pctChange' | null
const [sortDir, setSortDir] = useState('desc'); // 'asc' | 'desc'
const call = useRpc();
const toast = useToast();
const { getPrice, registerCodes } = useMarket();
@@ -226,8 +253,34 @@ function StrategyTabInner({ strategyId, strategyName }) {
}
};
/** 涨幅排序:点击表头切换升/降序 */
const togglePctSort = () => {
if (sortKey === 'pctChange') {
setSortDir((d) => (d === 'desc' ? 'asc' : 'desc'));
} else {
setSortKey('pctChange');
setSortDir('desc');
}
};
const name = strategyName || (strategyId === 'grid-supermarket' ? '网格策略持仓' : strategyId === 'manual-t' ? '做T持仓' : strategyId);
// 计算每行涨幅 + 按涨幅排序(sortKey='pctChange' 时)
const rowsForRender = useMemo(() => {
const rows = (positions ?? []).map((p) => ({ ...p, pctChange: calcPctChange(getPrice(p.code)) }));
if (sortKey === 'pctChange') {
const dir = sortDir === 'asc' ? 1 : -1;
rows.sort((a, b) => {
const va = a.pctChange ?? -Infinity;
const vb = b.pctChange ?? -Infinity;
return (va - vb) * dir;
});
}
return rows;
}, [positions, sortKey, sortDir, getPrice]);
const pctArrow = sortKey === 'pctChange' ? (sortDir === 'desc' ? ' ▼' : ' ▲') : '';
return (
<div className="odl-root" style={{ padding: 16, fontFamily: 'system-ui' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 8 }}>
@@ -287,15 +340,16 @@ function StrategyTabInner({ strategyId, strategyName }) {
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>代码</th>
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>名称</th>
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>现价</th>
<th onClick={togglePctSort} style={{ padding: '8px 6px', lineHeight: '24px', cursor: 'pointer', userSelect: 'none' }}>涨幅{pctArrow}</th>
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>昨收</th>
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>成本价</th>
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>最后一笔成交价</th>
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>策略份额</th>
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>总持仓</th>
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>操作</th>
</tr>
</thead>
<tbody>
{(positions ?? []).map((p) => {
{rowsForRender.map((p) => {
const isExpanded = expandedCode === p.code;
const trades = (holdingTrades ?? {})[p.code];
const isLoading = holdingTradesLoading === p.code;
@@ -340,10 +394,11 @@ function StrategyTabInner({ strategyId, strategyName }) {
<td style={{ padding: '8px 6px' }}>{p.code}</td>
<td style={{ padding: '8px 6px' }}>{p.name}</td>
<PriceCell price={getPrice(p.code)?.lastPrice} lastClose={getPrice(p.code)?.lastClose} />
<td style={{ padding: '8px 6px', ...pctStyle(p.pctChange) }}>{fmtPct(p.pctChange)}</td>
<td style={{ padding: '8px 6px' }}>{fmtPrice(getPrice(p.code)?.lastClose)}</td>
<td style={{ padding: '8px 6px' }}>{fmtPrice(p.avgPrice)}</td>
<td style={{ padding: '8px 6px' }}>{fmtPrice(p.lastTradePrice)}</td>
<td style={{ padding: '8px 6px', fontWeight: 'bold' }}>{p.shares}</td>
<td style={{ padding: '8px 6px' }}>{p.volume}</td>
<td style={{ padding: '8px 6px', whiteSpace: 'nowrap' }} onClick={(e) => e.stopPropagation()}>
{removeTarget && removeTarget.code === p.code ? (
<span>
@@ -383,7 +438,7 @@ function StrategyTabInner({ strategyId, strategyName }) {
</tr>
{isExpanded && (
<tr style={{ borderBottom: '1px solid #eee', background: '#fafafa' }}>
<td colSpan={9} style={{ padding: '4px 8px 12px' }}>
<td colSpan={10} style={{ padding: '4px 8px 12px' }}>
{isLoading ? (
<div style={{ padding: 8, color: '#999', fontSize: 12 }}>交易记录加载中...</div>
) : !trades || trades.length === 0 ? (