Files
kyugao aac16b904c feat(R-013): 策略自定义字段完整实现 + 列显隐配置 + 单元格编辑
功能实现(迭代 11):
- 数据层: strategies.configSchema 自定义字段定义(text/number/boolean/enum + def + unit 单位可空)
  + strategy_holdings "values" JSON 列(幂等 ALTER)+ readValues/writeValues + 归一化
- API: holdings/values-update 端点(服务端按 configSchema 校验: number有限/enum在选项/boolean布尔)
- 设置页: 「策略分组」策略行展开配置字段(StrategyFieldsEditor 独立组件)
- 持仓表: 自定义字段列化 + 列显隐/排序(ColumnSettingsPopover,每策略独立 strategyColumns 配置)
- 单元格点击编辑(FieldCellEditor): 文本/数字输入框、布尔开关、枚举下拉 + ✓ 确认
- UI 细节: ✎ 可编辑标记(值后)、编辑列宽稳定、数字输入框固定 5 位宽

修复:
- SettingsSection Fragment/StrategyFieldsEditor 未导入导致的配置页白屏
- z.dict schema dts 推断 cosmokit 引用(显式类型注解)
- 自定义字段从展开交易明细移出(字段已列化)

文档: R-013/迭代11 技术方案/验收标准/迭代目标 同步(D6 演进 + unit + 列配置)
回归: test-r013-custom-fields 21/21 + test-r013-columns 16/16 + tabs 23/23
2026-09-02 17:21:41 +08:00

8.7 KiB
Raw Permalink Blame History

技术实现方案:11-策略自定义字段配置

迭代编号:11 依据:PLAN-012 + R-013(目标数据模型 / 端到端示例)+ 技术约束-015 / 产品约束-010 / UI约束-005

1. 数据层

1.1 settings.strategies 扩展 configSchemasrc/settings.js

// strategySchema 中 strategies 项扩展:
strategies: z.array(z.object({
  id: z.string().required(),
  name: z.string().required(),
  configSchema: z.array(z.object({
    key:   z.string().required(),        // 稳健标识(英文 slug
    label: z.string(),                   // UI 展示名(可中文)
    type:  z.union([z.const('text'), z.const('number'), z.const('boolean'), z.const('enum')]).required(),
    enum:  z.array(z.string()),          // 仅 type=enum 时使用
    def:   z.any(),                      // 默认值(text=string / number=number / boolean=boolean / enum=枚举项)
    unit:  z.string().default(''),       // 单位(2026-09-02 加:可为空;如 % / 元 / 手;展示时拼在值后)
  })).default([]),                        // 缺省 []
})).default(DEFAULT_STRATEGIES),
  • 读取归一化:旧项无 configSchema → 补默认空数组(读取时或 schema default 保证);
  • addStrategy 返回值默认带 configSchema: []renameStrategy / updateStrategies 整表更新天然携带;
  • 兼容:settings 旧数据({id,name})经 schemastery default/union 归一化,无迁移脚本。

