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:
2026-09-02 14:00:56 +08:00
parent 35f2ad1a23
commit 4ac8a29805
15 changed files with 629 additions and 385 deletions
+144 -95
View File
@@ -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 联动删除对应 tabD3
* - 策略行 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' },
];
/** 策略设置 schemaschemastery 定义,settings.register 要求 schema 对象) */
export const strategySchema = z.object({
// 策略定义表(R-011 收窄:仅 id + namevisible/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=trueQ3,产品约束-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);
}
/**