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

140 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 技术实现方案:11-策略自定义字段配置
> 迭代编号:11 依据:PLAN-012 + R-013(目标数据模型 / 端到端示例)+ 技术约束-015 / 产品约束-010 / UI约束-005
## 1. 数据层
### 1.1 settings.strategies 扩展 configSchemasrc/settings.js
```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
```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
```js
// 既有:strategy-positions → manager.getStrategyPositions(strategyId)
// PositionManager 返回项附加 valuesfrom SqliteStore 映射)
// 新增端点:
case 'holdings/values-update':
return await manager.updateHoldingValues(args.holdingId, args.values);
```
```js
// 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 数据模型(定稿)
```js
// 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 **只存在 strategyColumns**configSchema **不加 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 通过;
- 老师人工验收(见验收标准)。