feat(策略持仓): 持仓行展开关联交易 + 成本价/最后成交价 + 隐藏输入框(迭代 08,R-010)
- strategy-positions 附加 holding_id + avgPrice/lastTradePrice(无关联默认成本价) - trades/by-holding 端点(按 holding 查委托汇总) - StrategyTab 持仓行展开(懒加载 + 内嵌汇总表,含交易日/时间列) - 神之一手 tab 激活时隐藏 AI 对话输入框(纯 CSS :has() 方案)
This commit is contained in:
@@ -75,6 +75,19 @@ export function apply(ctx) {
|
|||||||
ctx.logger?.warn?.('[one-divine-lot] 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) =>
|
const withConnection = (render) => (props) =>
|
||||||
createElement(ConnectionProvider, { connection },
|
createElement(ConnectionProvider, { connection },
|
||||||
createElement(MarketDataProvider, null, render(props)));
|
createElement(MarketDataProvider, null, render(props)));
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function AllPositionsTab(props) {
|
|||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 16, fontFamily: 'system-ui' }}>
|
<div className="odl-root" style={{ padding: 16, fontFamily: 'system-ui' }}>
|
||||||
<h3 style={{ margin: '0 0 12px' }}>全部持仓({positions ? positions.positions.length : 0} 只)</h3>
|
<h3 style={{ margin: '0 0 12px' }}>全部持仓({positions ? positions.positions.length : 0} 只)</h3>
|
||||||
<LoadState loading={loading} error={error} onRetry={load} autoRetry={2}>
|
<LoadState loading={loading} error={error} onRetry={load} autoRetry={2}>
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
export function PlaceholderTab({ title }) {
|
export function PlaceholderTab({ title }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 40, textAlign: 'center', fontFamily: 'system-ui', color: '#999' }}>
|
<div className="odl-root" style={{ padding: 40, textAlign: 'center', fontFamily: 'system-ui', color: '#999' }}>
|
||||||
<div style={{ fontSize: 16, marginBottom: 8 }}>{title}</div>
|
<div style={{ fontSize: 16, marginBottom: 8 }}>{title}</div>
|
||||||
<div style={{ fontSize: 13 }}>功能开发中,敬请期待</div>
|
<div style={{ fontSize: 13 }}>功能开发中,敬请期待</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,13 +15,19 @@
|
|||||||
* - 窄屏(<480px):每元素一行,两端对齐
|
* - 窄屏(<480px):每元素一行,两端对齐
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useRpc } from './connection.jsx';
|
import { useRpc } from './connection.jsx';
|
||||||
import { LoadState } from './LoadState.jsx';
|
import { LoadState } from './LoadState.jsx';
|
||||||
import { ToastProvider, useToast } from './Toast.jsx';
|
import { ToastProvider, useToast } from './Toast.jsx';
|
||||||
import { useMarket } from '../market/MarketDataProvider.jsx';
|
import { useMarket } from '../market/MarketDataProvider.jsx';
|
||||||
import { PriceCell } from './PriceCell.jsx';
|
import { PriceCell } from './PriceCell.jsx';
|
||||||
|
|
||||||
|
/** 价格格式化(2 位小数) */
|
||||||
|
function fmtPrice(v) {
|
||||||
|
if (v == null || !isFinite(v)) return '—';
|
||||||
|
return Number(v).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
/** 添加持仓区域响应式样式(内联注入,含媒体查询) */
|
/** 添加持仓区域响应式样式(内联注入,含媒体查询) */
|
||||||
const ADD_FORM_CSS = `
|
const ADD_FORM_CSS = `
|
||||||
.odl-add-form {
|
.odl-add-form {
|
||||||
@@ -110,6 +116,10 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
|||||||
const [selectedCode, setSelectedCode] = useState('');
|
const [selectedCode, setSelectedCode] = useState('');
|
||||||
const [addShares, setAddShares] = useState('');
|
const [addShares, setAddShares] = useState('');
|
||||||
const [removeTarget, setRemoveTarget] = useState(null); // { code, shares }
|
const [removeTarget, setRemoveTarget] = useState(null); // { code, shares }
|
||||||
|
// R-010:持仓行展开看关联交易(Holding → 委托汇总)
|
||||||
|
const [expandedCode, setExpandedCode] = useState(null); // 当前展开的 code
|
||||||
|
const [holdingTrades, setHoldingTrades] = useState(null); // { code: [委托汇总] }
|
||||||
|
const [holdingTradesLoading, setHoldingTradesLoading] = useState(null); // 展开加载中的 code
|
||||||
const call = useRpc();
|
const call = useRpc();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { getPrice, registerCodes } = useMarket();
|
const { getPrice, registerCodes } = useMarket();
|
||||||
@@ -187,10 +197,39 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** R-010:展开某持仓行 → 懒加载该 holding 的委托汇总 */
|
||||||
|
const toggleExpand = async (code, holdingId) => {
|
||||||
|
if (expandedCode === code) {
|
||||||
|
// 折叠
|
||||||
|
setExpandedCode(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExpandedCode(code);
|
||||||
|
if (holdingId == null) {
|
||||||
|
setHoldingTrades((prev) => ({ ...(prev ?? {}), [code]: [] }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 懒加载:有缓存直接用,否则请求
|
||||||
|
if (holdingTrades && holdingTrades[code]) return;
|
||||||
|
setHoldingTradesLoading(code);
|
||||||
|
try {
|
||||||
|
const res = await call('one-divine-lot/trades/by-holding', { args: { holdingId } });
|
||||||
|
if (res?.ok && Array.isArray(res.value)) {
|
||||||
|
setHoldingTrades((prev) => ({ ...(prev ?? {}), [code]: res.value }));
|
||||||
|
} else {
|
||||||
|
setHoldingTrades((prev) => ({ ...(prev ?? {}), [code]: [] }));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setHoldingTrades((prev) => ({ ...(prev ?? {}), [code]: [] }));
|
||||||
|
} finally {
|
||||||
|
setHoldingTradesLoading(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const name = strategyName || (strategyId === 'grid-supermarket' ? '网格策略持仓' : strategyId === 'manual-t' ? '做T持仓' : strategyId);
|
const name = strategyName || (strategyId === 'grid-supermarket' ? '网格策略持仓' : strategyId === 'manual-t' ? '做T持仓' : strategyId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 16, fontFamily: 'system-ui' }}>
|
<div className="odl-root" style={{ padding: 16, fontFamily: 'system-ui' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 8 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 8 }}>
|
||||||
<h3 style={{ margin: 0 }}>{name}({(positions ?? []).length} 只)</h3>
|
<h3 style={{ margin: 0 }}>{name}({(positions ?? []).length} 只)</h3>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
@@ -244,60 +283,122 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
|||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ textAlign: 'left', borderBottom: '2px solid #ddd' }}>
|
<tr style={{ textAlign: 'left', borderBottom: '2px solid #ddd' }}>
|
||||||
|
<th style={{ padding: '8px 6px', lineHeight: '24px', width: 28 }}> </th>
|
||||||
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>代码</th>
|
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>代码</th>
|
||||||
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>名称</th>
|
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>名称</th>
|
||||||
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>现价</th>
|
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>现价</th>
|
||||||
|
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>成本价</th>
|
||||||
|
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>最后一笔成交价</th>
|
||||||
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>策略份额</th>
|
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>策略份额</th>
|
||||||
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>总持仓</th>
|
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>总持仓</th>
|
||||||
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>操作</th>
|
<th style={{ padding: '8px 6px', lineHeight: '24px' }}>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{(positions ?? []).map((p) => (
|
{(positions ?? []).map((p) => {
|
||||||
<tr key={p.code} style={{ borderBottom: '1px solid #eee' }}>
|
const isExpanded = expandedCode === p.code;
|
||||||
<td style={{ padding: '8px 6px' }}>{p.code}</td>
|
const trades = (holdingTrades ?? {})[p.code];
|
||||||
<td style={{ padding: '8px 6px' }}>{p.name}</td>
|
const isLoading = holdingTradesLoading === p.code;
|
||||||
<PriceCell price={getPrice(p.code)?.lastPrice} lastClose={getPrice(p.code)?.lastClose} />
|
const hasHolding = p.holdingId != null;
|
||||||
<td style={{ padding: '8px 6px', fontWeight: 'bold' }}>{p.shares}</td>
|
return (
|
||||||
<td style={{ padding: '8px 6px' }}>{p.volume}</td>
|
<React.Fragment key={p.code}>
|
||||||
<td style={{ padding: '8px 6px', whiteSpace: 'nowrap' }}>
|
<tr
|
||||||
{removeTarget && removeTarget.code === p.code ? (
|
onClick={() => toggleExpand(p.code, p.holdingId)}
|
||||||
<span>
|
style={{
|
||||||
<input
|
borderBottom: '1px solid #eee',
|
||||||
type="number"
|
cursor: hasHolding ? 'pointer' : 'default',
|
||||||
min="1"
|
background: isExpanded ? '#f5f5f5' : 'transparent',
|
||||||
step={100}
|
}}
|
||||||
value={removeTarget.shares}
|
>
|
||||||
onChange={(e) => setRemoveTarget({ code: p.code, shares: e.target.value })}
|
<td style={{ padding: '8px 6px', textAlign: 'center', width: 28 }}>
|
||||||
style={{ padding: 2, width: 70, marginRight: 6 }}
|
{hasHolding ? (
|
||||||
/>
|
<svg
|
||||||
<button onClick={() => handleRemove(p.code)} style={{ padding: '2px 8px', cursor: 'pointer' }}>
|
width="14"
|
||||||
确认
|
height="14"
|
||||||
</button>
|
viewBox="0 0 14 14"
|
||||||
<button onClick={() => setRemoveTarget(null)} style={{ padding: '2px 8px', cursor: 'pointer', marginLeft: 4 }}>
|
style={{
|
||||||
取消
|
display: 'inline-block',
|
||||||
</button>
|
transition: 'transform .15s ease',
|
||||||
</span>
|
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||||
) : (
|
color: '#2e7d32',
|
||||||
<span>
|
verticalAlign: 'middle',
|
||||||
<button
|
}}
|
||||||
onClick={() => handleMoveAllIn(p.code)}
|
>
|
||||||
style={{ padding: '2px 8px', cursor: 'pointer', border: '1px solid #2e7d32', borderRadius: 3, background: '#fff', color: '#2e7d32', marginRight: 4 }}
|
<path
|
||||||
title="将该票全部未分配份额移入本策略"
|
d="M4.5 2.5 L9.5 7 L4.5 11.5"
|
||||||
>
|
fill="none"
|
||||||
全部移入
|
stroke="currentColor"
|
||||||
</button>
|
strokeWidth="2"
|
||||||
<button
|
strokeLinecap="round"
|
||||||
onClick={() => setRemoveTarget({ code: p.code, shares: p.shares })}
|
strokeLinejoin="round"
|
||||||
style={{ padding: '2px 8px', cursor: 'pointer' }}
|
/>
|
||||||
>
|
</svg>
|
||||||
移出
|
) : (
|
||||||
</button>
|
<span style={{ color: '#ccc', fontSize: 10 }}>·</span>
|
||||||
</span>
|
)}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '8px 6px' }}>{p.code}</td>
|
||||||
|
<td style={{ padding: '8px 6px' }}>{p.name}</td>
|
||||||
|
<PriceCell price={getPrice(p.code)?.lastPrice} lastClose={getPrice(p.code)?.lastClose} />
|
||||||
|
<td style={{ padding: '8px 6px' }}>{fmtPrice(p.avgPrice)}</td>
|
||||||
|
<td style={{ padding: '8px 6px' }}>{fmtPrice(p.lastTradePrice)}</td>
|
||||||
|
<td style={{ padding: '8px 6px', fontWeight: 'bold' }}>{p.shares}</td>
|
||||||
|
<td style={{ padding: '8px 6px' }}>{p.volume}</td>
|
||||||
|
<td style={{ padding: '8px 6px', whiteSpace: 'nowrap' }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
{removeTarget && removeTarget.code === p.code ? (
|
||||||
|
<span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step={100}
|
||||||
|
value={removeTarget.shares}
|
||||||
|
onChange={(e) => setRemoveTarget({ code: p.code, shares: e.target.value })}
|
||||||
|
style={{ padding: 2, width: 70, marginRight: 6 }}
|
||||||
|
/>
|
||||||
|
<button onClick={() => handleRemove(p.code)} style={{ padding: '2px 8px', cursor: 'pointer' }}>
|
||||||
|
确认
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setRemoveTarget(null)} style={{ padding: '2px 8px', cursor: 'pointer', marginLeft: 4 }}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleMoveAllIn(p.code)}
|
||||||
|
style={{ padding: '2px 8px', cursor: 'pointer', border: '1px solid #2e7d32', borderRadius: 3, background: '#fff', color: '#2e7d32', marginRight: 4 }}
|
||||||
|
title="将该票全部未分配份额移入本策略"
|
||||||
|
>
|
||||||
|
全部移入
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setRemoveTarget({ code: p.code, shares: p.shares })}
|
||||||
|
style={{ padding: '2px 8px', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
移出
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{isExpanded && (
|
||||||
|
<tr style={{ borderBottom: '1px solid #eee', background: '#fafafa' }}>
|
||||||
|
<td colSpan={9} style={{ padding: '4px 8px 12px' }}>
|
||||||
|
{isLoading ? (
|
||||||
|
<div style={{ padding: 8, color: '#999', fontSize: 12 }}>交易记录加载中...</div>
|
||||||
|
) : !trades || trades.length === 0 ? (
|
||||||
|
<div style={{ padding: 8, color: '#999', fontSize: 12 }}>
|
||||||
|
{hasHolding ? '该持仓暂无关联交易记录(可在「交易记录」tab 为委托设置归属)' : '该持仓无关联锚点(holding_id)'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<HoldingTradesTable trades={trades} />
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
)}
|
)}
|
||||||
</td>
|
</React.Fragment>
|
||||||
</tr>
|
);
|
||||||
))}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
@@ -306,6 +407,72 @@ function StrategyTabInner({ strategyId, strategyName }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** R-010:持仓关联的委托汇总表(只显示委托汇总,不分笔成交) */
|
||||||
|
const ORDER_STATUS_MAP = {
|
||||||
|
48: '未报', 49: '待报', 50: '已报', 51: '已报待撤', 52: '部成待撤',
|
||||||
|
53: '部撤', 54: '已撤', 55: '部成', 56: '已成', 57: '废单',
|
||||||
|
};
|
||||||
|
|
||||||
|
function fmtDateH(d) {
|
||||||
|
if (!d || d.length !== 8) return d ?? '—';
|
||||||
|
return d.slice(0, 4) + '-' + d.slice(4, 6) + '-' + d.slice(6, 8);
|
||||||
|
}
|
||||||
|
function fmtTimeH(t) {
|
||||||
|
if (!t || t.length !== 6) return t ?? '';
|
||||||
|
return t.slice(0, 2) + ':' + t.slice(2, 4) + ':' + t.slice(4, 6);
|
||||||
|
}
|
||||||
|
function fmtNumH(v, digits = 2) {
|
||||||
|
if (v == null || !isFinite(v)) return '—';
|
||||||
|
return Number(v).toLocaleString('zh-CN', { minimumFractionDigits: digits, maximumFractionDigits: digits });
|
||||||
|
}
|
||||||
|
function dirTextH(d) {
|
||||||
|
if (d === 'buy') return '买入';
|
||||||
|
if (d === 'sell') return '卖出';
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
function dirColorH(d) {
|
||||||
|
if (d === 'buy') return { color: '#d32f2f', fontWeight: 'bold' };
|
||||||
|
if (d === 'sell') return { color: '#2e7d32', fontWeight: 'bold' };
|
||||||
|
return { color: '#999' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function HoldingTradesTable({ trades }) {
|
||||||
|
const list = Array.isArray(trades) ? trades : [];
|
||||||
|
return (
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12, marginTop: 4 }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ borderBottom: '1px solid #ddd', color: '#666' }}>
|
||||||
|
<th style={{ ...tdH, textAlign: 'left' }}>交易日</th>
|
||||||
|
<th style={{ ...tdH, textAlign: 'left' }}>时间</th>
|
||||||
|
<th style={{ ...tdH, textAlign: 'left' }}>方向</th>
|
||||||
|
<th style={{ ...tdH, textAlign: 'left' }}>状态</th>
|
||||||
|
<th style={{ ...tdH, textAlign: 'right' }}>委托量</th>
|
||||||
|
<th style={{ ...tdH, textAlign: 'right' }}>成交量</th>
|
||||||
|
<th style={{ ...tdH, textAlign: 'right' }}>委托价</th>
|
||||||
|
<th style={{ ...tdH, textAlign: 'right' }}>成交均价</th>
|
||||||
|
<th style={{ ...tdH, textAlign: 'right' }}>成交额</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{list.map((o) => (
|
||||||
|
<tr key={o.orderId} style={{ borderBottom: '1px solid #f0f0f0' }}>
|
||||||
|
<td style={tdH}>{fmtDateH(o.tradeDate)}</td>
|
||||||
|
<td style={tdH}>{fmtTimeH(o.insertTime)}</td>
|
||||||
|
<td style={{ ...tdH, ...dirColorH(o.direction) }}>{dirTextH(o.direction)}</td>
|
||||||
|
<td style={tdH}>{ORDER_STATUS_MAP[o.status] ?? o.status ?? '—'}</td>
|
||||||
|
<td style={{ ...tdH, textAlign: 'right' }}>{fmtNumH(o.orderVolume, 0)}</td>
|
||||||
|
<td style={{ ...tdH, textAlign: 'right' }}>{fmtNumH(o.tradedVolume, 0)}</td>
|
||||||
|
<td style={{ ...tdH, textAlign: 'right' }}>{fmtNumH(o.limitPrice)}</td>
|
||||||
|
<td style={{ ...tdH, textAlign: 'right' }}>{o.tradedVolume > 0 ? fmtNumH(o.amount / o.tradedVolume) : '—'}</td>
|
||||||
|
<td style={{ ...tdH, textAlign: 'right' }}>{fmtNumH(o.amount)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const tdH = { padding: '6px 6px', lineHeight: '20px', whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' };
|
||||||
|
|
||||||
/** 导出组件(包 ToastProvider) */
|
/** 导出组件(包 ToastProvider) */
|
||||||
export function StrategyTab(props) {
|
export function StrategyTab(props) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user