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:
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* R-011 回归测试:tabs 统一管理数据层
|
||||
* 独立内存 mock scope(settings 读写),验证:
|
||||
* 1. 归一化迁移:旧布尔对象 tabs → 有序数组(内置保留原显隐,策略接续追加)
|
||||
* 2. 迁移:旧隐藏策略 → visible=true(Q3)
|
||||
* 3. getTabs:策略行 name join strategies(Q4 自动跟随改名)
|
||||
* 4. updateTabs:整表更新(顺序 + 显隐)
|
||||
* 5. addStrategy:联动追加 tab 条目(末尾,Q2)
|
||||
* 6. removeStrategy:联动删除对应 tab 条目(D3)
|
||||
* 纯内存,不触真实数据(技术约束-011)
|
||||
*/
|
||||
import {
|
||||
getTabs, updateTabs, normalizeTabs,
|
||||
addStrategy, removeStrategy, renameStrategy, getStrategies,
|
||||
} from '../src/settings.js';
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
function assert(cond, msg) {
|
||||
if (cond) { pass++; console.log(" ✅ " + msg); }
|
||||
else { fail++; console.log(" ❌ " + msg); }
|
||||
}
|
||||
|
||||
/** 内存 mock scope:get/update 读改写对象 */
|
||||
function makeScope(initial) {
|
||||
const store = structuredClone(initial ?? {});
|
||||
return {
|
||||
get: () => structuredClone(store),
|
||||
update: async (patch) => { Object.assign(store, structuredClone(patch)); },
|
||||
_store: store,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- 1. 旧格式归一化(布尔对象 tabs + 策略带 visible/order) ----------
|
||||
console.log("\n[1] 旧格式归一化迁移");
|
||||
const oldScope = makeScope({
|
||||
strategies: [
|
||||
{ id: 'grid-supermarket', name: '网格超市', visible: true, order: 5 },
|
||||
{ id: 'manual-t', name: '手动做T', visible: false, order: 9 },
|
||||
],
|
||||
tabs: { allPositions: true, tradeRecords: false, watchlist: true },
|
||||
});
|
||||
const oldTabs = normalizeTabs(oldScope);
|
||||
assert(oldTabs.length === 5, "归一化后共 5 个条目(3 内置 + 2 策略)");
|
||||
assert(oldTabs[0].kind === 'builtin' && oldTabs[0].refKey === 'allPositions', '内置 allPositions 在前');
|
||||
assert(oldTabs[1].refKey === 'tradeRecords' && oldTabs[1].visible === false, '内置 tradeRecords 保留旧显隐=false');
|
||||
assert(oldTabs[2].refKey === 'watchlist' && oldTabs[2].visible === true, '内置 watchlist 保留旧显隐=true');
|
||||
const manualTab = oldTabs.find((t) => t.kind === 'strategy' && t.refId === 'manual-t');
|
||||
assert(manualTab && manualTab.visible === true, "旧隐藏策略 manual-t 迁移后 visible=true(Q3)");
|
||||
assert(manualTab && manualTab.name === '手动做T', '策略行 name 来自 strategies');
|
||||
assert(oldTabs[3].order === 3 && oldTabs[4].order === 4, '策略按 order 接续(order 连续)');
|
||||
|
||||
// ---------- 2. 已归一化数组直接返回 ----------
|
||||
console.log("\n[2] 已归一化数组直接返回");
|
||||
const arrScope = makeScope({ tabs: [
|
||||
{ id: 'tab-a', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 1 },
|
||||
{ id: 'tab-b', kind: 'builtin', refKey: 'watchlist', name: '关注列表', visible: true, order: 0 },
|
||||
] });
|
||||
const arrTabs = normalizeTabs(arrScope);
|
||||
assert(arrTabs[0].id === 'tab-b' && arrTabs[1].id === 'tab-a', '数组按 order 排序');
|
||||
|
||||
// ---------- 3. 策略行 name join(改名自动跟随,Q4) ----------
|
||||
console.log("\n[3] getTabs 策略名 join");
|
||||
const joinScope = makeScope({
|
||||
strategies: [{ id: 'manual-t', name: '手动做T新名' }],
|
||||
tabs: [
|
||||
{ id: 'tab-all', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 0 },
|
||||
{ id: 'tab-s', kind: 'strategy', refId: 'manual-t', name: '旧名', visible: true, order: 1 },
|
||||
],
|
||||
});
|
||||
const joined = getTabs(joinScope);
|
||||
assert(joined.find((t) => t.id === 'tab-s').name === '手动做T新名', '策略改名后 tabs 行 name 自动跟随(Q4)');
|
||||
|
||||
// ---------- 4. updateTabs 整表更新 ----------
|
||||
console.log("\n[4] updateTabs 整表更新");
|
||||
const updScope = makeScope({ tabs: [
|
||||
{ id: 'tab-a', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 0 },
|
||||
{ id: 'tab-b', kind: 'builtin', refKey: 'watchlist', name: '关注列表', visible: true, order: 1 },
|
||||
] });
|
||||
await updateTabs(updScope, [
|
||||
{ id: 'tab-b', kind: 'builtin', refKey: 'watchlist', name: '关注列表', visible: false, order: 0 },
|
||||
{ id: 'tab-a', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 1 },
|
||||
]);
|
||||
const updTabs = getTabs(updScope);
|
||||
assert(updTabs[0].id === 'tab-b' && updTabs[0].visible === false, '整表更新:顺序与显隐生效');
|
||||
assert(updTabs.length === 2, '整表更新后条目数不变');
|
||||
|
||||
// ---------- 5. addStrategy 联动追加 tab(末尾,Q2) ----------
|
||||
console.log("\n[5] addStrategy 联动追加 tab");
|
||||
const addScope = makeScope({
|
||||
strategies: [{ id: 'grid-supermarket', name: '网格超市' }],
|
||||
tabs: [
|
||||
{ id: 'tab-all', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 0 },
|
||||
{ id: 'tab-s', kind: 'strategy', refId: 'grid-supermarket', name: '网格超市', visible: true, order: 1 },
|
||||
],
|
||||
});
|
||||
const added = await addStrategy(addScope, '长线持有');
|
||||
assert(getStrategies(addScope).length === 2, '策略定义表新增');
|
||||
const addTabs = getTabs(addScope);
|
||||
assert(addTabs.length === 3, 'tabs 联动新增一条');
|
||||
assert(addTabs[2].kind === 'strategy' && addTabs[2].refId === added.id, '新策略 tab 追加到末尾(Q2)');
|
||||
assert(addTabs[2].name === '长线持有', '新策略 tab name 正确');
|
||||
assert(addTabs[2].visible === true, '新策略 tab 默认显示');
|
||||
assert(addTabs[2].order === 2, 'order 连续');
|
||||
|
||||
// ---------- 6. removeStrategy 联动删除 tab(D3) ----------
|
||||
console.log("\n[6] removeStrategy 联动删除 tab");
|
||||
const rmScope = makeScope({
|
||||
strategies: [
|
||||
{ id: 'grid-supermarket', name: '网格超市' },
|
||||
{ id: 'manual-t', name: '手动做T' },
|
||||
],
|
||||
tabs: [
|
||||
{ id: 'tab-all', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 0 },
|
||||
{ id: 'tab-g', kind: 'strategy', refId: 'grid-supermarket', name: '网格超市', visible: true, order: 1 },
|
||||
{ id: 'tab-m', kind: 'strategy', refId: 'manual-t', name: '手动做T', visible: true, order: 2 },
|
||||
],
|
||||
});
|
||||
await removeStrategy(rmScope, 'grid-supermarket');
|
||||
const rmTabs = getTabs(rmScope);
|
||||
assert(rmTabs.length === 2, '删除策略后 tabs 联动删除对应条目');
|
||||
assert(!rmTabs.some((t) => t.refId === 'grid-supermarket'), 'grid-supermarket tab 条目已删除');
|
||||
assert(rmTabs.some((t) => t.refId === 'manual-t'), 'manual-t tab 条目保留');
|
||||
assert(getStrategies(rmScope).length === 1, '策略定义表删除');
|
||||
|
||||
// ---------- 7. renameStrategy 只改名(tabs 行 join 跟随,不影响定义) ----------
|
||||
console.log("\n[7] renameStrategy");
|
||||
const rnScope = makeScope({
|
||||
strategies: [{ id: 'manual-t', name: '手动做T' }],
|
||||
tabs: [
|
||||
{ id: 'tab-m', kind: 'strategy', refId: 'manual-t', name: '手动做T', visible: true, order: 0 },
|
||||
],
|
||||
});
|
||||
await renameStrategy(rnScope, 'manual-t', '手动T+');
|
||||
assert(getStrategies(rnScope)[0].name === '手动T+', '策略定义名更新');
|
||||
assert(getTabs(rnScope)[0].name === '手动T+', 'tabs 行 name join 跟随(Q4)');
|
||||
|
||||
// ---------- 汇总 ----------
|
||||
console.log("\n结果: " + pass + " 通过 / " + fail + " 失败");
|
||||
if (fail > 0) process.exit(1);
|
||||
Reference in New Issue
Block a user