Files
one_divine_lot/src/client/index.js
T
kyugao 4ac8a29805 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
2026-09-02 14:00:56 +08:00

133 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 神之一手(one_divine_lot)客户端插件
*
* 注册:
* - conversation.view tabs:全部持仓 / 交易记录 / 关注列表(通用,按 tabConfig 显隐)+ 策略 tabs(按策略配置)
* - settings.section:神之一手设置菜单(通用设置 + 策略分组)
*
* 迭代 02
* - O2 策略 tab 动态化:策略配置驱动注册
* - 设置 tab 化扩展:三个通用 tab(全部持仓/交易记录/关注列表)由 tabConfig 控制显隐
* - 刷新页面 = 插件重载 = 按最新配置重新注册(无事件机制)
*/
import { createElement } from 'react';
import { AllPositionsTab } from './views/AllPositionsTab.jsx';
import { StrategyTab } from './views/StrategyTab.jsx';
import { PlaceholderTab } from './views/PlaceholderTab.jsx';
import { TradeRecordsTab } from './views/TradeRecordsTab.jsx';
import { SettingsSection } from './views/SettingsSection.jsx';
import { QmtConnectionChip } from './views/QmtConnectionChip.jsx';
import { ConnectionProvider } from './views/connection.jsx';
import { MarketDataProvider } from './market/MarketDataProvider.jsx';
export const inject = ['slots', 'connection'];
async function fetchTabs() {
try {
const res = await fetch('/odl/api/tabs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
const data = await res.json();
if (data && data.ok && Array.isArray(data.value)) return data.value;
} catch (e) { /* ignore */ }
return null;
}
function strategyTabId(strategyId) {
return 'odl-strategy-' + strategyId;
}
// R-011:内置 tab 渲染映射(refKey → render);order 由 settings.tabs 统一管理
const GENERAL_RENDER = {
allPositions: (withConnection) => withConnection((props) => createElement(AllPositionsTab, props)),
tradeRecords: (withConnection) => withConnection((props) => createElement(TradeRecordsTab, props)),
watchlist: (withConnection) => withConnection((props) => createElement(PlaceholderTab, { ...props, title: '关注列表' })),
};
export function apply(ctx) {
const connection = ctx.get('connection');
if (!connection) {
ctx.logger?.warn?.('[one-divine-lot] connection 服务不可用');
}
// 神之一手 tab 激活时隐藏会话输入框(composer)
// 借鉴 dsh-context 的纯 CSS 方案::has(.odl-root) 命中 → 隐藏 [data-composer-seat]
// 我们的 tab 根元素带 odl-root classchat/其他 tab 无此 class → 输入框正常显示
const ODL_HIDE_COMPOSER_CSS = `
[data-conversation-scroll]:has(.odl-root)>[data-composer-seat]{display:none}
`;
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="one-divine-lot/hide-composer"]')) {
const tag = document.createElement('style');
tag.dataset.pluginCss = 'one-divine-lot/hide-composer';
tag.textContent = ODL_HIDE_COMPOSER_CSS;
document.head.appendChild(tag);
}
const withConnection = (render) => (props) =>
createElement(ConnectionProvider, { connection },
createElement(MarketDataProvider, null, render(props)));
ctx.slots.register({
name: 'settings.section',
id: 'one-divine-lot',
order: 50,
label: () => '神之一手',
}, withConnection((props) => createElement(SettingsSection, props)));
let tabDisposers = [];
let disposed = false;
const disposeAllTabs = () => {
for (const d of tabDisposers) {
try { d(); } catch (e) { /* ignore */ }
}
tabDisposers = [];
};
// R-011:统一注册 —— 读 settings.tabs 有序数组,按 order 排序、过滤 visible
// builtin 走内置 renderGENERAL_RENDER),strategy 走 StrategyTab
const registerAllTabs = async () => {
const tabs = await fetchTabs();
if (disposed) return;
disposeAllTabs();
if (!tabs) return;
const sorted = tabs.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
let n = 0;
for (const t of sorted) {
if (t.visible === false) continue;
const render = t.kind === 'builtin'
? GENERAL_RENDER[t.refKey]
: (withConnection) => withConnection((props) => createElement(StrategyTab, { ...props, strategyId: t.refId, strategyName: t.name }));
if (!render) continue;
const dispose = ctx.slots.register({
name: 'conversation.view',
id: t.kind === 'builtin' ? t.id : strategyTabId(t.refId),
order: t.order ?? 0,
label: () => t.name ?? t.id,
}, render(withConnection));
tabDisposers.push(dispose);
n++;
}
ctx.logger?.info?.('[one-divine-lot] 已注册 ' + n + ' 个会话 tab');
};
registerAllTabs();
// R-004 Q9:会话头部快捷切换 chip(与 DSH 内置 PTC 标签 order=-10 并排,技术约束-009
ctx.slots.register({
name: 'conversation.session.header.actions',
id: 'odl-qmt-switch',
order: -9,
}, withConnection((props) => createElement(QmtConnectionChip, props)));
ctx.effect(() => {
return () => {
disposed = true;
disposeAllTabs();
};
}, 'one-divine-lot.client');
}