feat(Tab设置+UI主题): 迭代09 Tab设置统一管理 + 迭代10 UI适配DSH主题
迭代09 (R-011): Tab 设置统一管理所有会话 tab
- settings.tabs 布尔对象 → 统一有序数组 (kind: builtin|strategy)
- 旧配置 schema union 兼容 + 读取归一化无感迁移
- strategies 收窄为 {id,name},策略行 name join 自动跟随改名
- 策略增删联动 tabs 条目 (add 追加末尾 / remove 联动删除)
- 客户端统一注册读 tabs 数组,内置与策略可任意混排
- 设置页 Tab 设置子 tab:混排表格 + 拖动手柄 + 显隐开关 + 落点立即持久化
- 策略分组瘦身:仅 新增/重命名/删除 + 指引提示
- 回归测试: test-r011-tabs (23) + test-r011-api (12)
迭代10 (R-012): UI 适配 DSH 浅色/深色/跟随系统主题
- 10 个视图文件 141 处硬编码色 → var(--dsw-alias-*, fallback)
- 语义映射: bg-layer/label/border/state/button-contrast 等
- 淡底 color-mix 自适应;遮罩 bg-mask-1;阴影 shadow-lv3
This commit is contained in:
+144
-95
@@ -5,38 +5,63 @@
|
||||
* 通过 ctx.settings.register('one-divine-lot', schema) 注册策略配置 namespace。
|
||||
*
|
||||
* 数据结构:
|
||||
* strategies: [{ id, name, visible, order }]
|
||||
* strategies: [{ id, name }](R-011 收窄:visible/order 归 tabs 统一管理)
|
||||
* tabs: [{ id, kind: builtin|strategy, refKey|refId, name, visible, order }]
|
||||
* 预置:网格超市、手动做T(R-003 O3-5:可删除,彻底自由)
|
||||
*
|
||||
* 迭代 02 新增(R-003 O3 策略 CRUD):
|
||||
* - generateStrategyId(name):中文/任意名 → 英文 slug id
|
||||
* - addStrategy / renameStrategy / moveStrategy / removeStrategy
|
||||
* R-011 迭代 09(产品约束-009、技术约束-014):
|
||||
* - settings.tabs 从布尔对象升级为统一有序数组(内置 + 策略混排)
|
||||
* - 旧格式读取时静默归一化迁移(旧隐藏策略迁移后显示,Q3)
|
||||
* - addStrategy 联动追加 tab(末尾,Q2);removeStrategy 联动删除对应 tab(D3)
|
||||
* - 策略行 name 由 getTabs join strategies(改名自动跟随,Q4)
|
||||
* - removeStrategy 的份额清理由上层(api.js 编排 storage)完成
|
||||
*/
|
||||
|
||||
import z from '@deepseek-ai/schemastery';
|
||||
|
||||
/** 内置 tab 元数据(常量,顺序即默认顺序;R-011) */
|
||||
export const BUILTIN_TABS = [
|
||||
{ id: 'tab-all-positions', refKey: 'allPositions', name: '全部持仓' },
|
||||
{ id: 'tab-trade-records', refKey: 'tradeRecords', name: '交易记录' },
|
||||
{ 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,
|
||||
}));
|
||||
|
||||
/** 默认策略(首次注册时的初始值) */
|
||||
export const DEFAULT_STRATEGIES = [
|
||||
{ id: 'grid-supermarket', name: '网格超市' },
|
||||
{ id: 'manual-t', name: '手动做T' },
|
||||
];
|
||||
|
||||
/** 策略设置 schema(schemastery 定义,settings.register 要求 schema 对象) */
|
||||
export const strategySchema = z.object({
|
||||
// 策略定义表(R-011 收窄:仅 id + name,visible/order 归 tabs 统一管理)
|
||||
strategies: z.array(z.object({
|
||||
id: z.string().required(),
|
||||
name: z.string().required(),
|
||||
visible: z.boolean().default(true),
|
||||
order: z.number().default(0),
|
||||
})).default([
|
||||
{ id: 'grid-supermarket', name: '网格超市', visible: true, order: 0 },
|
||||
{ id: 'manual-t', name: '手动做T', visible: true, order: 1 },
|
||||
]),
|
||||
// 通用 tab 显隐配置(设置 tab 化扩展,2026-08-28)
|
||||
tabs: z.object({
|
||||
allPositions: z.boolean().default(true), // 全部持仓
|
||||
tradeRecords: z.boolean().default(true), // 交易记录(未来 tab,本期空占位)
|
||||
watchlist: z.boolean().default(true), // 关注列表(未来 tab,本期空占位)
|
||||
}).default({
|
||||
allPositions: true,
|
||||
tradeRecords: true,
|
||||
watchlist: true,
|
||||
}),
|
||||
})).default(DEFAULT_STRATEGIES),
|
||||
// 统一 tab 列表(R-011:唯一顺序与显隐来源;内置 + 策略混排)
|
||||
// union 兼容存量:旧格式为布尔对象(迭代 02),新格式为有序数组;normalizeTabs 读取时归一化
|
||||
tabs: z.union([
|
||||
z.object({
|
||||
allPositions: z.boolean().default(true),
|
||||
tradeRecords: z.boolean().default(true),
|
||||
watchlist: z.boolean().default(true),
|
||||
}),
|
||||
z.array(z.object({
|
||||
id: z.string().required(),
|
||||
kind: z.union([z.const('builtin'), z.const('strategy')]).required(),
|
||||
refKey: z.string(), // builtin 专用:allPositions/tradeRecords/watchlist(可空)
|
||||
refId: z.string(), // strategy 专用:策略 id(可空)
|
||||
name: z.string(), // 展示名(策略行 join 时刷新,Q4 自动跟随改名;可空)
|
||||
visible: z.boolean().default(true),
|
||||
order: z.number().default(0),
|
||||
})),
|
||||
]).default(DEFAULT_TABS),
|
||||
// QMT 连接配置(R-004,技术约束-008):list + 激活态 + 默认态
|
||||
qmtConnections: z.object({
|
||||
list: z.array(z.object({
|
||||
@@ -50,18 +75,6 @@ export const strategySchema = z.object({
|
||||
}).default({ list: [], activeId: '', defaultId: '' }),
|
||||
});
|
||||
|
||||
/** 默认通用 tab 配置 */
|
||||
export const DEFAULT_TABS = {
|
||||
allPositions: true,
|
||||
tradeRecords: true,
|
||||
watchlist: true,
|
||||
};
|
||||
|
||||
/** 默认策略(首次注册时的初始值) */
|
||||
export const DEFAULT_STRATEGIES = [
|
||||
{ id: 'grid-supermarket', name: '网格超市', visible: true, order: 0 },
|
||||
{ id: 'manual-t', name: '手动做T', visible: true, order: 1 },
|
||||
];
|
||||
|
||||
/** 简易拼音映射表(常用汉字 → 拼音,用于 slug 生成兜底) */
|
||||
const PINYIN_MAP = {
|
||||
@@ -182,23 +195,12 @@ export function registerSettings(ctx, config) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取策略列表(含 visible 过滤)
|
||||
* 读取策略定义表(R-011 收窄:仅 id + name,无 visible/order)
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
*/
|
||||
export function getStrategies(scope) {
|
||||
const value = scope?.get() ?? {};
|
||||
const list = Array.isArray(value.strategies) ? value.strategies : DEFAULT_STRATEGIES;
|
||||
// 统一按 order 排序(设置页 / tab 栏 / 逻辑层共用同一顺序)
|
||||
return list.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可见策略列表(visible === true,按 order 排序)
|
||||
*/
|
||||
export function getVisibleStrategies(scope) {
|
||||
return getStrategies(scope)
|
||||
.filter((s) => s.visible !== false)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
return Array.isArray(value.strategies) ? value.strategies : DEFAULT_STRATEGIES;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,10 +224,10 @@ export async function addStrategy(scope, name) {
|
||||
const strategy = {
|
||||
id,
|
||||
name: String(name ?? '').trim(),
|
||||
visible: true,
|
||||
order: list.reduce((max, s) => Math.max(max, s.order ?? 0), -1) + 1,
|
||||
};
|
||||
await updateStrategies(scope, [...list, strategy]);
|
||||
// R-011:联动在 tabs 列表末尾追加策略 tab 条目(Q2 追加末尾)
|
||||
await appendStrategyTab(scope, strategy);
|
||||
return strategy;
|
||||
}
|
||||
|
||||
@@ -245,33 +247,6 @@ export async function renameStrategy(scope, id, name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动策略(排序,上移/下移)
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @param {string} id 策略 id
|
||||
* @param {'up'|'down'} dir 方向
|
||||
* @returns {Promise<boolean>} 是否成功
|
||||
*/
|
||||
export async function moveStrategy(scope, id, dir) {
|
||||
const list = getStrategies(scope)
|
||||
.slice()
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
const idx = list.findIndex((s) => s.id === id);
|
||||
if (idx < 0) throw Object.assign(new Error(`策略不存在: ${id}`), { code: 'strategy-not-found' });
|
||||
const swapIdx = dir === 'up' ? idx - 1 : idx + 1;
|
||||
if (swapIdx < 0 || swapIdx >= list.length) return false; // 已在边界
|
||||
const a = list[idx];
|
||||
const b = list[swapIdx];
|
||||
const orderA = a.order ?? 0;
|
||||
const orderB = b.order ?? 0;
|
||||
const next = list.map((s) => {
|
||||
if (s.id === a.id) return { ...s, order: orderB };
|
||||
if (s.id === b.id) return { ...s, order: orderA };
|
||||
return s;
|
||||
});
|
||||
await updateStrategies(scope, next);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除策略(O3;份额清理由上层编排 storage)
|
||||
@@ -284,40 +259,114 @@ export async function removeStrategy(scope, id) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取通用 tab 显隐配置(设置 tab 化扩展)
|
||||
* 归一化 tabs 为有序数组(R-011 迁移:旧布尔对象 → 数组;幂等,不重复迁移)
|
||||
* 旧格式:tabs = { allPositions, tradeRecords, watchlist } + strategies 自带 visible/order
|
||||
* 新格式:tabs = [{ id, kind, refKey|refId, name, visible, order }]
|
||||
* 旧隐藏策略迁移后 visible=true(Q3,产品约束-009)。
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @returns {{ allPositions: boolean, tradeRecords: boolean, watchlist: boolean }}
|
||||
* @returns {Array} 归一化后的 tabs 数组(按 order 排序)
|
||||
*/
|
||||
export function getTabConfig(scope) {
|
||||
export function normalizeTabs(scope) {
|
||||
const value = scope?.get() ?? {};
|
||||
const tabs = value.tabs ?? {};
|
||||
return {
|
||||
allPositions: tabs.allPositions !== false,
|
||||
tradeRecords: tabs.tradeRecords !== false,
|
||||
watchlist: tabs.watchlist !== false,
|
||||
};
|
||||
const raw = value.tabs;
|
||||
// 已归一化(数组)
|
||||
if (Array.isArray(raw)) return raw.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
|
||||
// 旧布尔对象或缺失 → 生成内置条目(保留原显隐值)
|
||||
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,
|
||||
}));
|
||||
|
||||
// 策略条目:按旧 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];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新通用 tab 显隐配置(整表更新)
|
||||
* 读取统一 tab 列表(R-011):归一化 + 策略行 name join(Q4 自动跟随改名)
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @param {object} tabs { allPositions?, tradeRecords?, watchlist? }
|
||||
* @returns {Array} tabs 数组(按 order 排序,策略行 name 已 join)
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统一 tab 列表(整表更新:顺序 + 显隐;R-011)
|
||||
* 策略行 name 由服务端 join 刷新,客户端可只传 id/order/visible 的数组。
|
||||
* @param {ReturnType<typeof registerSettings>} scope
|
||||
* @param {Array} tabs 完整 tabs 数组
|
||||
*/
|
||||
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,
|
||||
}));
|
||||
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 取代
|
||||
*/
|
||||
export function getTabConfig(scope) {
|
||||
const tabs = getTabs(scope);
|
||||
const cfg = { allPositions: true, tradeRecords: true, watchlist: true };
|
||||
for (const t of tabs) {
|
||||
if (t.kind === 'builtin' && t.refKey in cfg) cfg[t.refKey] = t.visible !== false;
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新通用 tab 显隐配置(R-011 迁移期兼容:写回统一数组)
|
||||
* @deprecated 由 updateTabs 取代
|
||||
*/
|
||||
export async function updateTabConfig(scope, tabs) {
|
||||
const current = scope?.get() ?? {};
|
||||
const next = {
|
||||
...current,
|
||||
tabs: {
|
||||
allPositions: tabs.allPositions !== false,
|
||||
tradeRecords: tabs.tradeRecords !== false,
|
||||
watchlist: tabs.watchlist !== false,
|
||||
},
|
||||
};
|
||||
await scope.update(next);
|
||||
const currentTabs = getTabs(scope);
|
||||
const next = currentTabs.map((t) => (t.kind === 'builtin' && t.refKey in tabs
|
||||
? { ...t, visible: tabs[t.refKey] !== false }
|
||||
: t));
|
||||
await updateTabs(scope, next);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user