迭代19-21收尾: 指示灯自适应探测(R-022/R-023)+ 列设置拖拽排序(R-024)+ Tab设置间隙线统一(R-025)+ 需求池归档(R-014/15/16/17/19 → 已完成)
This commit is contained in:
@@ -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-021,2026-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-019,2026-09-08):
|
||||
- 老师原始诉求「新需求,让 AI 会话可以以策略为 Workspace」经四轮讨论定稿(R-019,2026-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 自适应链**」:每次探测完成(成功/失败/异常)后按结果排下一轮。
|
||||
|
||||
| 维度 | QmtHealthMonitor(R-022) | QmtMcpManager(R-023) |
|
||||
|---|---|---|
|
||||
| 健康/已连接 | intervalMs 默认 5 分钟(稳态,原设计) | intervalMs 默认 5 分钟 |
|
||||
| 异常/未知/未连接 | retryMs 默认 10 秒快速重试 | retryMs 默认 10 秒快速重试 |
|
||||
| 探测目标 | 激活 baseUrl + /health(GET) | 激活 baseUrl + /mcp(SDK Client 握手,只读) |
|
||||
| 停止调度 | stop()(mounted 守卫) | 无激活连接 / dispose() / unmount() |
|
||||
| 并发防护 | _inFlight 去重 | _probing 去重 |
|
||||
| 前端/API | 零改动(读缓存秒回) | 零改动(getStatus 读缓存秒回) |
|
||||
|
||||
## 实现步骤
|
||||
|
||||
### R-022 QmtHealthMonitor(src/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 QmtMcpManager(src/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.mjs(10 项:启动即探测/异常快速重试自动恢复/健康稳态不空转/stop 停止调度…)
|
||||
- scripts/test-mcp-status.mjs(12 项:无激活连接不调度/异常快速重试自动恢复/已连接稳态不空转/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)。
|
||||
- **范围**:只改 QmtHealthMonitor(R-022)与 QmtMcpManager(R-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 build;test-quote-sync / test-position-sync 通过 |
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 自动化:node scripts/test-health-monitor.mjs(10/10)+ node scripts/test-mcp-status.mjs(12/12)全绿;
|
||||
pnpm typecheck;pnpm 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 + build(tsdown 产物,弹层 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 DnD(SettingsSection.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 token;top:0(插前)/ bottom:0(插后)。
|
||||
|
||||
## 边界与不做
|
||||
|
||||
- 服务端/API/表格列结构/持久化逻辑零改动;
|
||||
- 不动「策略分组」子 tab(该处无排序交互);
|
||||
- 不引入第三方 DnD 库。
|
||||
|
||||
## 回归
|
||||
|
||||
- pnpm typecheck + build(SettingsSection 变更需重编 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 归档至 已完成/ 并更新索引实现状态。
|
||||
Reference in New Issue
Block a user