1.2 strategy_holdings 新增 values 列 + 读写(src/storage/SqliteStore.js

// init() 内补列(幂等,沿用 _ensureTradeAttributionColumns 模式;只读连接容忍)
_ensureHoldingValuesColumn() {
  const cols = new Set(this.db.prepare('PRAGMA table_info(strategy_holdings)').all().map(c => c.name));
  if (!cols.has('values')) this.db.exec('ALTER TABLE strategy_holdings ADD COLUMN values TEXT');
}

readValues(holdingId)   // SELECT values FROM strategy_holdings WHERE holding_id=? → JSON.parse ?? null
writeValues(holdingId, values)  // UPDATE strategy_holdings SET values=? WHERE holding_id=? → JSON.stringify
  • openHolding/addShares/reduceShares/closeHolding 不碰 values 列(份额生命周期与字段值独立);
  • getCurrentHoldings / getHoldingHistory 的 _mapHolding 附加 values 字段(解析 JSON)。

2. API 层(src/api/strategies.js

// 既有:strategy-positions → manager.getStrategyPositions(strategyId)
//   PositionManager 返回项附加 valuesfrom SqliteStore 映射)
// 新增端点:
case 'holdings/values-update':
  return await manager.updateHoldingValues(args.holdingId, args.values);
// src/position/PositionManager.js —— updateHoldingValues(holdingId, values)
// 1) 定位 holdings 行(须存在且当前持仓 closed_at IS NULL;否则 404
// 2) 服务端校验(按所属策略 configSchema):
//    number → Number.isFinite(Number(v))enum → enum.includes(v)boolean → typeof v === 'boolean'
//    未定义 key 的额外键放行(可扩展);空值/缺省可写入
// 3) 校验失败抛 { code: 'field-validation' },成功 writeValues 并返回 { holdingId, values }

3. 设置页 UIsrc/client/views/SettingsSection.jsx

  • 「策略分组」子 tab 策略行加展开手柄(▶/▸ toggle,样式沿用 expand 惯例);
  • 展开区:
    • 字段列表表格:展示名 | key | 类型 | 枚举选项 | 默认值 | 操作(删除)—— 空态提示「尚未配置自定义字段」;
    • 添加/编辑字段表单:字段名(label)→ 自动生成 key(拼音 slug,复用 generateStrategyId 思路)/ 可手改校验唯一、类型下拉(文本/数字/布尔/枚举)、枚举选项(type=enum 时逗号分隔输入)、默认值(按类型渲染输入);
    • 行内编辑/删除:编辑回填表单,删除后保存生效;
    • 保存:整表提交 strategies/update(映射为 { ...s, configSchema }),成功 Toast + 刷新(沿用「刷新页面后生效」机制)。

4. 策略持仓 tab UIsrc/client/views/StrategyTab.jsx

2026-09-02 演进(老师选 A + 列化):自定义字段不再放展开区,直接作为表格列展示;值编辑 = 点击字段单元格内联编辑;展开区(R-010)仅保留交易明细。

  • 自定义字段列按列配置(§7)渲染,取值 = values[key] ?? defboolean→开/关,空→—);
  • 点击字段单元格(有 holding 锚点行)进入内联编辑:text/number=输入框(Enter 保存 / Esc 取消 / blur 保存)、boolean=开关(点击即提交切换)、enum=下拉(选择即提交);
  • 保存调 one-divine-lot/holdings/values-update(按该行现有 values 合并,仅改本字段;空值删键回退 def);成功刷新,失败 Toast;
  • 单元格点击 stopPropagation,不与行展开(tr onClick)冲突;无 holding 锚点(未分配)的字段列不可编辑(虚线标识可点);

5. 回归脚本(scripts/test-r013-custom-fields.mjs

  • ODL_TEST_DATA_DIR 独立数据目录(技术约束-011);
  • 用例:addStrategy 带 configSchema(四类型)→ openHolding ×2 同策略不同 code → writeValues 各自值 → 读回校验 → 校验拦截(数字非法 / 枚举越界)→ 旧策略无 configSchema 持仓行 readValues 为 NULL
  • 断言存储与读取一致、校验拒绝。

7. 表格列显隐配置(迭代 11 范围扩展,2026-09-02 老师确认)

背景:R-013 落地后老师要求——策略持仓表的列可显示/隐藏配置:不限于自定义字段列,还包含持仓表原有基础数据列;除 代码/名称/操作(+展开箭头)外全部支持;每策略独立配置;入口 = 策略持仓 tab 顶部「列设置」;支持列排序。并入本次迭代。

7.1 数据模型(定稿)

// settings 新增 strategyColumns(仅存与默认不同的覆盖;读取时归一化)
strategyColumns: {
  [strategyId]: [
    { key: 'lastPrice', visible: true },        // 基础数据列(可配置区)
    { key: 'pctChange', visible: true },
    { key: 'lastClose', visible: false },
    { key: 'avgPrice', visible: true },
    { key: 'lastTradePrice', visible: true },
    { key: 'shares', visible: true },
    { key: 'gridGap', visible: true },           // 自定义字段列(key = configSchema.key
    { key: 'gridAmount', visible: true },
  ],
}

列分类

  • 固定列:展开箭头 | 代码 | 名称(恒显、锁最前)| 操作(锁最后,含 全部移入/移出)—— 不可配置;
  • 可配置区:基础数据列(现价/涨幅/昨收/成本价/最后一笔成交价/份额)+ 该策略全部自定义字段列(key=configSchema.key)—— 显隐 + 顺序可配置;
  • 自定义字段列存在性仍由 configSchema 决定(策略未配该字段则无此列);configSchema 增删字段 → 列配置读取时自动跟随(新增字段默认追加末尾、删除字段自然移除)。

显隐单一数据源(老师确认 2026-09-02):visible 只存在 strategyColumnsconfigSchema 不加 visible;展开区编辑区恒显示该策略全部字段(编辑入口需全量),表格列显隐由列配置单独管——避免双显隐源打架。

默认值/兼容:未配置 strategyColumns 的策略 → 归一化为 基础列全显(默认序)+ configSchema 字段列全显(configSchema 序);老数据缺字段 → 补默认。

7.2 涉及改动

  1. src/settings.js:基础列目录常量 COLUMN_METAkey/label);strategySchema + strategyColumns;读取归一化 normalizeStrategyColumns(基础列 + 该策略 configSchema 字段合成全列 → 应用覆盖 → 返回有序可见列);更新 API;
  2. src/api/strategies.js:新增 strategy-columns(读归一化结果)/ strategy-columns/update {strategyId, columns}
  3. src/client/views/StrategyTab.jsx:表格动态列化——表头/行按列配置渲染(固定列 + 可见可配置列),行数据映射每列取值;
  4. 新组件 ColumnSettingsPopover(views/):tab 顶部「列设置」按钮 → 弹层:可配置列列表(标签 + 显隐勾选 + 拖动排序)+ 立即持久化 strategy-columns/update
  5. 展开区(§4)不再显示自定义字段(已列化),仅保留交易明细;字段值编辑 = 表格字段单元格点击内联编辑(§4 演进);
  6. 回归脚本 + typecheck/build。

7.3 列取值(StrategyTab 渲染参考)

列 key 取值
lastPrice / lastClose / pctChange getPrice(code) 行情
avgPrice / lastTradePrice p 的字段(QMT 成本价 / R-010 最后一笔成交价)
shares p.shares
自定义字段 key p.values?.[key] ?? def(展示同展开区:boolean→开/关,空→—)

6. 验证

  • build + typecheck 通过;
  • 老师人工验收(见验收标准)。