Files
one_divine_lot/src/client/index.js
T
kyugao 2790b92378 feat(策略持仓): 持仓行展开关联交易 + 成本价/最后成交价 + 隐藏输入框(迭代 08,R-010)
- strategy-positions 附加 holding_id + avgPrice/lastTradePrice(无关联默认成本价)
- trades/by-holding 端点(按 holding 查委托汇总)
- StrategyTab 持仓行展开(懒加载 + 内嵌汇总表,含交易日/时间列)
- 神之一手 tab 激活时隐藏 AI 对话输入框(纯 CSS :has() 方案)
2026-09-02 01:20:58 +08:00

163 lines
5.7 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 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 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 = [];
};
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');
}