迭代22+23: 策略tab静态化 + 计算字段(数字/判定型)+ 网格超市信号字段
- R-027 静态化:策略恒为内置两项(不可增删改名,保留显隐/排序);设置页移除「策略分组」; 字段配置入口迁到策略 tab 内「列设置」旁(FieldConfigDialog + schema-update 单策略写入) - R-026 计算字段:自写公式引擎(中文变量、四则/括号/round·abs·min·max、缺值短路→—)+ 变量目录(行情/合约/持仓/自定义字段)+ strategy-positions 逐行现算(只读内存缓存,不落库、列只读) - R-028 判定型:比较运算 + and/or + inferResultKind + 结果类型一致性校验 + 判定列 ✓/— 渲染; 网格超市落地 可下空单=涨停价>基准值+网格大小、可下多单=跌停价<基准值-网格大小 - 变量选择改标签平铺(老师反馈);公式手册 docs/99-其他材料/计算字段公式说明.md - 约束同步:产品约束-002/003/004/009/013/014、技术约束-014/015/022/023/024、UI约束-002/003/005/008 - 回归:新增 test-r026/test-r027,更新 r011/r013/r017,17 个脚本全绿;typecheck/build 通过
This commit is contained in:
+156
-148
@@ -15,6 +15,12 @@
|
||||
* - addStrategy 联动追加 tab(末尾,Q2);removeStrategy 联动删除对应 tab(D3)
|
||||
* - 策略行 name 由 getTabs join strategies(改名自动跟随,Q4)
|
||||
* - removeStrategy 的份额清理由上层(api.js 编排 storage)完成
|
||||
*
|
||||
* R-027(迭代 22 阶段一)策略 tab 静态化:
|
||||
* - 策略集合与名称固定为内置两项(BUILTIN_STRATEGIES):不可新增/删除/重命名
|
||||
* - tab 列表由 内置 tab + 内置策略 tab 常量派生;存量 tabs 仅保留 visible/order 偏好
|
||||
* - 字段定义真相源迁到 strategyFields;strategies[] 收窄为身份表(仅兼容读取旧 configSchema)
|
||||
* - 退役 addStrategy/removeStrategy/renameStrategy/updateStrategies/append|removeStrategyTab
|
||||
*/
|
||||
|
||||
import z from '@deepseek-ai/schemastery';
|
||||
@@ -26,15 +32,31 @@ export const BUILTIN_TABS = [
|
||||
{ id: 'tab-watchlist', refKey: 'watchlist', name: '关注列表' },
|
||||
];
|
||||
|
||||
/** 默认 tab 列表(首次注册时的初始值;策略行由 DEFAULT_STRATEGIES 派生) */
|
||||
export const DEFAULT_TABS = BUILTIN_TABS.map((t, i) => ({
|
||||
id: t.id, kind: 'builtin', refKey: t.refKey, name: t.name, visible: true, order: i,
|
||||
}));
|
||||
/** 内置策略(R-027 静态化:集合与名称固定,不可增删改名;字段定义另存 strategyFields) */
|
||||
export const BUILTIN_STRATEGIES = [
|
||||
{ id: 'grid-supermarket', name: '网格超市' },
|
||||
{ id: 'manual-t', name: '手动做T' },
|
||||
];
|
||||
|
||||
/** 默认策略(首次注册时的初始值) */
|
||||
export const DEFAULT_STRATEGIES = [
|
||||
{ id: 'grid-supermarket', name: '网格超市', configSchema: [] },
|
||||
{ id: 'manual-t', name: '手动做T', configSchema: [] },
|
||||
/** 内置策略 id 判定(字段定义写入 / 校验用) */
|
||||
export function isBuiltinStrategy(strategyId) {
|
||||
return BUILTIN_STRATEGIES.some((s) => s.id === strategyId);
|
||||
}
|
||||
|
||||
/** 未知策略 id 直接抛错(写路径守卫) */
|
||||
export function assertBuiltinStrategy(strategyId) {
|
||||
if (!isBuiltinStrategy(strategyId)) {
|
||||
throw Object.assign(new Error('未知策略: ' + strategyId), { code: 'strategy-not-found' });
|
||||
}
|
||||
}
|
||||
|
||||
/** 默认 tab 列表(首次注册时的初始值;R-027:内置 tab + 内置策略 tab) */
|
||||
export const DEFAULT_TABS = [
|
||||
...BUILTIN_TABS.map((t, i) => ({ id: t.id, kind: 'builtin', refKey: t.refKey, name: t.name, visible: true, order: i })),
|
||||
...BUILTIN_STRATEGIES.map((s, i) => ({
|
||||
id: 'tab-strategy-' + s.id, kind: 'strategy', refId: s.id, name: s.name,
|
||||
visible: true, order: BUILTIN_TABS.length + i,
|
||||
})),
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -56,6 +78,28 @@ export const COLUMN_META = [
|
||||
{ key: 'high', label: '最高', fixed: false, defaultVisible: false },
|
||||
];
|
||||
|
||||
/**
|
||||
* 策略自定义字段定义 schema(R-013 四类型 + R-026 计算类型)
|
||||
* - type='formula' 时使用 formula(中文变量表达式串)+ decimals(显示小数位),不使用 def
|
||||
*/
|
||||
const fieldSchema = z.object({
|
||||
key: z.string().required(), // 稳健标识(英文 slug,与字段值 JSON 的 key 对齐)
|
||||
label: z.string(), // UI 展示名(可中文;计算字段中并作公式变量名)
|
||||
type: z.union([
|
||||
z.const('text'),
|
||||
z.const('number'),
|
||||
z.const('boolean'),
|
||||
z.const('enum'),
|
||||
z.const('formula'), // R-026:计算字段(值由公式实时算出,不落库)
|
||||
]).required(),
|
||||
enum: z.array(z.string()).default([]), // 仅 type=enum:预设选项
|
||||
def: z.any(), // 默认值(text=string / number=number / boolean=boolean / enum=枚举项)
|
||||
unit: z.string().default(''), // 单位(可为空;如 % / 元 / 手 —— 展示时拼在值后)
|
||||
formula: z.string().default(''), // 仅 type=formula:表达式串(中文变量)
|
||||
resultKind: z.union([z.const('number'), z.const('boolean')]).default('number'), // R-028:数字 / 判定
|
||||
decimals: z.number().default(2), // 仅 type=formula 且 resultKind=number:显示小数位(0-4)
|
||||
});
|
||||
|
||||
/** 策略设置 schema(schemastery 定义,settings.register 要求 schema 对象) */
|
||||
/** @type {import('@deepseek-ai/schemastery').Schema<any>} 策略设置 schema(显式注解:含 z.dict,规避 dts 推断 cosmokit Dict 引用) */
|
||||
export const strategySchema = z.object({
|
||||
@@ -65,20 +109,10 @@ export const strategySchema = z.object({
|
||||
strategies: z.array(z.object({
|
||||
id: z.string().required(),
|
||||
name: z.string().required(),
|
||||
configSchema: z.array(z.object({
|
||||
key: z.string().required(), // 稳健标识(英文 slug,与字段值 JSON 的 key 对齐)
|
||||
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()).default([]), // 仅 type=enum:预设选项
|
||||
def: z.any(), // 默认值(text=string / number=number / boolean=boolean / enum=枚举项)
|
||||
unit: z.string().default(''), // 单位(可为空;如 % / 元 / 手 —— 展示时拼在值后)
|
||||
})).default([]),
|
||||
})).default(DEFAULT_STRATEGIES),
|
||||
configSchema: z.array(fieldSchema).default([]),
|
||||
})).default([]),
|
||||
// R-027:字段定义真相源(写路径唯一入口;strategies[].configSchema 仅兼容读取旧数据)
|
||||
strategyFields: z.dict(z.array(fieldSchema).default([])).default({}),
|
||||
// 统一 tab 列表(R-011:唯一顺序与显隐来源;内置 + 策略混排)
|
||||
// union 兼容存量:旧格式为布尔对象(迭代 02),新格式为有序数组;normalizeTabs 读取时归一化
|
||||
tabs: z.union([
|
||||
@@ -237,126 +271,115 @@ export function registerSettings(ctx, config) {
|
||||
return scope;
|
||||
}
|
||||
|
||||
/** 读取 settings 快照(容错:测试用桩 scope 无 get 时返回空对象) */
|
||||
function scopeValue(scope) {
|
||||
return (scope && typeof scope.get === 'function') ? (scope.get() ?? {}) : {};
|
||||
}
|
||||
|
||||
/** 字段类型白名单(含 R-026 计算类型) */
|
||||
const FIELD_TYPES = new Set(['text', 'number', 'boolean', 'enum', 'formula']);
|
||||
|
||||
/**
|
||||
* 读取策略定义表(R-011 收窄 + R-013 扩展:{id, name, configSchema})
|
||||
* 归一化:旧存量数据缺 configSchema(R-013 前)→ 补空数组(Q4 兼容)。
|
||||
* 字段定义归一化(R-013 + R-026):类型白名单、枚举数组、公式串、小数位夹取 0-4
|
||||
* @param {Array} fields configSchema 原始数组
|
||||
* @returns {Array} 归一化后的字段定义数组
|
||||
*/
|
||||
export function normalizeFields(fields) {
|
||||
return (Array.isArray(fields) ? fields : []).map((f) => {
|
||||
const type = FIELD_TYPES.has(f?.type) ? f.type : 'text';
|
||||
const out = {
|
||||
key: String(f?.key ?? ''),
|
||||
label: String(f?.label ?? ''),
|
||||
type,
|
||||
enum: Array.isArray(f?.enum) ? f.enum.map((x) => String(x)) : [],
|
||||
unit: String(f?.unit ?? ''),
|
||||
};
|
||||
if (type === 'formula') {
|
||||
out.formula = String(f?.formula ?? '').trim();
|
||||
out.resultKind = f?.resultKind === 'boolean' ? 'boolean' : 'number'; // R-028
|
||||
const n = Number(f?.decimals);
|
||||
out.decimals = Number.isFinite(n) ? Math.min(4, Math.max(0, Math.round(n))) : 2;
|
||||
} else if (f?.def !== undefined) {
|
||||
out.def = f.def;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取策略表(R-027 静态化:恒为内置两项;configSchema 取 strategyFields,缺失时兼容读取旧 strategies[])
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @returns {Array<{id: string, name: string, configSchema: Array}>}
|
||||
*/
|
||||
export function getStrategies(scope) {
|
||||
const value = scope?.get() ?? {};
|
||||
const list = Array.isArray(value.strategies) ? value.strategies : DEFAULT_STRATEGIES;
|
||||
return list.map((s) => ({ ...s, configSchema: Array.isArray(s.configSchema) ? s.configSchema : [] }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新策略(整表更新;R-013:configSchema 随整表携带)
|
||||
* 写入归一化:项缺 configSchema → 补 [](防客户端旧格式整表提交丢定义)。
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @param {Array} strategies 完整策略列表
|
||||
*/
|
||||
export async function updateStrategies(scope, strategies) {
|
||||
const next = (Array.isArray(strategies) ? strategies : []).map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
configSchema: Array.isArray(s.configSchema) ? s.configSchema : [],
|
||||
const value = scopeValue(scope);
|
||||
const legacy = new Map((Array.isArray(value.strategies) ? value.strategies : []).map((s) => [s.id, s.configSchema]));
|
||||
const store = (value.strategyFields && typeof value.strategyFields === 'object') ? value.strategyFields : {};
|
||||
return BUILTIN_STRATEGIES.map((s) => ({
|
||||
...s,
|
||||
configSchema: normalizeFields(
|
||||
Array.isArray(store[s.id]) ? store[s.id] : (Array.isArray(legacy.get(s.id)) ? legacy.get(s.id) : []),
|
||||
),
|
||||
}));
|
||||
await scope.update({ strategies: next });
|
||||
}
|
||||
|
||||
/** 读取某策略字段定义(未知名 → []) */
|
||||
export function getStrategyFields(scope, strategyId) {
|
||||
return getStrategies(scope).find((s) => s.id === strategyId)?.configSchema ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增策略(O3)
|
||||
* 写入某策略字段定义(R-027:单策略写入,唯一写路径;不触碰 strategies[] 与份额数据)
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @param {string} name 策略名称
|
||||
* @returns {Promise<object>} 新策略对象
|
||||
* @param {string} strategyId 内置策略 id
|
||||
* @param {Array} configSchema 完整字段定义数组
|
||||
* @returns {Promise<object>} 更新后的策略对象
|
||||
*/
|
||||
export async function addStrategy(scope, name) {
|
||||
const list = getStrategies(scope);
|
||||
const id = generateStrategyId(name, list.map((s) => s.id));
|
||||
const strategy = {
|
||||
id,
|
||||
name: String(name ?? '').trim(),
|
||||
configSchema: [], // R-013:新策略默认无自定义字段(定义在设置页配置)
|
||||
};
|
||||
await updateStrategies(scope, [...list, strategy]);
|
||||
// R-011:联动在 tabs 列表末尾追加策略 tab 条目(Q2 追加末尾)
|
||||
await appendStrategyTab(scope, strategy);
|
||||
return strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名策略(O3;分配数据挂 id 不挂名称,改名不影响)
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @param {string} id 策略 id
|
||||
* @param {string} name 新名称
|
||||
* @returns {Promise<boolean>} 是否成功
|
||||
*/
|
||||
export async function renameStrategy(scope, id, name) {
|
||||
const list = getStrategies(scope);
|
||||
const target = list.find((s) => s.id === id);
|
||||
if (!target) throw Object.assign(new Error(`策略不存在: ${id}`), { code: 'strategy-not-found' });
|
||||
const next = list.map((s) => (s.id === id ? { ...s, name: String(name ?? '').trim() || s.name } : s));
|
||||
await updateStrategies(scope, next);
|
||||
return true;
|
||||
export async function updateStrategyFields(scope, strategyId, configSchema) {
|
||||
assertBuiltinStrategy(strategyId);
|
||||
const cur = scopeValue(scope);
|
||||
const store = (cur.strategyFields && typeof cur.strategyFields === 'object') ? cur.strategyFields : {};
|
||||
await scope.update({ ...cur, strategyFields: { ...store, [strategyId]: normalizeFields(configSchema) } });
|
||||
return getStrategies(scope).find((s) => s.id === strategyId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除策略(O3;份额清理由上层编排 storage)
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @param {string} id 策略 id
|
||||
* @returns {Promise<boolean>} 是否成功
|
||||
*/
|
||||
export async function removeStrategy(scope, id) {
|
||||
const list = getStrategies(scope);
|
||||
const target = list.find((s) => s.id === id);
|
||||
if (!target) throw Object.assign(new Error(`策略不存在: ${id}`), { code: 'strategy-not-found' });
|
||||
await updateStrategies(scope, list.filter((s) => s.id !== id));
|
||||
// R-011:联动删除 tabs 列表中对应策略条目(D3)
|
||||
await removeStrategyTab(scope, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化 tabs 为有序数组(R-011 迁移:旧布尔对象 → 数组;幂等,不重复迁移)
|
||||
* 旧格式:tabs = { allPositions, tradeRecords, watchlist } + strategies 自带 visible/order
|
||||
* 新格式:tabs = [{ id, kind, refKey|refId, name, visible, order }]
|
||||
* 旧隐藏策略迁移后 visible=true(Q3,产品约束-009)。
|
||||
* 归一化 tabs(R-027 静态化):序列由常量派生(内置 tab + 内置策略 tab),
|
||||
* 存量 tabs 仅保留 visible/order 偏好;未知条目(自建策略 tab / 已退役 tab)丢弃,缺失条目补齐。
|
||||
* 旧布尔对象格式(迭代 02)继续兼容:仅取内置显隐。
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @returns {Array} 归一化后的 tabs 数组(按 order 排序)
|
||||
*/
|
||||
export function normalizeTabs(scope) {
|
||||
const value = scope?.get() ?? {};
|
||||
const raw = value.tabs;
|
||||
// 已归一化(数组)
|
||||
if (Array.isArray(raw)) return raw.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
const raw = scopeValue(scope).tabs;
|
||||
|
||||
// 旧布尔对象或缺失 → 生成内置条目(保留原显隐值)
|
||||
const legacy = (typeof raw === 'object' && raw !== null) ? raw : {};
|
||||
const tabs = BUILTIN_TABS.map((t, i) => ({
|
||||
id: t.id, kind: 'builtin', refKey: t.refKey, name: t.name,
|
||||
visible: legacy[t.refKey] !== false, order: i,
|
||||
}));
|
||||
// 旧布尔对象(迭代 02):映射到内置 tab 显隐
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
return DEFAULT_TABS.map((t) => (t.kind === 'builtin' && t.refKey in raw
|
||||
? { ...t, visible: raw[t.refKey] !== false }
|
||||
: t));
|
||||
}
|
||||
if (!Array.isArray(raw)) return DEFAULT_TABS.map((t) => ({ ...t }));
|
||||
|
||||
// 策略条目:按旧 order 接续追加;旧隐藏策略迁移后显示(Q3)
|
||||
const strategies = Array.isArray(value.strategies) ? value.strategies : DEFAULT_STRATEGIES;
|
||||
const strategyRows = strategies.map((s, i) => ({
|
||||
id: 'tab-strategy-' + s.id, kind: 'strategy', refId: s.id, name: s.name,
|
||||
visible: true, order: tabs.length + i,
|
||||
}));
|
||||
return [...tabs, ...strategyRows];
|
||||
const byId = new Map(raw.map((t) => [t.id, t]));
|
||||
return DEFAULT_TABS
|
||||
.map((t) => {
|
||||
const hit = byId.get(t.id);
|
||||
return hit
|
||||
? { ...t, visible: hit.visible !== false, order: typeof hit.order === 'number' ? hit.order : t.order }
|
||||
: { ...t };
|
||||
})
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取统一 tab 列表(R-011):归一化 + 策略行 name join(Q4 自动跟随改名)
|
||||
* 读取统一 tab 列表(R-011/R-027):归一化结果(策略行 name 恒为内置名,无改名场景)
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @returns {Array} tabs 数组(按 order 排序,策略行 name 已 join)
|
||||
* @returns {Array} tabs 数组(按 order 排序)
|
||||
*/
|
||||
export function getTabs(scope) {
|
||||
const tabs = normalizeTabs(scope);
|
||||
const nameMap = new Map(getStrategies(scope).map((s) => [s.id, s.name]));
|
||||
return tabs.map((t) => (t.kind === 'strategy'
|
||||
? { ...t, name: nameMap.get(t.refId) ?? t.name ?? t.refId }
|
||||
: t));
|
||||
return normalizeTabs(scope);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,35 +390,21 @@ export function getTabs(scope) {
|
||||
*/
|
||||
export async function updateTabs(scope, tabs) {
|
||||
const current = scope?.get() ?? {};
|
||||
const normalized = (Array.isArray(tabs) ? tabs : []).map((t, i) => ({
|
||||
id: t.id,
|
||||
kind: t.kind === 'strategy' ? 'strategy' : 'builtin',
|
||||
refKey: t.refKey,
|
||||
refId: t.refId,
|
||||
name: t.name,
|
||||
visible: t.visible !== false,
|
||||
order: typeof t.order === 'number' ? t.order : i,
|
||||
}));
|
||||
const known = new Set(DEFAULT_TABS.map((t) => t.id));
|
||||
const normalized = (Array.isArray(tabs) ? tabs : [])
|
||||
.filter((t) => known.has(t?.id)) // R-027:只接受内置条目(策略条目恒存在,不可增删)
|
||||
.map((t, i) => ({
|
||||
id: t.id,
|
||||
kind: t.kind === 'strategy' ? 'strategy' : 'builtin',
|
||||
refKey: t.refKey,
|
||||
refId: t.refId,
|
||||
name: t.name,
|
||||
visible: t.visible !== false,
|
||||
order: typeof t.order === 'number' ? t.order : i,
|
||||
}));
|
||||
await scope.update({ ...current, tabs: normalized });
|
||||
}
|
||||
|
||||
/** 追加策略 tab 条目到末尾(addStrategy 联动;Q2 追加末尾) */
|
||||
export async function appendStrategyTab(scope, strategy) {
|
||||
const tabs = getTabs(scope);
|
||||
const next = [...tabs, {
|
||||
id: 'tab-strategy-' + strategy.id, kind: 'strategy', refId: strategy.id,
|
||||
name: strategy.name, visible: true,
|
||||
order: tabs.reduce((max, t) => Math.max(max, t.order ?? 0), -1) + 1,
|
||||
}];
|
||||
await updateTabs(scope, next);
|
||||
}
|
||||
|
||||
/** 删除策略 tab 条目(removeStrategy 联动;D3) */
|
||||
export async function removeStrategyTab(scope, strategyId) {
|
||||
const tabs = getTabs(scope);
|
||||
await updateTabs(scope, tabs.filter((t) => !(t.kind === 'strategy' && t.refId === strategyId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取通用 tab 显隐配置(R-011 迁移期兼容:返回布尔对象,供旧调用点使用)
|
||||
* @deprecated 由 getTabs 取代
|
||||
@@ -437,15 +446,14 @@ export async function updateTabConfig(scope, tabs) {
|
||||
* @returns {Array<{key: string, label: string, fixed: boolean, visible: boolean, kind: 'base'|'field'}>}
|
||||
*/
|
||||
export function normalizeStrategyColumns(scope, strategyId) {
|
||||
const value = scope?.get() ?? {};
|
||||
const strategies = Array.isArray(value.strategies) ? value.strategies : [];
|
||||
const strategy = strategies.find((s) => s.id === strategyId);
|
||||
const configSchema = Array.isArray(strategy?.configSchema) ? strategy.configSchema : [];
|
||||
const value = scopeValue(scope);
|
||||
// R-027:字段定义真相源 = strategyFields(缺失时兼容读取旧 strategies[].configSchema)
|
||||
const configSchema = getStrategyFields(scope, strategyId);
|
||||
const raw = (value.strategyColumns && value.strategyColumns[strategyId]) || [];
|
||||
|
||||
// 1. 默认全列(基础列默认序 + 字段列 configSchema 序;显隐缺省跟随 defaultVisible,R-015 行情列默认隐藏)
|
||||
const baseCols = COLUMN_META.map((c) => ({ key: c.key, label: c.label, fixed: false, kind: 'base', defaultVisible: c.defaultVisible !== false }));
|
||||
const fieldCols = configSchema.map((f) => ({ key: f.key, label: f.label || f.key, fixed: false, kind: 'field' }));
|
||||
const fieldCols = configSchema.map((f) => ({ key: f.key, label: f.label || f.key, fixed: false, kind: 'field', type: f.type }));
|
||||
const defaults = [...baseCols, ...fieldCols];
|
||||
|
||||
// 2. 应用覆盖:visible + 顺序(未覆盖的列保持默认,追加在默认序后)
|
||||
@@ -485,7 +493,7 @@ export function getStrategyColumns(scope, strategyId) {
|
||||
* @param {Array} columns 完整列数组 [{key, visible}](含不可见列——保留位置,visible 控制显示)
|
||||
*/
|
||||
export async function updateStrategyColumns(scope, strategyId, columns) {
|
||||
const current = scope?.get() ?? {};
|
||||
const current = scopeValue(scope);
|
||||
const store = current.strategyColumns && typeof current.strategyColumns === 'object' ? current.strategyColumns : {};
|
||||
const next = { ...store, [strategyId]: (Array.isArray(columns) ? columns : []).map((c) => ({ key: c.key, visible: c.visible !== false })) };
|
||||
await scope.update({ ...current, strategyColumns: next });
|
||||
|
||||
Reference in New Issue
Block a user