迭代19-21收尾: 指示灯自适应探测(R-022/R-023)+ 列设置拖拽排序(R-024)+ Tab设置间隙线统一(R-025)+ 需求池归档(R-014/15/16/17/19 → 已完成)

This commit is contained in:
2026-09-10 14:04:51 +08:00
parent 8782aa9c50
commit 3dfa39b8bf
29 changed files with 1216 additions and 72 deletions
@@ -1,6 +1,6 @@
# 迭代 17 UI 交互设计:策略会话(讨论依据控件)
> 依据:R-019已定稿)+ 产品逻辑设计(同迭代)| 日期:2026-09-08 | 状态:**设计稿(D 问题待拍板)**
> 依据:R-021(合并原 R-019+R-021已定稿)+ 产品逻辑设计(同迭代)| 日期:2026-09-08 | 状态:**设计稿(D 问题待拍板)**
> 视觉基线:UI约束-004--dsw-* token/ UI约束-001(头部 chip 形态)/ 现有 QmtConnectionChip 交互模式;复用 Toast、外点关闭、RPC(useRpc) 机制。
## 1. 交互总览(一句话)
@@ -73,4 +73,4 @@
| D-1 | 注入条目呈现形态 | 折叠「数据依据」卡片,点开全文(见 §4.2) |
| D-2 | 交易摘要聚合粒度 | 按持仓聚合 + 已清仓标注;如需近期逐笔再加近 N 笔 |
| D-3 | 依据跨刷新持久化 | 会话级不持久化(重开重选) |
| D-4 | chip 位置 | 对话视图输入框上方 accessory 行左侧(仿 workspace chip 位置);备选=会话头部与 QMT chip 并排 |
| D-4 | chip 位置 | 对话视图输入框上方 accessory 行左侧(仿 workspace chip 位置);备选=会话头部与 QMT chip 并排 |
@@ -1,6 +1,6 @@
# 迭代 17 产品逻辑设计:策略会话
> 依据:R-019已定稿)| 日期:2026-09-08 | 状态:**设计稿(D 问题待老师拍板后定稿)**
> 依据:R-021(合并原 R-019+R-021已定稿)| 日期:2026-09-08 | 状态:**设计稿(D 问题待老师拍板后定稿)**
## 0. 一句话逻辑
@@ -70,7 +70,7 @@
## 5. 非目标(本期明确不做)
- 宿主工作区/目录注册(R-019 R3);
- 宿主工作区/目录注册(R-019 R3,已并入 R-021);
- 账本写/交易执行、任何操作按钮;
- 按需取数工具(AI 自行调用业务接口);
- 复盘产物落盘 / 复盘管理(含 DB)——未来 T-002 方向;
@@ -81,6 +81,46 @@
注入的可行通道候选:① 宿主 injected-context/上下文节点(可折叠条目、非用户消息)→ 首选;② 会话 send(文本消息形态进会话)→ 兜底(呈现为一条前置依据消息)。**渲染与动作方案以调研结果为准**(本期设计保留两种形态的表达,UI 交互设计按"数据依据条目"统一描述)。
## 0.5 产品逻辑总图(2026-09-08 老师确认:画像 / 模型 / 主线 / 分期草案)
### 目标交易者画像(Q-画像,2026-09-08 确认)
- **混合交易模式的多策略分仓管理**:同一账户下并存多种交易风格的策略分组(当前:手动做T + 程序化网格超市;未来:情绪流交易风格的管理逻辑);
- 即"策略 = 分仓 + 交易风格/规则的容器";系统须能承载 手动主观 + 自动化规则 + 未来情绪流 三类玩法(彼此数据/规则/复盘口径不同)。
### 交易员工作模型(知识库归纳,作为产品逻辑基准)
- **日循环**:盘前计划 → 盘中执行/监控 → 盘后记账核对 → 复盘总结 → 明日计划;
- **策略循环**:规则制定 → 按规则执行 → 规则校验(复盘)→ 规则修订;
- **复盘闭环**:重建事实 → 归因 → 对照规则 → 提炼改进;**复盘原料 = 决策时的依据记录**(知识库共识:没有决策留痕的复盘只是猜)。
### AI 分工定位(人机合一,只读)
- **盘中 = AI 参谋台**(状态聚合 / 规则触发提醒 / 轻问答 / 决策留痕 / 人扣扳机执行),快、准、不打断;
- **盘后 = AI 复盘主持人**(自动重建事实线 → 引导人过结论 → 结论留痕),人不被 AI 下"对错"裁决;
- 决策依据记录(盘中一键留痕:理由 + 当时快照)是产品逻辑主线之一(2026-09-08 老师确认)——复盘有据的前提。
### 能力地图(现状 → 缺口)
- 已具备"事实记录层":对账单快照 / 策略账本(唯一权威)/ 交易记录+归属 / 行情快照 / 策略 configSchema(可承载规则参数);
- 缺口(按承接排序):①复盘事实线入口+可讨论会话(R-021 合并需求,本迭代)②决策依据留痕(盘中)③规则触发提醒 ④盘后自动日结/统计 ⑤复盘管理库(决策日志+复盘记录结构化存储检索)。
### 分期草案(2026-09-08 老师认可分步实现;**各 Phase 具体内容待讨论细化**)
| 阶段 | 主题 | 交付设想(草案) |
|---|---|---|
| Phase 0(迭代 17 | 复盘会话闭环 | R-021 策略会话复盘(原 R-019+R-021 合并;策略数据依据 + 粒度复盘上下文,事实线 → 会话讨论 → 结论在会话) |
| Phase 1 | 盘中辅助(雏形) | 决策依据一键留痕(理由+快照落库)+ 规则触发/偏离提醒雏形(configSchema × 行情)+ 会话内盘中快查 |
| Phase 2 | 盘后日结与统计 | 自动日结(当日各策略事实线+盈亏+异常)→ 一键入会话复盘;周期统计(轮次收益/胜率/策略健康) |
| Phase 3 | 复盘管理库 | 决策日志+复盘结论结构化沉淀/检索/关联(DB),对接目标-006;情绪流风格管理逻辑另立项 |
## 0.6 Phase 0 范围确认(2026-09-08 老师确认:就这样)
- **Phase 0 = 复盘会话闭环,范围 = R-021 单一合并需求(原 R-019 + R-0212026-09-08 老师指令合并、逻辑整合入 R-021)**;
- **复盘单元建模(A 拍板)**:引入"轮次 round"概念(holding 之下的交易周期聚合层):
- 网格超市:一次买入触发 + 对应卖出配对 = 一轮(每格触发记一轮);
- 手动做T:一次开仓 → 清仓 = 一轮;
- 支持人工合并/拆分修正(先人工可调);
- round 承接"复盘对象"的最小粒度,委托/段为 round 内明细;
- **行情口径(C 拍板)**:本期事实线 = 交易记录 + 持仓史 + 现价对照(无历史 K 线/分时);历史行情背景 Phase 3 复盘库时再评估;
- **决策日志预留(拍板)**:Phase 0 设计为"决策依据记录"留数据模型位置(时刻/理由/类别/当时快照/关联 round 或 holding),Phase 1 实现,不返工;
- **风格差异**:复盘口径按策略类型可扩展(手动/网格,未来情绪流预留策略类型维度)。
## 7. D 问题(AI 建议项,待老师拍板)
| # | 问题 | AI 建议 |
@@ -88,4 +128,4 @@
| D-1 | 注入条目的呈现形态 | 对话流顶部「数据依据」条目:默认折叠为一行(图标+策略+快照时间+行数摘要),点开看全文;不伪装成用户消息 |
| D-2 | 交易摘要聚合粒度 | 按持仓聚合(每持仓一段汇总 + 已清仓标注),不逐笔铺开(避免注入过长);如老师要"近期逐笔"可加近 N 笔小表 |
| D-3 | 依据选择是否跨刷新持久化 | 本期**会话级不持久化**(刷新需重选;历史注入条目仍在消息流),与 R-016 会话级偏好一致、不加存储;复盘管理未来再考虑 |
| D-4 | 选择控件挂载位置 | 对话视图输入框上方 accessory 行左侧(仿 DSH workspace chip 位置),仅聊天视图可见;替代方案=会话头部(与 QMT chip 并排)待老师定 |
| D-4 | 选择控件挂载位置 | 对话视图输入框上方 accessory 行左侧(仿 DSH workspace chip 位置),仅聊天视图可见;替代方案=会话头部(与 QMT chip 并排)待老师定 |
@@ -6,7 +6,7 @@
## 目标描述
- 老师原始诉求「新需求,让 AI 会话可以以策略为 Workspace」经四轮讨论定稿(R-0192026-09-08):
- 老师原始诉求「新需求,让 AI 会话可以以策略为 Workspace」经四轮讨论定稿(R-0192026-09-08;同日老师指令 R-019 并入 R-021 合并为单一需求):
- **改造现有 DSH 对话窗口**,不引入宿主工作区/目录;
- 会话的数据 = ①开始前/过程中注入的策略当前数据摘要 + ②会话过程本身产生的对话;复盘管理(可能带 DB 沉淀)为神之一手未来模块,本期不做;
- 注入口径:当前持仓明细 + 该策略全历史关联交易聚合摘要(含已清仓);
@@ -36,4 +36,4 @@
## 对老师配合的请求
1. 拍板 D1-D4(产品/UI 设计中的 AI 建议项,见设计文档末);
2. 实现验收需真实会话环境人工目视。
2. 实现验收需真实会话环境人工目视。
@@ -0,0 +1,45 @@
# 技术实现方案:19-指示灯状态自适应探测
> 迭代编号:19 依据:R-022 + R-023 | 涉及约束:产品约束-012(指示灯体系)、技术约束-016、技术约束-020/021(本迭代新增)
## 方案总览
两只连接类状态缓存统一改「**结果驱动 setTimeout 自适应链**」:每次探测完成(成功/失败/异常)后按结果排下一轮。
| 维度 | QmtHealthMonitorR-022 | QmtMcpManagerR-023 |
|---|---|---|
| 健康/已连接 | intervalMs 默认 5 分钟(稳态,原设计) | intervalMs 默认 5 分钟 |
| 异常/未知/未连接 | retryMs 默认 10 秒快速重试 | retryMs 默认 10 秒快速重试 |
| 探测目标 | 激活 baseUrl + /healthGET | 激活 baseUrl + /mcpSDK Client 握手,只读) |
| 停止调度 | stop()mounted 守卫) | 无激活连接 / dispose() / unmount() |
| 并发防护 | _inFlight 去重 | _probing 去重 |
| 前端/API | 零改动(读缓存秒回) | 零改动(getStatus 读缓存秒回) |
## 实现步骤
### R-022 QmtHealthMonitorsrc/data-source/QmtHealthMonitor.js
1. 常量 HEALTH_RETRY_MS=10s;构造注入 { intervalMs, retryMs }(测试用短间隔,对齐 QuoteSync 先例);
2. 固定 setInterval(5min) → _nextDelay()/_scheduleNext()setTimeout 链,探测完成重排);
3. resolveActiveBaseUrl 移入 try:解析失败也落 healthy:false 缓存走快速重试
(原实现解析在 try 外,抛错场景灯会灰且不自动重试——顺带修复的隐性缺陷);
4. start() 立即探测不变;stop() 用 clearTimeout + mounted 守卫。
### R-023 QmtMcpManagersrc/mcp/QmtMcpManager.js
1. 常量 PROBE_RETRY_MS=10s;构造注入 { intervalMs, retryMs }
2. probe() 结果写缓存后 _scheduleNext()target 为空早退置 disabled 并取消定时器;
3. unmount() 取消定时器;dispose() 置 _disposed + 取消 + 卸载;_probing 防并发;
4. 探测本体(SDK Client connect/initialize/tools/list)与挂载/重连逻辑零改动。
### 回归
- scripts/test-health-monitor.mjs10 项:启动即探测/异常快速重试自动恢复/健康稳态不空转/stop 停止调度…)
- scripts/test-mcp-status.mjs12 项:无激活连接不调度/异常快速重试自动恢复/已连接稳态不空转/dispose 停止调度/节奏选择;
SDK 握手不可纯内存 mock,守护调度机制:stub probe 翻转状态 + 触达真实 _scheduleNext
## 涉及文件
- src/data-source/QmtHealthMonitor.js、src/mcp/QmtMcpManager.js(核心改动)
- scripts/test-health-monitor.mjs、scripts/test-mcp-status.mjs(新增回归)
- lib/*build 再产物,部署用)
@@ -0,0 +1,45 @@
# 迭代复盘:19-指示灯状态自适应探测
## 结论
达成(R-022 + R-023 全范围):会话头部两只「连接类」指示灯(QMT连接 / MCP)统一改为
**结果驱动自适应探测**——健康/已连接维持 5 分钟稳态(原设计不变),异常/未知每 10 秒快速重试;
QMT Bridge 与其 MCP 服务启动/恢复后两灯 ≤20s 自动回绿,不再等满 5 分钟 / 不再永不恢复。
前端指示灯与 sync-status / qmt-health / mcp-status 端点零改动(读缓存秒回语义不变)。
## 事实记录
### R-022 QMT 健康灯(src/data-source/QmtHealthMonitor.js
- 根因(已证实):健康缓存固定 5 分钟探测一次激活 /health;其余三灯有高频自愈回路
(持仓 10s / 行情 5s / MCP 客户端重连)。Bridge 在两次探测间启动/恢复时缓存停留在上次失败结果。
- 变更:setInterval(5min) → setTimeout 自适应链(_nextDelay/_scheduleNext,结果驱动);
HEALTH_RETRY_MS=10s;构造注入 { intervalMs, retryMs }resolveActiveBaseUrl 移入 try
(解析失败也落 healthy:false 走快速重试,原实现该场景灯灰且不自动重试);_inFlight 防并发保留。
### R-023 MCP 状态灯(src/mcp/QmtMcpManager.js
- 根因(已证实):状态缓存只在 挂载/切换/手动探测 时刷新,无定时回路;dsh-mcp-client 自带 reconnect
只恢复真实连接(fiber 层),不刷新插件状态缓存。
- 变更:probe() 结果写缓存后 _scheduleNext()PROBE_RETRY_MS=10s;构造注入 { intervalMs, retryMs }
无激活连接早退置 disabled 并取消定时器;unmount()/dispose() 停止调度;_probing 防并发;
探测本体与挂载/重连逻辑零改动。
### 验证
- scripts/test-health-monitor.mjs 10/10 绿;scripts/test-mcp-status.mjs 12/12 绿(调度机制守护);
- pnpm typecheck 0 错;pnpm build 通过(lib 两文件均含自适应逻辑);
- 存量回归 test-quote-sync / test-position-sync 均 0 退出。
## 待老师确认事项
1. R-022 / R-023 定稿确认(边界:健康/已连接稳态 5 分钟不变 / 异常 10s 重试 / 不新增配置 / API 前端零改动);
2. 真实环境人工验收(验收标准.md 第 8 条):停/启 QMT Bridge → QMT 灯与 MCP 灯 ≤20s 自动回绿、
点击仍即时探测、插件重载后缓存秒回。
## 经验沉淀(候选)
- 定时轮询类功能的恢复感知要与故障探测解耦:固定低频定时器做稳态,异常态必须用快速重试把「恢复」
也纳入自愈回路——灯类状态"看起来没生效"的根因通常是恢复路径的响应时延,而不是探测本身失效。
- 「外层有自管重连」不等于「状态缓存会自愈」:重连只恢复真实连接,展示层缓存若无自己的刷新回路,
必须单独加状态自适应探测(R-023 教训)。
@@ -0,0 +1,32 @@
# 迭代目标:19-指示灯状态自适应探测(QMT 健康灯 + MCP 灯)
## 目标
会话头部两只「连接类」指示灯(QMT连接 / MCP)改为**结果驱动自适应探测**:
健康/已连接时维持 5 分钟稳态探测(低开销),异常/未知时每 10 秒快速重试,
使 QMT Bridge 与其 MCP 服务启动/恢复后指示灯 ≤20s 自动回绿,不再等满 5 分钟。
## 目标描述
- **背景**:两只灯的数据分别来自 QmtHealthMonitor / QmtMcpManager 的内存状态缓存,刷新机制均为
固定/缺失低频定时回路——QMT 健康缓存固定每 5 分钟探测一次(R-022),MCP 状态缓存只在
挂载/切换/手动探测时刷新、无定时回路(R-023)。持仓(10s)/ 行情(5s)灯靠各自定时同步秒级自愈;
唯独两只连接灯恢复感知滞后,老师实测反馈「QMT连接灯一直没亮」「MCP 灯也不自动恢复」(2026-09-09)。
- **范围**:只改 QmtHealthMonitorR-022)与 QmtMcpManagerR-023)的探测调度;前端指示灯组件、
sync-status / qmt-health / mcp-status 端点零改动;健康稳态间隔 5 分钟不变;不新增配置项。
- **验收线**:两灯异常/未知态 ≤10s 重试;健康/已连接态维持 5 分钟不空转;服务恢复后缓存回正常态
沿快速重试被感知;无激活连接与 dispose 后停止调度。对应需求 R-022 + R-023。
## 目标讨论过程
1. 老师报告症状(2026-09-09):Bridge 启动后其他三灯自动点亮,QMT 灯不亮 → 定位为健康缓存固定 5 分钟探测。
2. AI 提议自适应节奏(健康 5min / 异常 10s),老师确认方向,迭代 19 实施(R-022)。
3. 老师复查发现 MCP 灯同样不自动恢复(R-023)→ 确认 QmtMcpManager 状态缓存无定时回路,
dsh-mcp-client 重连只恢复真实连接、不刷新本类缓存 → 同模式加自适应探测,并入本次迭代。
4. 决策点见 R-022 / R-023 决策记录。
## 对老师(项目主理人)的配合需求
- 确认 R-022 / R-023 定稿与迭代 19 验收(核实标准见验收标准.md);
- 真实环境人工验收:停/启 QMT Bridge → QMT 灯 ≤20s 回绿、MCP 灯 ≤20s 回绿;点击灯/「检测 MCP」仍即时探测;
插件重载后灯按缓存秒回并正常。
@@ -0,0 +1,29 @@
# 验收标准:19-指示灯状态自适应探测
## 验收标准线
| # | 标准 | 判定 |
|---|---|---|
| 1 | QMT 健康:异常/未知态按 retryMs(默认 10s)快速重试 | test-health-monitor [2]:≥2 次快速重试且缓存 healthy=false |
| 2 | QMT 健康:Bridge 恢复后 ≤ 一个重试周期缓存回 healthy=true(灯回绿),无需等 5 分钟 | 同脚本 [2] |
| 3 | QMT 健康:健康态维持稳态间隔(默认 5 分钟)不空转 | 同脚本 [3] |
| 4 | QMT 健康:stop() 后停止自动调度 | 同脚本 [4] |
| 5 | MCP:无激活连接置 disabled 且不调度(真实 probe 早退不触 SDK | test-mcp-status [1] |
| 6 | MCP:异常态按 retryMs 快速重试;MCP 服务恢复后自动探测到 connected(灯回绿) | 同脚本 [2] |
| 7 | MCP:已连接态按 intervalMs 稳态不空转;dispose() 后停止调度;_nextDelay 节奏正确 | 同脚本 [3][4][5] |
| 8 | 启动即探测 + 读缓存秒回语义不变(QMT 健康 / MCP 状态) | 两脚本 [1]sync-status/qmt-health/mcp-status 端点零改动 |
| 9 | 构建产物(lib/)含两处自适应逻辑;typecheck 0 错;存量不回归 | pnpm typecheck + pnpm buildtest-quote-sync / test-position-sync 通过 |
## 验收方法
- 自动化:node scripts/test-health-monitor.mjs10/10+ node scripts/test-mcp-status.mjs12/12)全绿;
pnpm typecheckpnpm build;存量回归脚本 0 退出。
- 人工(老师真实环境):重启插件后四灯状态秒回;停 QMT Bridge → QMT 灯 ≤10s 转红、MCP 灯 ≤10s 转红、
持仓/行情灯按 30s/15s 阈值转黄;**重新启动 QMT Bridge → QMT 灯与 MCP 灯 ≤20s 自动回绿**(本次修复核心);
点击 QMT 灯 /「检测 MCP」仍即时探测并刷新悬停详情。
## 验收目标
修复确认:QMT Bridge 及其 MCP 服务启动/恢复后,会话头部 QMT连接灯与 MCP 灯自动回绿(≤20s),
与其余两盏数据灯恢复节奏一致;健康/已连接稳态无额外探测开销;前端与 API 行为不变。
验收通过后 R-022 / R-023 定稿并归档(老师确认)。
@@ -0,0 +1,88 @@
# 技术实现方案:20-策略列设置拖拽排序
## 总体思路
在 ColumnSettingsPopover.jsx 内复用 Tab 设置(SettingsSection.jsx TabSettings)已验证的
**原生 HTML5 Drag & Drop** 模式:行 draggable + onDragStart/onDragOver/onDrop + 落点高亮 + drop 重排,
零第三方依赖。持久化沿用现有 strategy-columns/update 整表覆盖 RPC,把「保存」语义从手动按钮改为
每次变更(drop / 勾选 / ↑↓)立即提交。
## 改动点(单文件:src/client/views/ColumnSettingsPopover.jsx
### 1. 状态
- 已有 `dragKey`(未被使用,正好用于 DnD):当前被拖行的 column.key
- 新增 `overKey`:当前悬停落点的 column.key(用于高亮);
- `saving` 保持:正在写库时禁止再次拖动/勾选。
### 2. 持久化函数(替换原 save)
- 新增 `persist(nextList, msg)`:提交整表 `{key, visible}` → strategy-columns/update
成功 Toast 提示 + onSaved() 刷新;失败 Toast 错误 + 本地回滚(还原为服务端最新列配置)。
- 勾选显隐:切完立即 persist(不再等「保存」按钮);
- ↑↓ 箭头:移动完立即 persist;
- drop 重排:重排完立即 persist(对齐 Tab 设置)。
### 3. 拖拽事件(照搬 TabSettings 模式)
```jsx
<div key={c.key} draggable={!saving}
onDragStart={(e) => { setDragKey(c.key); e.dataTransfer.effectAllowed = 'move'; }}
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setOverKey(c.key); }}
onDragLeave={() => { if (overKey === c.key) setOverKey(null); }}
onDrop={(e) => { e.preventDefault(); handleDrop(c.key); }}
style={{ cursor: saving ? 'default' : 'grab',
background: overKey === c.key ? 'color-mix(in srgb, var(--dsw-alias-state-business-primary, #1565c0) 12%, transparent)' : 'transparent' }}>
<span></span>
<button></button><button></button>
<input type="checkbox" ... />
...
</div>
```
### 4. handleDrop(照搬 TabSettings 逻辑,key 定位)
```js
const handleDrop = (targetKey) => {
if (!dragKey || dragKey === targetKey) { setDragKey(null); setOverKey(null); return; }
const from = list.findIndex((c) => c.key === dragKey);
const to = list.findIndex((c) => c.key === targetKey);
if (from < 0 || to < 0) { setDragKey(null); setOverKey(null); return; }
const next = list.slice();
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
setDragKey(null); setOverKey(null);
persist(next, `${moved.label}」已移动`, next);
};
```
### 5. UI 文案
- 标题下提示改为:「勾选显示列,按住 ≡ 拖动调整顺序;代码/名称/操作固定」;
- 底部按钮区:去掉「保存」,保留「关闭」;
- 每行最前加 ≡ 手柄(配色沿用 Tab 设置 `#999`/label-tertiary)。
## 边界与不做
- 服务端、API、表格渲染零改动(strategy-columns/update 已是整表覆盖语义);
- 固定列不参与排序,不进列表(现状维持);
- 不做方案 C(表格表头直接拖列头),不引入第三方 DnD 库;
- `saving` 期间禁用拖拽与勾选,防并发写。
## 回归
- scripts/test-r013-columns.mjs(列配置归一化/读写的服务端逻辑,未改动应全绿);
- npm run typecheck + buildtsdown 产物,弹层 JSX 变更需重编 web bundle);
- 页面人工验收:拖拽重排 / 落点高亮 / drop 即存 / ↑↓ 微调 / 刷新保持。
## 实现纪要(2026-09-10,老师验收反馈后修订)
> 老师反馈:初版按原方案做的是「整行高亮」(overKey → 目标行淡蓝底),拖动时无法判断会插入目标列的**前面还是后面**,
> 观感不符合习惯。定稿修订为 **间隙高亮线**:
- 落点判定:拖动悬停时读鼠标在目标行内的 **Y 坐标**getBoundingClientRect),上半 → 插入该行**之前**(线画行上缘),
下半 → 插入该行**之后**(线画行下缘);两行之间显示 3px 主色圆角高亮线(absolute 定位在行自身 padding 内,left/right 6
无负偏移无裁剪风险),指示精确插入位置。
- drop 判定:drop 事件内直接按坐标重算 before(不依赖 state 闭包,避免滞后);无操作原地(insertAt 等于原槽位)
跳过写库;移除后目标下标左移(to > from 时 to-1)。
- 状态:overKey(整行)→ overPos { idx, half }(间隙线锚点行 + 上下缘);保留 onDragEnd 清理防拖出弹层残留。
- 边界维持:固定列不参与、服务端零改动、↑↓ 兜底保留、立即持久化不变。
@@ -0,0 +1,47 @@
# 迭代复盘:20-策略列设置拖拽排序
## 结论
达成(R-024):策略 tab「列设置」弹层的列排序从 ↑↓ 箭头点按升级为**鼠标选中后拖动排序**,
复用 R-011 Tab 设置已验证的原生 HTML5 DnD 模式;老师 2026-09-10 页面确认交互直观,验收通过。
## 事实记录
### src/client/views/ColumnSettingsPopover.jsx(单文件改动,服务端零改动)
- 排序交互升级:每行新增 ≡ 拖拽手柄(整行 draggable),拖到目标位置松手即重排;↑↓ 箭头保留作兜底微调。
- **落点立即持久化**:勾选显隐 / 拖拽 / ↑↓ 任一操作立即调 strategy-columns/update 整表提交;弹层去掉「保存」
按钮只留「关闭」,Toast 反馈;saving 期间禁用拖拽与勾选防并发写;失败回滚到服务端最新配置。
- **落点指示修订(老师验收反馈)**:初版为整行高亮(overKey → 目标行淡蓝底),老师反馈无法判断插入目标列的
前/后;改为**行间间隙高亮线**——按鼠标在目标行内的 Y 坐标取上半/下半,决定线画在该行上缘(插其前)/下缘(插其后),
明确指示精确插入位置;drop 事件内按坐标重算(不依赖 state 闭包),拖回原位置不重复写库,onDragEnd 清理残留。
- 修复:间隙线渲染曾误将 style 对象直接作 React child`{gapLineStyle(i)}`),改为条件渲染 `<div style={gapLineStyle(i)}/>`
### 验证
- pnpm typecheck 0 错;pnpm build 通过(client bundle 185KB 含间隙线逻辑,grep 确认编译产物);
- test-r013-columns.mjs 回归:11 过 / 5 败 —— **与本次改动无关**git stash 基线验证同样 11/5);
失败根因:脚本仍按 R-013 时期期望「6 个基础列」,而 COLUMN_META 已有 10 列(R-015 新增涨停/跌停/今开/最高 4 个默认隐藏列),
测试期望未随迭代 13 更新的存量债务,不在本迭代范围,列为遗留事项。
### 文档
- docs/05-需求池/R-024.md:定稿 + 变更记录(整行高亮 → 间隙高亮线);
- docs/03-设计约束/UI交互约束.md:新增 UI约束-007(列表/弹层排序交互规范:间隙高亮线标准,Tab 设置是否统一待老师评估);
- docs/04-迭代记录/20-策略列设置拖拽排序/:迭代目标 + 技术实现方案(含实现纪要)+ 验收标准(10 项)。
## 遗留事项(待老师决策)
1. **test-r013-columns.mjs 期望过期**COLUMN_META 现 10 列(R-015 新增 4 行情列默认隐藏),脚本期望仍为 6 列,
默认列数相关 5 项断言失败。建议后续小迭代同步更新脚本期望(或重构为从 COLUMN_META 推导期望),本次未动。
2. **Tab 设置整行高亮是否统一为间隙高亮线**:UI约束-007 列设置弹层已用间隙线,Tab 设置(UI约束-003)仍为整行高亮;
老师反馈列设置「直观多了」——若 Tab 设置同样已存在「无法判断插入前/后」的观感问题,可同模式调整(独立小任务,本次未动)。
## 经验沉淀(候选)
- 排序类拖拽的落点指示,**间隙线(两行之间)比整行高亮更符合用户观感**:整行高亮只能表达「落在哪一行」,
无法表达「插入该行前还是后」;主流列表/表格拖拽(如 Notion、表格列排序)均用间隙指示。
- 服务端整表覆盖语义(updateStrategyColumns 整表提交)天然适配前端即时持久化,无需逐条增量接口;
「保存按钮」在每次变更即写库后失去存在意义(本迭代据此移除)。
- 回归脚本的期望值要随数据模型演进同步更新(test-r013-columns 教训:R-015 扩展 COLUMN_META 后脚本未跟随,
留下 5 项静默失败,直到本迭代回归才暴露)。
@@ -0,0 +1,30 @@
# 迭代目标:20-策略列设置拖拽排序
## 目标
策略 tab「列设置」弹层(ColumnSettingsPopover)的列排序交互从 **↑↓ 箭头点按** 升级为
**鼠标选中后拖动排序**(原生 HTML5 DnD),并对齐 Tab 设置的即时持久化体验。
## 目标描述
- **背景**:策略 tab 的可配置列(基础数据列 + 自定义字段列)在「列设置」弹层中管理,排序目前依赖
↑↓ 箭头(R-013 迭代 11 产物,当时刻意简化,代码注释自述「用箭头替代 DnD」)。老师反馈希望
改成鼠标选中后拖动排序(2026-09-10)。
- **结论**:复用 R-011 Tab 设置已验证的原生 HTML5 DnD 模式(draggable 行 + 落点高亮 + drop 重排),
零第三方依赖;落点立即持久化(strategy-columns/update 整表覆盖),弹层去掉「保存」按钮只留「关闭」。
- **范围**:仅改 src/client/views/ColumnSettingsPopover.jsx 单文件;服务端/API/表格渲染零改动;
固定列(代码/名称/操作/展开箭头)不参与排序,维持现状。
- **验收线**:拖拽可重排列顺序且落点高亮可见;drop 后立即写库(无需点保存);↑↓ 箭头仍可微调;
勾选显隐立即生效;刷新后列顺序保持。对应需求 R-024(已定稿)。
## 目标讨论过程
1. 老师反馈(2026-09-10):策略 tab 列设计可否做成鼠标选中后拖动排序。
2. AI 调研:Tab 设置(R-011)已实现同款原生 HTML5 DnDSettingsSection.jsx),可零依赖复用;
列设置弹层存在未使用的 dragKey 状态(原计划 DnD 的残留)。
3. 登记 R-024(讨论中)→ 方案草案 A/B/C → 老师确认方案 A(弹层内拖拽);
保存时机 / 箭头去留按 AI 推荐默认(立即持久化 + ↑↓ 保留兜底)。
## 对老师(项目主理人)的配合需求
- 无(纯前端交互优化,服务端零改动,验收走页面人工确认)。
@@ -0,0 +1,32 @@
# 验收标准:20-策略列设置拖拽排序
## 验收线(在哪里验收)
DSH Web GUI → 神之一手 → 任一策略 tab → 工具条「列设置」按钮 → 弹层。
页面侧人工验收;服务端逻辑零改动,跑 scripts/test-r013-columns.mjs 回归确认无破坏。
## 验收方法(逐项操作)
| # | 操作 | 预期 |
|---|---|---|
| 1 | 打开「列设置」弹层,鼠标按住某行 ≡ 手柄(含整行)拖动到另一行附近移动鼠标 | 目标行上缘/下缘显示**主色间隙高亮线**(插入线),明确指示落点在目标列之前还是之后,非整行高亮 |
| 2 | drop 后直接关闭弹层(不点任何保存) | 策略表格表头列顺序已按新顺序展示(立即持久化生效) |
| 3 | 刷新页面 / 重新打开弹层 | 列顺序保持拖拽后的结果(写库成功) |
| 4 | 点 ↑ / ↓ 箭头 | 仍可逐格微调,且立即持久化(关闭弹层后表格列顺序同步变化) |
| 5 | 勾选/取消勾选某一列 | 显隐立即生效(关闭弹层后,表格对应列隐藏/出现) |
| 6 | 拖动到目标行上半区 vs 下半区各松手一次 | 上半区 = 插入该目标列**之前**;下半区 = 插入该目标列**之后**(以落点线位置为准) |
| 7 | 拖动期间把行拖回原位置或原地松手 | 无异常,列顺序不变(不重复写库) |
| 8 | 固定列(代码/名称/操作) | 弹层列表不含固定列,不参与拖拽,恒显示(维持现状) |
| 9 | 自定义字段列(某策略配了 configSchema) | 与基础列一样可拖拽排序、可勾选显隐 |
| 10 | 保存中(快速连续拖动) | 无并发写异常;saving 期间拖拽/勾选禁用,Toast 提示保存结果 |
## 验收目标
- 功能正交:拖拽排序 / ↑↓ 微调 / 显隐勾选 三者并存且各自立即持久化;
- 零回归:scripts/test-r013-columns.mjs 全绿、typecheck + build 通过;
- 视觉一致性:≡ 手柄沿用 Tab 设置(UI约束-003)同套样式;落点为**间隙高亮线**(主色 token,UI约束-004 无硬编码色值);
- 服务端零改动确认:git diff 无 src/api / src/settings.js 变更。
## 判定
- 老师页面人工确认 10 项全过 + 服务端回归全绿 ⇒ 迭代 20 验收通过,R-024 归档至 已完成/ 并更新索引实现状态。
@@ -0,0 +1,84 @@
# 技术实现方案:21-Tab设置排序间隙高亮统一
## 总体思路
把 SettingsSection.jsx 的 TabSettings 组件落点指示从「整行高亮(overId → 行背景 color-mix 12%)」
改为「行间间隙高亮线(overPos { idx, half } → 目标行上缘/下缘 3px 主色线)」,与 ColumnSettingsPopover.jsx
(R-024)逐点对齐;其余交互(≡ 手柄、开关、立即持久化、徽标、saving 禁用)不动。
## 改动点(单文件:src/client/views/SettingsSection.jsx,仅 TabSettings 组件)
### 1. 状态
- `overId`(整行高亮锚点)→ `overPos { idx, half }`(间隙线锚点行下标 + 上/下缘);
- `dragId` 保留。
### 2. 悬停判定(与 R-024 全同)
```jsx
const handleOver = (e, index) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const rect = e.currentTarget.getBoundingClientRect();
const half = e.clientY - rect.top < rect.height / 2 ? 'top' : 'bottom';
setOverPos({ idx: index, half });
};
```
### 3. drop(事件内按坐标重算,不依赖 state 闭包)
```jsx
const handleDrop = async (e, index) => {
e.preventDefault();
if (!tabs || !dragId) { setDragId(null); setOverPos(null); return; }
const rect = e.currentTarget.getBoundingClientRect();
const before = e.clientY - rect.top < rect.height / 2;
let to = before ? index : index + 1;
const from = tabs.findIndex((x) => x.id === dragId);
if (from < 0 || to < 0 || to > tabs.length) { setDragId(null); setOverPos(null); return; }
if (to === from || to === from + 1) { setDragId(null); setOverPos(null); return; } // 原地跳过写库
const next = tabs.slice();
const [moved] = next.splice(from, 1);
const insertAt = to > from ? to - 1 : to;
next.splice(insertAt, 0, moved);
const ordered = next.map((x, i) => ({ ...x, order: i }));
setDragId(null);
setOverPos(null);
await persist(ordered, '「' + moved.name + '」已移动');
};
```
### 4. 行渲染(tr 上挂间隙线样式)
tr 为表格行,直接内嵌 div 会破坏表格结构(tr 只允许 td/th)——**间隙线以 td 为定位锚点绘制**:
首选在每个 td 内各画一段 3px 高亮线(结构合规、视觉连续),实现纪要记录最终选型。
```jsx
<tr
key={t.id}
draggable={!saving}
onDragStart={(e) => { setDragId(t.id); setOverPos(null); e.dataTransfer.effectAllowed = 'move'; }}
onDragOver={(e) => handleOver(e, i)}
onDrop={(e) => handleDrop(e, i)}
onDragEnd={() => { setDragId(null); setOverPos(null); }}
style={{ cursor: saving ? 'default' : 'grab' }}
>
<td style={{ position: 'relative', padding: '8px 6px' }}> {gapLine(i, 'td1')} </td>
...
</tr>
```
间隙线样式(与 R-024 全同):absolute、left/right 内缩 6、height 3、borderRadius 2、
背景 primary tokentop:0(插前)/ bottom:0(插后)。
## 边界与不做
- 服务端/API/表格列结构/持久化逻辑零改动;
- 不动「策略分组」子 tab(该处无排序交互);
- 不引入第三方 DnD 库。
## 回归
- pnpm typecheck + buildSettingsSection 变更需重编 web bundle);
- 页面人工验收:Tab 设置拖动 → 间隙线位置正确(上=前/下=后)、整行高亮消失、drop 即存、
拖回原位不重复写库、刷新页面后顺序保持。
@@ -0,0 +1,26 @@
# 迭代目标:21-Tab设置排序间隙高亮统一
## 目标
设置页「Tab 设置」的分组 tab 拖拽排序落点指示,从**整行高亮**统一为**行间间隙高亮线**,
与策略 tab 列设置弹层(R-024/UI约束-007)完全一致——明确指示插入目标行的前/后。
## 目标描述
- **背景**:迭代 20 复盘遗留第 2 项——R-024 将列设置弹层落点改为间隙高亮线后老师确认「直观多了」,
但设置页 Tab 设置(R-011/UI约束-003)仍是整行高亮(overId → 目标行淡蓝底),无法判断插入前/后,
两处观感不一致。2026-09-10 老师指令:Tab 设置也统一为间隙高亮线形式。
- **范围**:仅改 src/client/views/SettingsSection.jsx 的 TabSettings 组件落点指示;表格结构、≡ 手柄、
显隐开关、立即持久化(tabs/update)、徽标、saving 禁用全部维持不变;服务端/API 零改动。
- **验收线**:拖动时目标行上缘/下缘显示主色间隙线(上半区=插前、下半区=插后);整行高亮消失;
拖回原位不重复写库;drop 后立即持久化。对应需求 R-025(已定稿)。
## 目标讨论过程
1. 迭代 20 复盘(2026-09-10)列遗留第 2 项:Tab 设置整行高亮是否统一为间隙线,留待老师评估。
2. 老师指令(2026-09-10):设置页分组 tab 排序也改上面(列设置弹层)的拖动高亮形式 → 登记 R-025 已定稿,
迭代 21 实施。
## 对老师(项目主理人)的配合需求
- 无(纯前端交互统一,服务端零改动,验收走页面人工确认)。
@@ -0,0 +1,27 @@
# 验收标准:21-Tab设置排序间隙高亮统一
## 验收线(在哪里验收)
DSH Web GUI → 设置页 → 「Tab 设置」子 tab → 拖动任一标签行排序。
页面侧人工验收;服务端逻辑零改动。
## 验收方法(逐项操作)
| # | 操作 | 预期 |
|---|---|---|
| 1 | 打开「Tab 设置」,按住某行 ≡ 手柄(含整行)拖动到另一行附近移动鼠标 | 目标行上缘/下缘显示**主色间隙高亮线**(插入线),明确指示落点在目标 tab 之前还是之后;原整行淡蓝高亮消失 |
| 2 | 拖动到目标行上半区 vs 下半区各松手一次 | 上半区 = 插入该 tab **之前**;下半区 = 插入该 tab **之后**(以落点线位置为准) |
| 3 | drop 后落点立即生效 | Toast「已移动」,刷新页面后 tab 顺序保持(tabs/update 已写库) |
| 4 | 拖动期间把行拖回原位置或原地松手 | 无异常,顺序不变,且不触发重复写库 |
| 5 | 既有交互回归 | ≡ 手柄、显示/隐藏开关、内置/策略徽标、saving 期间拖拽禁用——全部维持原行为 |
| 6 | 策略分组子 tab | 无排序交互,不受影响(仍为 新增/重命名/删除) |
## 验收目标
- 功能对准:Tab 设置落点与列设置弹层(R-024)**同一套间隙线交互**,观感一致;
- 零回归:既有 tabs 持久化(整表 order + visible)、显隐开关、策略改名联动 name join 不受影响;
- typecheck + build 通过;无新增硬编码色值(UI约束-004)。
## 判定
- 老师页面人工确认 6 项全过 + build 通过 ⇒ 迭代 21 验收通过,R-025 归档至 已完成/ 并更新索引实现状态。