迭代14: 策略tab历史持仓展示(R-016)+ 今日已清仓默认层
- R-016 Q1-Q7: 两个策略 tab title 旁「历史持仓」开关 + 清仓范围筛选(近一周默认/1月/3月/半年/1年) - SqliteStore.getHoldingsHistory(strategy_id + closed_at 非空 + sinceMs,closed_at DESC,只读) - DataStore 门面透传 + strategy-holdings/history 端点(name 兜底 + 参数校验) - 前端: 开关/范围下拉/历史行灰显+「已清仓」徽标/展开复用 R-010(rowKey 唯一化) - R-016 二轮补充(Q8-Q9 老师拍板): 今日已清仓默认层 - range=today = 本地自然日 00:00 起(特判零点,不落回溯毫秒档) - 当天已清仓的持仓默认恒显示、不受「历史持仓」开关控制(开关只控今天之前) - rowsToRender 分层合成 + holdingId 去重;标题计数不含已清仓行;零写路径变更 - 回归: test-r016-history 24/24 + 存量回归全绿
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
* strategies/remove → 删除策略 { strategyId },联动清份额
|
||||
* strategies/move → 排序 { strategyId, dir }
|
||||
* strategy-positions → 某策略持仓 { strategyId }
|
||||
* strategy-holdings/history → 某策略已清仓历史持仓 { strategyId, range }(R-016)
|
||||
* unallocated → 未分配持仓
|
||||
* summary → 分仓摘要
|
||||
* add-shares → 添加份额 { code, strategyId, shares }
|
||||
|
||||
+53
-2
@@ -4,6 +4,7 @@
|
||||
* 端点:
|
||||
* strategies / strategies/update / strategies/add / strategies/remove
|
||||
* strategy-positions / unallocated / summary
|
||||
* strategy-holdings/history(R-016 迭代 14:已清仓历史持仓,范围筛选)
|
||||
* add-shares / move-all-shares / remove-shares
|
||||
* tabs / tabs/update
|
||||
*/
|
||||
@@ -14,6 +15,24 @@ import {
|
||||
getStrategyColumns, updateStrategyColumns,
|
||||
} from '../settings.js';
|
||||
|
||||
/** R-016 范围档 → 回溯毫秒(自然日近似,展示用途足够;技术约束-019)。
|
||||
* range='today' 不走回溯毫秒——二轮补充(2026-09-07 老师定):本地自然日 00:00 起清仓的持仓
|
||||
* 作为「今日已清仓」默认层(开关「历史持仓」不控制它,当天清仓默认显示),sinceMs 在 handler 内特判。 */
|
||||
const HISTORY_RANGE_MS = {
|
||||
week: 7 * 86400000,
|
||||
month: 30 * 86400000,
|
||||
quarter: 90 * 86400000,
|
||||
halfYear: 182 * 86400000,
|
||||
year: 365 * 86400000,
|
||||
};
|
||||
|
||||
/** 本地自然日零点毫秒(range='today' 用:今天 00:00 起 = 当日清仓的持仓) */
|
||||
function localDayStartMs() {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d.getTime();
|
||||
}
|
||||
|
||||
/** 策略/份额/tabs 端点方法表 */
|
||||
export const STRATEGY_METHODS = new Set([
|
||||
'strategies',
|
||||
@@ -31,15 +50,16 @@ export const STRATEGY_METHODS = new Set([
|
||||
'holdings/values-update', // R-013:写某持仓行的自定义字段值
|
||||
'strategy-columns', // 迭代11列显隐:读某策略列配置(归一化)
|
||||
'strategy-columns/update', // 迭代11列显隐:写某策略列配置
|
||||
'strategy-holdings/history', // R-016 迭代 14:某策略已清仓历史持仓(范围筛选,只读)
|
||||
]);
|
||||
|
||||
/**
|
||||
* 处理策略/份额/tabs 端点
|
||||
* @param {string} method
|
||||
* @param {object} args
|
||||
* @param {object} runtime { manager, settings }
|
||||
* @param {object} runtime { manager, settings, storage }
|
||||
*/
|
||||
export async function handleStrategy(method, args, { manager, settings }) {
|
||||
export async function handleStrategy(method, args, { manager, settings, storage }) {
|
||||
switch (method) {
|
||||
case 'strategies':
|
||||
return getStrategies(settings);
|
||||
@@ -78,6 +98,37 @@ export async function handleStrategy(method, args, { manager, settings }) {
|
||||
case 'strategy-columns/update':
|
||||
await updateStrategyColumns(settings, args.strategyId, args.columns);
|
||||
return getStrategyColumns(settings, args.strategyId);
|
||||
case 'strategy-holdings/history': {
|
||||
// R-016 迭代 14:某策略已清仓历史持仓(当前持仓快照路径零改动,技术约束-019)
|
||||
if (!args.strategyId) {
|
||||
throw Object.assign(new Error('缺少 strategyId 参数'), { code: 'bad-request' });
|
||||
}
|
||||
const range = args.range ?? 'week';
|
||||
// R-016 二轮补充(2026-09-07 老师定):range='today' = 本地自然日 00:00 起清仓的持仓
|
||||
// (前端默认层——当天清仓默认显示,与「历史持仓」开关解耦);其余 5 档维持回溯毫秒语义
|
||||
let sinceMs;
|
||||
if (range === 'today') {
|
||||
sinceMs = localDayStartMs();
|
||||
} else {
|
||||
const ms = HISTORY_RANGE_MS[range];
|
||||
if (!ms) {
|
||||
throw Object.assign(new Error('range 非法: ' + range + '(today|week|month|quarter|halfYear|year)'), { code: 'bad-request' });
|
||||
}
|
||||
sinceMs = Date.now() - ms;
|
||||
}
|
||||
const rows = await storage.getHoldingsHistory(args.strategyId, { sinceMs });
|
||||
// name 兜底:该 holding 最近一笔关联委托的 name(前端免二次请求)
|
||||
const ids = rows.map((r) => r.holdingId);
|
||||
const nameMap = new Map();
|
||||
if (ids.length > 0) {
|
||||
const ph = ids.map(() => '?').join(',');
|
||||
const orows = storage.sqlite.db.prepare(
|
||||
"SELECT holding_id, name FROM trade_orders WHERE holding_id IN (" + ph + ") AND name != '' ORDER BY insert_ts DESC"
|
||||
).all(...ids);
|
||||
for (const o of orows) if (!nameMap.has(o.holding_id)) nameMap.set(o.holding_id, o.name);
|
||||
}
|
||||
return rows.map((r) => ({ ...r, name: nameMap.get(r.holdingId) ?? null }));
|
||||
}
|
||||
default:
|
||||
throw Object.assign(new Error('unknown strategy method: ' + method), { code: 'not-found' });
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
* - 全部移入(O6:行内按钮,该票全部未分配份额移入当前策略)
|
||||
* - 操作反馈:Toast 轻提示
|
||||
* - 加载失败兜底:LoadState 三态
|
||||
* - 历史持仓展示(R-016 迭代 14):title 旁开关 + 清仓时间范围筛选(近一周默认/1个月/3个月/半年/1年),
|
||||
* 历史行灰显 + 「已清仓」徽标,展开复用 R-010 看关联交易(追溯该持仓的历史操作)
|
||||
* - 今日已清仓默认层(R-016 二轮补充,2026-09-07 老师定):当天(本地自然日 00:00 起)已清仓的持仓
|
||||
* **默认显示**、不受「历史持仓」开关控制——清仓转历史后 tab 不因此少行;开关只控制今天之前的历史
|
||||
*
|
||||
* 添加持仓区域为响应式布局(内联 <style> + 媒体查询):
|
||||
* - 宽屏:三元素一行(选择标的 | 份额 | 确认按钮)
|
||||
@@ -185,10 +189,38 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
||||
const [columnOpen, setColumnOpen] = useState(false);
|
||||
// 迭代11:正在单元格内编辑的自定义字段({code, key} | null)
|
||||
const [editingCell, setEditingCell] = useState(null);
|
||||
// R-016 迭代 14:历史持仓展示(title 旁开关 + 清仓范围筛选;会话级,不持久化,Q4)
|
||||
// R-016 二轮补充(2026-09-07 老师定):「今日已清仓」是**默认层**(range='today',恒显示),
|
||||
// 开关只控制今天之前的历史范围层(week/month/quarter/halfYear/year)——开关关≠当天清仓也被藏起
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const [historyRange, setHistoryRange] = useState('week'); // week|month|quarter|halfYear|year
|
||||
const [historyRows, setHistoryRows] = useState({}); // range → [已清仓持仓行](含 'today' 键)
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const call = useRpc();
|
||||
const toast = useToast();
|
||||
const { getPrice, registerCodes } = useMarket();
|
||||
|
||||
/** R-016:拉某范围已清仓历史持仓(独立只读端点;当前持仓快照路径零改动)。
|
||||
* range='today' = 本地自然日 00:00 起清仓(R-016 二轮补充:今日已清仓默认层) */
|
||||
const loadHistory = useCallback(async (range) => {
|
||||
setHistoryLoading(true);
|
||||
try {
|
||||
const res = await call('one-divine-lot/strategy-holdings/history', { args: { strategyId, range } });
|
||||
if (res && res.ok && Array.isArray(res.value)) {
|
||||
setHistoryRows((prev) => ({ ...(prev ?? {}), [range]: res.value }));
|
||||
} else {
|
||||
// 失败可见化(迭代 14 复盘:静默兜底=排障盲区)
|
||||
toast.error('历史持仓加载失败: ' + ((res && res.error && res.error.message) || 'unknown'));
|
||||
setHistoryRows((prev) => ({ ...(prev ?? {}), [range]: [] }));
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('历史持仓加载失败: ' + (err?.message ?? String(err)));
|
||||
setHistoryRows((prev) => ({ ...(prev ?? {}), [range]: [] }));
|
||||
} finally {
|
||||
setHistoryLoading(false);
|
||||
}
|
||||
}, [call, strategyId, toast]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -220,14 +252,31 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// R-016 二轮补充:今日已清仓层随每次持仓重载刷新(进 tab / 移出等操作后,
|
||||
// 当天清仓的行立即转入默认层,不依赖「历史持仓」开关;loadHistory 自容错不外抛)
|
||||
loadHistory('today');
|
||||
}
|
||||
}, [call, strategyId]);
|
||||
}, [call, strategyId, loadHistory, registerCodes]);
|
||||
|
||||
useEffect(() => {
|
||||
ensureAddFormCss();
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
/** 开关:开启即取最新(关闭仅隐藏,缓存保留;进 tab 默认关)。
|
||||
* R-016 二轮补充:开关只控制「今天之前的历史范围层」;range='today'(当日清仓)恒显示、不受本开关控制 */
|
||||
const toggleHistory = () => {
|
||||
const next = !showHistory;
|
||||
setShowHistory(next);
|
||||
if (next) loadHistory(historyRange);
|
||||
};
|
||||
|
||||
/** 切换范围:未缓存才拉取 */
|
||||
const changeHistoryRange = (r) => {
|
||||
setHistoryRange(r);
|
||||
if (!historyRows[r]) loadHistory(r);
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!selectedCode || !addShares) return;
|
||||
const res = await call('one-divine-lot/add-shares', {
|
||||
@@ -271,30 +320,30 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
||||
}
|
||||
};
|
||||
|
||||
/** R-010:展开某持仓行 → 懒加载该 holding 的委托汇总 */
|
||||
const toggleExpand = async (code, holdingId) => {
|
||||
if (expandedCode === code) {
|
||||
/** R-010:展开某持仓行 → 懒加载该 holding 的委托汇总(R-016:键改唯一 rowKey,同码多行不串) */
|
||||
const toggleExpand = async (rowKey, holdingId) => {
|
||||
if (expandedCode === rowKey) {
|
||||
// 折叠
|
||||
setExpandedCode(null);
|
||||
return;
|
||||
}
|
||||
setExpandedCode(code);
|
||||
setExpandedCode(rowKey);
|
||||
if (holdingId == null) {
|
||||
setHoldingTrades((prev) => ({ ...(prev ?? {}), [code]: [] }));
|
||||
setHoldingTrades((prev) => ({ ...(prev ?? {}), [rowKey]: [] }));
|
||||
return;
|
||||
}
|
||||
// 懒加载:有缓存直接用,否则请求
|
||||
if (holdingTrades && holdingTrades[code]) return;
|
||||
setHoldingTradesLoading(code);
|
||||
if (holdingTrades && holdingTrades[rowKey]) return;
|
||||
setHoldingTradesLoading(rowKey);
|
||||
try {
|
||||
const res = await call('one-divine-lot/trades/by-holding', { args: { holdingId } });
|
||||
if (res?.ok && Array.isArray(res.value)) {
|
||||
setHoldingTrades((prev) => ({ ...(prev ?? {}), [code]: res.value }));
|
||||
setHoldingTrades((prev) => ({ ...(prev ?? {}), [rowKey]: res.value }));
|
||||
} else {
|
||||
setHoldingTrades((prev) => ({ ...(prev ?? {}), [code]: [] }));
|
||||
setHoldingTrades((prev) => ({ ...(prev ?? {}), [rowKey]: [] }));
|
||||
}
|
||||
} catch {
|
||||
setHoldingTrades((prev) => ({ ...(prev ?? {}), [code]: [] }));
|
||||
setHoldingTrades((prev) => ({ ...(prev ?? {}), [rowKey]: [] }));
|
||||
} finally {
|
||||
setHoldingTradesLoading(null);
|
||||
}
|
||||
@@ -325,6 +374,7 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
||||
}
|
||||
return rows;
|
||||
}, [positions, sortKey, sortDir, getPrice]);
|
||||
//(R-016:rowsToRender 在此下方合成,历史行固定追加、不参与涨幅排序)
|
||||
|
||||
// 迭代11列显隐:可见列(表头/行渲染依据;未加载配置时=null,回退旧固定列)
|
||||
const visibleColumns = useMemo(() => {
|
||||
@@ -332,8 +382,62 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
||||
return columns.filter((c) => c.visible !== false);
|
||||
}, [columns]);
|
||||
|
||||
// R-016 迭代 14:渲染行 = 当前持仓行 + 已清仓行。
|
||||
// 二轮补充(2026-09-07 老师定):已清仓行分两层——
|
||||
// L1「今日已清仓」(range='today',本地自然日 00:00 起清仓):**默认恒显示**,不受「历史持仓」开关控制
|
||||
// L2「历史范围层」(week/month/...):仅开关开启时显示(用户主动查看更早的历史)
|
||||
// 层间按 holdingId 去重('today' 含近 7 天清仓时,开启 week 不重复出两行);标题计数 (N 只) 只算当前持仓
|
||||
const histRowsFor = (range) =>
|
||||
(historyRows[range] ?? []).map((h) => ({
|
||||
...h,
|
||||
isHistory: true, // 历史行标识:灰显/只读/行情列 —
|
||||
name: h.name ?? null, // name 兜底(接口已按关联委托补,无则 null → 显示 —)
|
||||
}));
|
||||
|
||||
const rowsToRender = useMemo(() => {
|
||||
const current = rowsForRender;
|
||||
const today = histRowsFor('today'); // L1:今天已清仓 —— 恒定显示
|
||||
if (!showHistory) return [...current, ...today];
|
||||
// L2:历史范围层(含 today 在内同一端点返回)——去重:today 已显示的不重复追加
|
||||
const todayIds = new Set(today.map((h) => h.holdingId));
|
||||
const rest = histRowsFor(historyRange).filter((h) => !todayIds.has(h.holdingId));
|
||||
return [...current, ...today, ...rest];
|
||||
}, [rowsForRender, showHistory, historyRows, historyRange]);
|
||||
|
||||
/** 历史行徽标/时间格式化(清仓时间 YYYY-MM-DD HH:mm) */
|
||||
const fmtClosedAt = (ts) => {
|
||||
if (!ts) return '—';
|
||||
const d = new Date(Number(ts));
|
||||
if (!isFinite(d.getTime())) return '—';
|
||||
const p2 = (n) => String(n).padStart(2, '0');
|
||||
return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()) + ' ' + p2(d.getHours()) + ':' + p2(d.getMinutes());
|
||||
};
|
||||
/** 持有天数(同日 = 1,向上取整) */
|
||||
const heldDays = (created, closed) => {
|
||||
if (!created || !closed) return null;
|
||||
const ms = Number(closed) - Number(created);
|
||||
if (!isFinite(ms) || ms < 0) return null;
|
||||
return Math.max(1, Math.ceil(ms / 86400000));
|
||||
};
|
||||
|
||||
/** 渲染某数据列单元格内容(col: {key,label,kind})—— 返回 React 节点 */
|
||||
const renderDataCell = (col, p) => {
|
||||
// R-016:历史行——行情类列一律 —(不订阅行情);shares 显示 DB 原值(Q2:0 就好);自定义字段只读
|
||||
if (p.isHistory) {
|
||||
if (col.kind === 'field') {
|
||||
const fieldDef0 = (strategySchema ?? []).find((f0) => f0.key === col.key);
|
||||
if (!fieldDef0) return null;
|
||||
const has0 = p.values && Object.prototype.hasOwnProperty.call(p.values, col.key);
|
||||
const v0 = has0 ? p.values[col.key] : fieldDef0.def;
|
||||
let shown0;
|
||||
if (fieldDef0.type === 'boolean') shown0 = v0 ? '开' : '关';
|
||||
else if (v0 === undefined || v0 === null || v0 === '') shown0 = '—';
|
||||
else shown0 = String(v0) + (fieldDef0.unit ? fieldDef0.unit : '');
|
||||
return <span style={{ color: 'var(--dsw-alias-label-tertiary, #999)' }}>{shown0}</span>;
|
||||
}
|
||||
if (col.key === 'shares') return <span style={{ color: 'var(--dsw-alias-label-tertiary, #999)' }}>{p.shares}</span>;
|
||||
return <span style={{ color: 'var(--dsw-alias-label-tertiary, #999)' }}>—</span>;
|
||||
}
|
||||
if (col.kind === 'field') {
|
||||
// 自定义字段列:p.values[key] ?? def;点击单元格进入内联编辑(老师选 A)
|
||||
const fieldDef = (strategySchema ?? []).find((f2) => f2.key === col.key);
|
||||
@@ -417,13 +521,44 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
||||
<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 }}>
|
||||
<h3 style={{ margin: 0 }}>{name}({(positions ?? []).length} 只)</h3>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<button
|
||||
onClick={() => setShowAdd(!showAdd)}
|
||||
style={{ padding: '6px 12px', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4, background: 'var(--dsw-alias-interactive-bg-hover, #f5f5f5)' }}
|
||||
>
|
||||
{showAdd ? '取消' : '+ 添加持仓'}
|
||||
</button>
|
||||
{/* R-016 迭代 14:历史持仓开关 + 范围筛选(title 旁)。
|
||||
R-016 二轮补充:今天已清仓的持仓默认显示(不受此开关控制),此开关只开关更早的历史(今天之前) */}
|
||||
<button
|
||||
onClick={toggleHistory}
|
||||
title="显示/隐藏今天之前已清仓的历史持仓(当天已清仓的持仓始终显示)"
|
||||
style={{
|
||||
padding: '6px 12px', cursor: 'pointer', borderRadius: 4, fontSize: 13,
|
||||
border: '1px solid ' + (showHistory ? 'var(--dsw-alias-state-business-primary, #1565c0)' : 'var(--dsw-alias-border-l2, #ccc)'),
|
||||
background: showHistory ? 'color-mix(in srgb, var(--dsw-alias-state-business-primary, #1565c0) 12%, var(--dsw-alias-bg-layer-1, #fff))' : 'var(--dsw-alias-bg-layer-1, #fff)',
|
||||
color: showHistory ? 'var(--dsw-alias-state-business-primary, #1565c0)' : 'var(--dsw-alias-label-primary, #333)',
|
||||
}}
|
||||
>
|
||||
{showHistory ? '历史持仓 开' : '历史持仓 关'}
|
||||
</button>
|
||||
{showHistory && (
|
||||
<select
|
||||
value={historyRange}
|
||||
onChange={(e2) => changeHistoryRange(e2.target.value)}
|
||||
title="已清仓时间范围(按清仓时间回溯)"
|
||||
style={{ padding: '5px 6px', fontSize: 13, border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4, background: 'var(--dsw-alias-bg-layer-1, #fff)' }}
|
||||
>
|
||||
<option value="week">近一周</option>
|
||||
<option value="month">近 1 个月</option>
|
||||
<option value="quarter">近 3 个月</option>
|
||||
<option value="halfYear">近半年</option>
|
||||
<option value="year">近 1 年</option>
|
||||
</select>
|
||||
)}
|
||||
{showHistory && historyLoading && (
|
||||
<span style={{ fontSize: 12, color: 'var(--dsw-alias-label-tertiary, #999)' }}>历史加载中...</span>
|
||||
)}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button
|
||||
onClick={() => setColumnOpen(!columnOpen)}
|
||||
@@ -476,7 +611,7 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{positions && positions.length === 0 ? (
|
||||
{rowsToRender.length === 0 ? (
|
||||
<div style={{ padding: 24, textAlign: 'center', color: 'var(--dsw-alias-label-tertiary, #999)' }}>
|
||||
暂无持仓,点击「+ 添加持仓」从全部持仓中添加
|
||||
</div>
|
||||
@@ -492,19 +627,24 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rowsForRender.map((p) => {
|
||||
const isExpanded = expandedCode === p.code;
|
||||
const trades = (holdingTrades ?? {})[p.code];
|
||||
const isLoading = holdingTradesLoading === p.code;
|
||||
{rowsToRender.map((p) => {
|
||||
const rowKey = p.isHistory ? 'h-' + p.holdingId : p.code; // 唯一行键:同码「当前行 + 历史行」不串展开与缓存
|
||||
const isExpanded = expandedCode === rowKey;
|
||||
const trades = (holdingTrades ?? {})[rowKey];
|
||||
const isLoading = holdingTradesLoading === rowKey;
|
||||
const hasHolding = p.holdingId != null;
|
||||
const histStyle = p.isHistory
|
||||
? { opacity: 0.72, color: 'var(--dsw-alias-label-tertiary, #999)' }
|
||||
: null;
|
||||
return (
|
||||
<React.Fragment key={p.code}>
|
||||
<React.Fragment key={rowKey}>
|
||||
<tr
|
||||
onClick={() => toggleExpand(p.code, p.holdingId)}
|
||||
onClick={() => toggleExpand(rowKey, p.holdingId)}
|
||||
style={{
|
||||
borderBottom: '1px solid var(--dsw-alias-border-l1, #eee)',
|
||||
cursor: hasHolding ? 'pointer' : 'default',
|
||||
background: isExpanded ? 'var(--dsw-alias-interactive-bg-hover, #f5f5f5)' : 'transparent',
|
||||
...(histStyle ?? {}),
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: '8px 6px', textAlign: 'center', width: 28 }}>
|
||||
@@ -514,13 +654,30 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
||||
<span style={{ color: 'var(--dsw-alias-border-l3, #ccc)', fontSize: 10 }}>·</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '8px 6px' }}>{p.code}</td>
|
||||
<td style={{ padding: '8px 6px' }}>{p.name}</td>
|
||||
<td style={{ padding: '8px 6px', whiteSpace: 'nowrap' }}>
|
||||
{p.code}
|
||||
{p.isHistory && (
|
||||
<span
|
||||
title={'清仓时间 ' + fmtClosedAt(p.closedAt)}
|
||||
style={{
|
||||
marginLeft: 6, fontSize: 11, padding: '1px 6px', borderRadius: 3,
|
||||
border: '1px solid var(--dsw-alias-border-l2, #ccc)',
|
||||
color: 'var(--dsw-alias-label-tertiary, #999)',
|
||||
background: 'var(--dsw-alias-bg-layer-2, #f5f5f5)',
|
||||
}}
|
||||
>
|
||||
已清仓 {fmtClosedAt(p.closedAt).slice(0, 10)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '8px 6px' }}>{p.isHistory ? (p.name ?? '—') : p.name}</td>
|
||||
{(visibleColumns ?? []).map((col) => (
|
||||
<td key={col.key} style={{ padding: '8px 6px', whiteSpace: 'nowrap' }}>{renderDataCell(col, p)}</td>
|
||||
))}
|
||||
<td style={{ padding: '8px 6px', whiteSpace: 'nowrap' }} onClick={(e) => e.stopPropagation()}>
|
||||
{removeTarget && removeTarget.code === p.code ? (
|
||||
{p.isHistory ? (
|
||||
<span style={{ color: 'var(--dsw-alias-border-l3, #ccc)' }}>—</span>
|
||||
) : removeTarget && removeTarget.code === p.code ? (
|
||||
<span>
|
||||
<input
|
||||
type="number"
|
||||
@@ -559,11 +716,18 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
||||
{isExpanded && (
|
||||
<tr style={{ borderBottom: '1px solid var(--dsw-alias-border-l1, #eee)', background: 'var(--dsw-alias-bg-layer-2, #fafafa)' }}>
|
||||
<td colSpan={4 + (visibleColumns?.length ?? 6)} style={{ padding: '4px 8px 12px' }}>
|
||||
{p.isHistory && (
|
||||
<div style={{ padding: '4px 8px', fontSize: 12, color: 'var(--dsw-alias-label-secondary, #666)' }}>
|
||||
已清仓持仓 · 清仓时间 {fmtClosedAt(p.closedAt)} · 持有 {heldDays(p.createdAt, p.closedAt) ?? '—'} 天 · 份额(清仓后){p.shares}
|
||||
</div>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<div style={{ padding: 8, color: 'var(--dsw-alias-label-tertiary, #999)', fontSize: 12 }}>交易记录加载中...</div>
|
||||
) : !trades || trades.length === 0 ? (
|
||||
<div style={{ padding: 8, color: 'var(--dsw-alias-label-tertiary, #999)', fontSize: 12 }}>
|
||||
{hasHolding ? '该持仓暂无关联交易记录(可在「交易记录」tab 为委托设置归属)' : '该持仓无关联锚点(holding_id)'}
|
||||
{hasHolding
|
||||
? (p.isHistory ? '该历史持仓暂无关联交易记录(归属设置须在清仓当日完成)' : '该持仓暂无关联交易记录(可在「交易记录」tab 为委托设置归属)')
|
||||
: '该持仓无关联锚点(holding_id)'}
|
||||
</div>
|
||||
) : (
|
||||
<HoldingTradesTable trades={trades} />
|
||||
|
||||
@@ -73,12 +73,18 @@ export class DataStore {
|
||||
return this.sqlite.getCurrentHoldings(strategyId);
|
||||
}
|
||||
|
||||
/** 历史持仓 */
|
||||
/** 历史持仓(含当前 + 已清仓;全表按 code 查,R-008 遗留) */
|
||||
async getHoldingHistory(code) {
|
||||
await this._ensure();
|
||||
return this.sqlite.getHoldingHistory(code);
|
||||
}
|
||||
|
||||
/** 某策略已清仓持仓(closed_at 非空 + 可选 sinceMs 下限;R-016 迭代 14,只读) */
|
||||
async getHoldingsHistory(strategyId, opts) {
|
||||
await this._ensure();
|
||||
return this.sqlite.getHoldingsHistory(strategyId, opts);
|
||||
}
|
||||
|
||||
/** 读取持仓自定义字段值(R-013:委托 SqliteStore) */
|
||||
readValues(holdingId) {
|
||||
return this.sqlite.readValues(holdingId);
|
||||
|
||||
@@ -375,6 +375,25 @@ export class SqliteStore {
|
||||
).all().map(this._mapHolding);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某策略已清仓持仓(closed_at 非空;R-016 迭代 14)
|
||||
* 只读新增,不动任何写路径(closeHolding 置 shares=0 语义维持——Q2 老师拍板:历史行份额显示 0)。
|
||||
* @param {string} strategyId 策略 ID
|
||||
* @param {{sinceMs?: number}} [opts] sinceMs:closed_at 下限(毫秒,含);缺省不过滤
|
||||
* @returns {Array} 历史持仓行(_mapHolding 同构),按 closed_at DESC(最近清仓在前)
|
||||
*/
|
||||
getHoldingsHistory(strategyId, { sinceMs } = {}) {
|
||||
this.init();
|
||||
const params = [strategyId];
|
||||
let sql = 'SELECT holding_id, strategy_id, code, shares, created_at, closed_at, "values" FROM strategy_holdings WHERE strategy_id=? AND closed_at IS NOT NULL';
|
||||
if (sinceMs != null && Number.isFinite(Number(sinceMs))) {
|
||||
sql += ' AND closed_at >= ?';
|
||||
params.push(Number(sinceMs));
|
||||
}
|
||||
sql += ' ORDER BY closed_at DESC';
|
||||
return this.db.prepare(sql).all(...params).map(this._mapHolding);
|
||||
}
|
||||
|
||||
/** 查询历史(含当前 + 已清仓;可加 code 过滤) */
|
||||
getHoldingHistory(code) {
|
||||
this.init();
|
||||
@@ -664,8 +683,6 @@ export class SqliteStore {
|
||||
return this.db.prepare(
|
||||
'SELECT * FROM trade_orders WHERE holding_id=? ORDER BY insert_ts DESC'
|
||||
).all(holdingId).map((r) => this._mapOrderRow(r));
|
||||
}
|
||||
|
||||
/**
|
||||
* 委托归属候选列表(用户手动设置用;候选 = 该 code 当前持仓的策略/持仓,Q3 全手动选)
|
||||
* @param {string} code 证券代码(含后缀)
|
||||
@@ -682,7 +699,6 @@ export class SqliteStore {
|
||||
shares: h.shares,
|
||||
}));
|
||||
}
|
||||
|
||||
_mapOrderRow(r) {
|
||||
return {
|
||||
orderId: r.order_id,
|
||||
|
||||
Reference in New Issue
Block a user