2790b92378
- strategy-positions 附加 holding_id + avgPrice/lastTradePrice(无关联默认成本价) - trades/by-holding 端点(按 holding 查委托汇总) - StrategyTab 持仓行展开(懒加载 + 内嵌汇总表,含交易日/时间列) - 神之一手 tab 激活时隐藏 AI 对话输入框(纯 CSS :has() 方案)
163 lines
5.7 KiB
JavaScript
163 lines
5.7 KiB
JavaScript
/**
|
||
* 神之一手(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 fetchStrategies() {
|
||
try {
|
||
const res = await fetch('/odl/api/strategies', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({}),
|
||
});
|
||
const data = await res.json();
|
||
return data && data.ok ? data.value : [];
|
||
} catch (e) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function fetchTabConfig() {
|
||
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) {
|
||
return {
|
||
allPositions: data.value.allPositions !== false,
|
||
tradeRecords: data.value.tradeRecords !== false,
|
||
watchlist: data.value.watchlist !== false,
|
||
};
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
return { allPositions: true, tradeRecords: true, watchlist: true };
|
||
}
|
||
|
||
function strategyTabId(strategyId) {
|
||
return 'odl-strategy-' + strategyId;
|
||
}
|
||
|
||
const GENERAL_TABS = [
|
||
{ key: 'allPositions', id: 'odl-all-positions', label: '全部持仓', order: 10,
|
||
render: (withConnection) => withConnection((props) => createElement(AllPositionsTab, props)) },
|
||
{ key: 'tradeRecords', id: 'odl-trade-records', label: '交易记录', order: 11,
|
||
render: (withConnection) => withConnection((props) => createElement(TradeRecordsTab, props)) },
|
||
{ key: 'watchlist', id: 'odl-watchlist', label: '关注列表', order: 12,
|
||
render: (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 class;chat/其他 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 = [];
|
||
};
|
||
|
||
const registerGeneralTabs = (tabConfig) => {
|
||
for (const tab of GENERAL_TABS) {
|
||
if (tabConfig[tab.key] === false) continue;
|
||
const dispose = ctx.slots.register({
|
||
name: 'conversation.view',
|
||
id: tab.id,
|
||
order: tab.order,
|
||
label: () => tab.label,
|
||
}, tab.render(withConnection));
|
||
tabDisposers.push(dispose);
|
||
}
|
||
};
|
||
|
||
const registerStrategyTabs = (strategies) => {
|
||
const visible = strategies.filter((s) => s.visible !== false);
|
||
visible.forEach((s, idx) => {
|
||
const dispose = ctx.slots.register({
|
||
name: 'conversation.view',
|
||
id: strategyTabId(s.id),
|
||
order: 13 + idx,
|
||
label: () => s.name,
|
||
}, withConnection((props) => createElement(StrategyTab, { ...props, strategyId: s.id, strategyName: s.name })));
|
||
tabDisposers.push(dispose);
|
||
});
|
||
return visible.length;
|
||
};
|
||
|
||
const registerAllTabs = async () => {
|
||
const [strategies, tabConfig] = await Promise.all([fetchStrategies(), fetchTabConfig()]);
|
||
if (disposed) return;
|
||
disposeAllTabs();
|
||
registerGeneralTabs(tabConfig);
|
||
const n = registerStrategyTabs(strategies);
|
||
ctx.logger?.info?.('[one-divine-lot] 已注册通用 tabs + ' + 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');
|
||
} |