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:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user