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
This commit is contained in:
2026-09-02 17:21:41 +08:00
parent 6f74ed4a79
commit aac16b904c
17 changed files with 1270 additions and 61 deletions
+52 -4
View File
@@ -9,9 +9,10 @@
* - QMT 连接配置:R-004 卡片形式(迭代 03)
*/
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, Fragment } from 'react';
import { useRpc } from './connection.jsx';
import { ToastProvider, useToast } from './Toast.jsx';
import { StrategyFieldsEditor } from './StrategyFieldsEditor.jsx'; // R-013:策略自定义字段编辑器
/** 确认弹窗 */
function ConfirmDialog({ title, message, onConfirm, onCancel }) {
@@ -183,6 +184,9 @@ function StrategyGroupSettings() {
const [editingName, setEditingName] = useState('');
const [confirmDelete, setConfirmDelete] = useState(null);
const [changedMsg, setChangedMsg] = useState(null);
// R-013:当前展开配置字段的策略 id(null=无展开)
const [expandedId, setExpandedId] = useState(null);
const [fieldSaving, setFieldSaving] = useState(false);
const call = useRpc();
const toast = useToast();
@@ -244,6 +248,25 @@ function StrategyGroupSettings() {
}
};
/** R-013:保存某策略的自定义字段定义(整表 strategies/update */
const handleSaveConfig = async (id, configSchema) => {
setFieldSaving(true);
try {
const res = await call('one-divine-lot/strategies/update', {
args: { strategies: strategies.map((s) => (s.id === id ? { ...s, configSchema } : s)) },
});
if (res && res.ok) {
handleChanged('策略自定义字段已保存');
} else {
toast.error((res && res.error && res.error.message) || '保存失败');
}
} catch (e) {
toast.error(e.message);
} finally {
setFieldSaving(false);
}
};
const handleDelete = async (id) => {
setSaving(true);
try {
@@ -310,7 +333,8 @@ function StrategyGroupSettings() {
</thead>
<tbody>
{strategies.map((s) => (
<tr key={s.id} style={{ borderBottom: '1px solid var(--dsw-alias-border-l1, #eee)' }}>
<Fragment key={s.id}>
<tr style={{ borderBottom: '1px solid var(--dsw-alias-border-l1, #eee)' }}>
<td style={{ padding: '8px 6px' }}>
{editingId === s.id ? (
<span>
@@ -325,11 +349,22 @@ function StrategyGroupSettings() {
<button onClick={() => setEditingId(null)} style={{ padding: '2px 8px', cursor: 'pointer', marginLeft: 4 }}>取消</button>
</span>
) : (
s.name
<span>
<button
onClick={() => { setExpandedId(expandedId === s.id ? null : s.id); }}
style={{ padding: '2px 6px', cursor: 'pointer', border: 'none', background: 'none', fontSize: 12, color: 'var(--dsw-alias-label-secondary, #666)', marginRight: 6 }}
title={expandedId === s.id ? '收起配置' : '展开配置自定义字段'}
>{expandedId === s.id ? '▾' : '▸'}</button>
{s.name}
</span>
)}
</td>
<td style={{ padding: '8px 6px', color: 'var(--dsw-alias-label-secondary, #666)', fontSize: 12 }}>{s.id}</td>
<td style={{ padding: '8px 6px', whiteSpace: 'nowrap' }}>
<button
onClick={() => { setExpandedId(expandedId === s.id ? null : s.id); setEditingId(null); }}
style={{ padding: '2px 8px', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 3, background: 'var(--dsw-alias-bg-layer-1, #fff)', marginRight: 4 }}
>{expandedId === s.id ? '收起字段' : '配置字段'}</button>
<button
onClick={() => { setEditingId(s.id); setEditingName(s.name); }}
style={{ padding: '2px 8px', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 3, background: 'var(--dsw-alias-bg-layer-1, #fff)', marginRight: 4 }}
@@ -340,6 +375,19 @@ function StrategyGroupSettings() {
>删除</button>
</td>
</tr>
{expandedId === s.id && (
<tr>
<td colSpan={3} style={{ padding: '4px 10px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1, #eee)' }}>
<StrategyFieldsEditor
strategy={s}
saving={fieldSaving}
onSave={(fields) => handleSaveConfig(s.id, fields)}
onClose={() => setExpandedId(null)}
/>
</td>
</tr>
)}
</Fragment>
))}
</tbody>
</table>
@@ -722,4 +770,4 @@ function QmtConnectionsSettings() {
)}
</div>
);
}
}