看股功能更新

This commit is contained in:
2026-08-16 00:05:26 +08:00
parent 9cce670b74
commit fc86fe0674
28 changed files with 3823 additions and 96 deletions

View File

@@ -1,7 +1,13 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { addWatchlist, getStockPreview, getWatchlist as getWatchlistApi, removeWatchlist } from '@/api/client';
import type { ChartLayoutPrefs, PreviewResponse, ScreenerItemOut, Timeframe, TooltipField } from '@/api/types';
import {
addWatchlist, clearTrades, getStockPreview, getTrades, getWatchlist as getWatchlistApi,
importTrades, removeWatchlist,
} from '@/api/client';
import type {
ChartLayoutPrefs, PreviewResponse, ScreenerItemOut, Timeframe, TooltipField,
TradesImportResponse, UserTrade,
} from '@/api/types';
import { useSettingsStore, DEFAULT_TOOLTIP_FIELDS, TOOLTIP_FIELDS, type PriceAdjust } from '@/stores/settings';
import DetailKLine from './DetailKLine.vue';
@@ -9,7 +15,12 @@ const props = defineProps<{
items: ScreenerItemOut[];
initial: string; // ts_code
}>();
const emit = defineEmits<{ (e: 'close'): void; (e: 'watched-change'): void }>();
const emit = defineEmits<{
(e: 'close'): void;
(e: 'watched-change'): void;
/** 浮层内切股(键盘 ↑/↓、侧栏点击)时上报当前 ts_code父组件据此同步路由 */
(e: 'change', code: string): void;
}>();
const settings = useSettingsStore();
// ---------- 状态 ----------
@@ -43,11 +54,6 @@ function setTimeframe(tf: Timeframe) {
settings.setChartLayout({ timeframe: tf });
}
// 数据口径徽标market=近段未复权兜底;其余为实际复权口径(可能因因子缺失与所选不同)
const ADJUST_LABELS: Record<string, string> = { bfq: '不复权', qfq: '前复权', hfq: '后复权' };
const sourceLabel = computed(() =>
data.value ? (ADJUST_LABELS[data.value.source] ?? data.value.source) : '');
// ---------- 副图 / MA / 高度(全部随用户偏好持久化) ----------
const SUBS = [
{ key: 'vol', label: 'VOL' },
@@ -68,12 +74,7 @@ function toggleSub(key: string) {
subPanes: cur.includes(key) ? cur.filter((k) => k !== key) : [...cur, key],
});
}
function adjustHeight(key: string, delta: number) {
const DEFAULTS: Record<string, number> = { vol: 64, macd: 100, kdj: 96, rsi: 84 };
const base = subHeights.value;
const next = Math.max(40, (base[key] ?? DEFAULTS[key] ?? 90) + delta);
settings.setChartLayout({ subHeights: { ...base, [key]: next } });
}
// 副图高度改为图内分隔条直接拖拽DetailKLine 订阅 onPaneDrag 持久化),此处不再提供按钮
// 副图拖拽排序
let dragKey: string | null = null;
@@ -120,8 +121,57 @@ function toggleTipField(key: TooltipField) {
tooltipFields: cur.includes(key) ? cur.filter((k) => k !== key) : [...cur, key],
});
}
function resetTipFields() {
settings.setChartLayout({ tooltipFields: [...DEFAULT_TOOLTIP_FIELDS] });
// ---------- 日期跳转(输入 YYYYMMDD把该日K线定位到可视区中央 ----------
const JUMP_END_DAYS = 170; // 锚点 + 170 自然日 ≈ 120 个交易日:锚点恰落在 240 根首吐窗口正中
const jumpInput = ref('');
const jumpErr = ref('');
const jumpTs = ref<number | null>(null); // 当前锚点本地零点时间戳null=最新行情模式
const klineRef = ref<InstanceType<typeof DetailKLine> | null>(null);
/** '20200218' → 本地零点时间戳(与 Candle.ts 的反序列化口径一致精确命中当日K线非法返回 null */
function parseJumpDate(raw: string): number | null {
if (!/^\d{8}$/.test(raw)) return null;
const y = +raw.slice(0, 4), m = +raw.slice(4, 6), d = +raw.slice(6, 8);
const dt = new Date(y, m - 1, d);
if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;
if (y < 1990 || y > 2099) return null;
return dt.getTime();
}
/** 时间戳 → 'YYYY-MM-DD'(后端 end 参数格式) */
function fmtDashDate(ms: number): string {
const dt = new Date(ms);
return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`;
}
function onJumpInput() {
jumpInput.value = jumpInput.value.replace(/\D/g, '').slice(0, 8);
jumpErr.value = '';
}
function jumpToDate() {
const ts = parseJumpDate(jumpInput.value.trim());
if (ts == null) { jumpErr.value = '日期格式20200218'; return; }
jumpErr.value = '';
// 非日K周期先切回日K再跳watcher 重拉时 load 会带上刚设好的锚点)
if (timeframe.value !== '1d') { jumpTs.value = ts; setTimeframe('1d'); return; }
// 快路径:目标日已在当前渲染窗口内 → 纯滚动居中,不打网络
if (klineRef.value?.centerOn(ts)) { jumpTs.value = ts; return; }
// 慢路径:目标日不在窗口内 → 以锚点为中点重拉一窗end 不含当日,故右缘=锚点+170 自然日)
jumpTs.value = ts;
void load(active.value);
}
/** 清除锚点回到最新行情(锚定窗口的右缘停在锚点日之后,需要这个出口) */
function clearJump() {
jumpTs.value = null;
jumpErr.value = '';
void load(active.value);
}
/** DetailKLine 上报:锚点在拉回的数据里也找不到(早于上市首日,或晚于最后一根的未来日期)。
* 统一走与「空数据」相同的回退:清锚点、提示、回最新行情,避免 chip/统计停留在假锚定状态。 */
function onCenterMiss() {
if (jumpTs.value == null) return;
jumpTs.value = null;
jumpErr.value = '该日期无K线早于上市或晚于最新数据已回到最新行情';
void load(active.value);
}
// ---------- 自选股(星标) ----------
@@ -149,6 +199,110 @@ async function toggleWatch() {
}
}
// ---------- 实盘交易点(交割单导入) ----------
const trades = ref<UserTrade[]>([]);
const showTrades = ref(true);
const showTradeImport = ref(false);
const importing = ref(false);
const importResult = ref<TradesImportResponse | null>(null);
const importErr = ref<string | null>(null);
/** 拉当前股的实盘成交(失败静默:未登录/网络异常都不影响看图)。
* 与 load() 同款的请求序号:切股瞬间并发拉 K 线与成交,乱序返回的旧股
* 成交绝不能回填——否则旧股买卖点会画到新股K线上。 */
let tradesToken = 0;
async function loadTrades(code: string) {
const token = ++tradesToken;
try {
const list = await getTrades(code);
if (token === tradesToken) trades.value = list;
} catch {
if (token === tradesToken) trades.value = [];
}
}
watch(active, (code) => {
trades.value = []; // 同步先清:新图挂载时(成交未返回)不能带着旧股标记
loadTrades(code);
}, { immediate: true });
const fmtQty = (q: number) =>
q >= 10000 ? `${(q / 10000).toFixed(1).replace(/\.0$/, '')}` : String(Math.round(q));
const fmtPrice = (p: number) => p.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
/** 按日聚合成标记:图上只显示 B/S/T 单个字母B=当日只买 贴 low 下方、S=当日只卖 贴 high 上方、
* T=当日买+卖「做T」贴 high 上方);数量/均价/费用收进 rows悬停字母时才显示。
* 均价是券商原始成交价的股数加权均值(不复权口径,仅作参考——标记位置贴 bar 高低点,
* 已随复权切换自动对齐)。 */
const tradeMarkers = computed(() => {
if (!showTrades.value) return [];
const byDay = new Map<string, { ts: number; list: UserTrade[] }>();
for (const t of trades.value) {
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(t.trade_date);
if (!m) continue;
let d = byDay.get(m[0]);
if (!d) byDay.set(m[0], d = { ts: new Date(+m[1], +m[2] - 1, +m[3]).getTime(), list: [] });
d.list.push(t);
}
const sumBy = (list: UserTrade[], f: (t: UserTrade) => number) => list.reduce((s, t) => s + f(t), 0);
const dirRow = (label: string, list: UserTrade[], tone: 'buy' | 'sell') => {
const qty = sumBy(list, (t) => t.qty);
const wsum = sumBy(list, (t) => (t.price ?? 0) * t.qty);
const wqty = sumBy(list, (t) => (t.price != null ? t.qty : 0));
const avg = wqty > 0 ? wsum / wqty : null;
return { label, text: `${fmtQty(qty)}${avg != null ? ` @ ${fmtPrice(avg)}` : ''}`, tone };
};
return [...byDay.entries()].map(([date, d]) => {
const buys = d.list.filter((t) => t.direction === 'buy');
const sells = d.list.filter((t) => t.direction === 'sell');
const kind: 'B' | 'S' | 'T' = buys.length && sells.length ? 'T' : buys.length ? 'B' : 'S';
const rows: { label: string; text: string; tone: 'buy' | 'sell' | '' }[] = [
...(buys.length ? [dirRow('买入', buys, 'buy')] : []),
...(sells.length ? [dirRow('卖出', sells, 'sell')] : []),
];
const fee = sumBy(d.list, (t) => t.fee ?? 0);
if (fee > 0) rows.push({ label: '费用', text: fmtPrice(fee), tone: '' });
return { key: date, ts: d.ts, kind, rows };
}).sort((a, b) => a.ts - b.ts);
});
async function onTradeFile(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
importing.value = true;
importResult.value = null;
importErr.value = null;
try {
importResult.value = await importTrades(file);
await loadTrades(active.value);
} catch (err) {
importErr.value = err instanceof Error ? err.message : '导入失败';
} finally {
importing.value = false;
(e.target as HTMLInputElement).value = ''; // 允许重选同一文件
}
}
async function onClearTrades() {
if (!window.confirm('确定清空全部股票的成交记录?此操作不可恢复(需重新导入交割单)。')) return;
try {
await clearTrades();
trades.value = [];
importResult.value = null;
importErr.value = null;
} catch (err) {
importErr.value = err instanceof Error ? err.message : '清空失败';
}
}
/** 打开导入弹窗:清掉上一次的结果/错误,避免误读为本次操作的结果 */
function openTradeImport() {
importResult.value = null;
importErr.value = null;
showMaConfig.value = false;
showTipConfig.value = false;
showTradeImport.value = true;
}
const filteredItems = computed(() => {
const q = filter.value.trim().toLowerCase();
if (!q) return props.items;
@@ -176,17 +330,28 @@ const header = computed(() => {
let fetchToken = 0;
async function load(code: string) {
const token = ++fetchToken;
const anchor = jumpTs.value; // 跳转锚点:之后的所有重拉(复权/周期/MA都围绕它取窗视口位置不漂
loading.value = true;
error.value = null;
data.value = null;
try {
const res = await getStockPreview(code, {
limit: 500,
// 锚定取数:窗口右缘=锚点+170 自然日(后端 end 不含当日),锚点恰好落在 240 根首吐窗口正中
end: anchor != null ? fmtDashDate(anchor + JUMP_END_DAYS * 86400000) : undefined,
adjust: adjust.value,
timeframe: timeframe.value,
mas: maPeriods.value,
});
if (token === fetchToken) data.value = res;
if (token !== fetchToken) return;
if (anchor != null && res.candles.length === 0) {
// 跳转日期早于上市等锚定窗口取不到任何K线 → 退回最新行情
jumpTs.value = null;
jumpErr.value = '该日期无数据(可能早于上市),已回到最新行情';
void load(code);
return;
}
data.value = res;
} catch (e) {
if (token === fetchToken) error.value = e instanceof Error ? e.message : '加载失败';
} finally {
@@ -211,7 +376,13 @@ async function loadOlder(end: string, count: number) {
return null; // 网络失败:图表停止向前翻页(不中断已渲染内容)
}
}
watch(active, (code) => load(code), { immediate: true });
watch(active, (code) => {
// 切股清空日期锚点:新股票以最新行情打开
jumpTs.value = null;
jumpErr.value = '';
load(code);
emit('change', code); // 父组件把当前股写进路由,刷新后可还原
}, { immediate: true });
watch(adjust, () => load(active.value));
watch(timeframe, () => load(active.value));
// MA 周期变化也要重拉(后端按 mas 计算指标序列)
@@ -236,10 +407,14 @@ function onKeydown(e: KeyboardEvent) {
if (e.isComposing) return;
const t = e.target as HTMLElement | null;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
// 弹层打开时接管按键Esc 关弹层,其余(含 ↑/↓)不再切换底层个股
const modalOpen = showTradeImport.value || showMaConfig.value || showTipConfig.value;
if (e.key === 'Escape') {
if (showMaConfig.value || showTipConfig.value) { showMaConfig.value = false; showTipConfig.value = false; }
if (showTradeImport.value) showTradeImport.value = false;
else if (showMaConfig.value || showTipConfig.value) { showMaConfig.value = false; showTipConfig.value = false; }
else emit('close');
}
else if (modalOpen) return;
else if (e.key === 'ArrowUp') { e.preventDefault(); moveActive(-1); }
else if (e.key === 'ArrowDown') { e.preventDefault(); moveActive(1); }
}
@@ -254,7 +429,8 @@ onBeforeUnmount(() => {
// ---------- 右侧信息栏增强52周高低 / 年初至今(从日线序列算,无数据留空) ----------
const stats = computed(() => {
const bars = timeframe.value === '1d' ? data.value?.candles : null;
// 日期跳转后窗口是历史段52周/年初至今口径失真,直接不显示
const bars = timeframe.value === '1d' && jumpTs.value == null ? data.value?.candles : null;
if (!bars || bars.length === 0) return { high52: null, low52: null, ytd: null };
const last = bars[bars.length - 1];
const lastTs = new Date(last.ts);
@@ -333,18 +509,8 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
@click="setAdjust(a.key)"
>{{ a.label }}</button>
</div>
<span v-if="data?.source === 'market'" class="rounded bg-amber-500/15 px-2 py-0.5 text-xs text-amber-300">
近段未复权数据
</span>
<span
v-else-if="data"
class="rounded px-2 py-0.5 text-xs"
:class="data.source === adjust ? 'bg-blue-500/15 text-blue-300' : 'bg-amber-500/15 text-amber-300'"
:title="data.source === adjust ? '' : '该股复权因子缺失,暂按此口径显示(可先同步市场数据)'"
>{{ sourceLabel }}</span>
<span class="ml-auto text-[13px] text-[#9BA3AE]"> 切换 · Esc 关闭 · 滚轮缩放 · 左滑加载历史</span>
<button type="button" class="btn-ghost !px-2.5 !py-1" title="关闭 (Esc)" @click="emit('close')">
<button type="button" class="btn-ghost !px-2.5 !py-1 ml-auto cursor-pointer" title="关闭 (Esc)" @click="emit('close')">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12" /></svg>
</button>
</header>
@@ -399,7 +565,7 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
:class="subPanes.includes(s.key)
? 'bg-blue-600 text-white'
: 'bg-[#101014] text-[#9BA3AE] line-through'"
:title="subPanes.includes(s.key) ? '点击隐藏 · 拖动排序 · 右侧按钮调高度' : '点击显示'"
:title="subPanes.includes(s.key) ? '点击隐藏 · 拖动排序 · 图内分隔线拖拽调高度' : '点击显示'"
@click="toggleSub(s.key)"
@dragstart="onDragStart($event, s.key)"
@dragover.prevent
@@ -407,10 +573,6 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
>
{{ s.label }}
</button>
<template v-if="subPanes.includes(s.key)">
<button type="button" class="border-l px-1 py-1 text-xs text-[#9BA3AE] hover:bg-[#1E2026] hover:text-[#E8EAED]" title="调高" @click="adjustHeight(s.key, 20)"></button>
<button type="button" class="border-l px-1 py-1 text-xs text-[#9BA3AE] hover:bg-[#1E2026] hover:text-[#E8EAED]" title="调矮" @click="adjustHeight(s.key, -20)"></button>
</template>
</div>
<button
type="button"
@@ -419,6 +581,20 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
title="主图叠加布林带"
@click="showBoll = !showBoll"
>BOLL</button>
<!-- 实盘交易点交割单导入的买卖标记 -->
<button
type="button"
class="rounded-md border px-2.5 py-1 text-[13px] transition-colors"
:class="showTrades && trades.length ? 'border-amber-500 bg-amber-500 text-white' : 'border-[#26272E] bg-[#101014] text-[#9BA3AE]'"
:title="trades.length ? `本股实盘成交 ${trades.length} 笔(交割单导入)` : '本股暂无实盘成交记录,点击导入交割单'"
@click="trades.length ? (showTrades = !showTrades) : openTradeImport()"
>交易点{{ trades.length ? ` ${trades.length}` : '' }}</button>
<button
type="button"
class="rounded-md border border-[#26272E] bg-[#101014] px-2.5 py-1 text-[13px] text-[#A8AFB8] transition-colors hover:border-[#3A3D46] hover:text-[#E8EAED]"
title="上传券商交割单,导入实盘买卖点"
@click="openTradeImport()"
>导入交割单</button>
<!-- MA 配置 -->
<div class="relative">
<button
@@ -478,13 +654,40 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
{{ f.label }}
</label>
</div>
<div class="mt-2 flex items-center justify-between">
<span class="text-xs text-[#9BA3AE]">已选 {{ tooltipFields.length }}/{{ TOOLTIP_FIELDS.length }}首行日期固定</span>
<button type="button" class="text-xs text-blue-600 hover:underline" @click="resetTipFields">恢复默认</button>
</div>
</div>
</div>
<span class="ml-auto text-xs text-[#9BA3AE]">点击开关 · 拖动排序 · 调高度 · 右上工具栏画线</span>
<!-- 日期跳转输入 YYYYMMDD该日K线居中显示 -->
<div class="ml-auto flex items-center gap-1.5">
<span class="text-xs text-[#9BA3AE]">定位</span>
<div class="flex items-stretch">
<input
v-model="jumpInput"
type="text"
class="w-24 rounded-l-md border border-[#26272E] bg-[#16181D] px-2 py-1 font-mono text-[13px] text-[#E8EAED] outline-none transition-colors placeholder:text-[#7A818C]"
:class="jumpErr ? 'border-red-500' : 'focus:border-blue-500'"
placeholder="20200218"
maxlength="8"
inputmode="numeric"
title="输入日期YYYYMMDD跳转后该日K线居中显示"
@input="onJumpInput"
@keyup.enter="jumpToDate"
/>
<button
type="button"
class="rounded-r-md border border-l-0 border-[#26272E] bg-[#101014] px-2 py-1 text-[13px] text-[#A8AFB8] transition-colors hover:border-[#3A3D46] hover:text-[#E8EAED]"
title="跳转到该日期的日K回车亦可"
@click="jumpToDate"
>跳转</button>
</div>
<button
v-if="jumpTs != null"
type="button"
class="rounded-md border border-blue-500/60 bg-blue-500/10 px-2 py-1 font-mono text-xs text-blue-300 transition-colors hover:bg-blue-500/20"
title="清除日期锚点,回到最新行情"
@click="clearJump"
>{{ fmtDashDate(jumpTs) }} </button>
<span v-if="jumpErr" class="text-xs text-red-400">{{ jumpErr }}</span>
</div>
</div>
<!-- 图表 -->
@@ -496,6 +699,7 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
<div v-else-if="error" class="flex h-full items-center justify-center text-sm text-red-400">{{ error }}</div>
<DetailKLine
v-else-if="data && data.candles.length"
ref="klineRef"
:ticker="data.ts_code"
:candles="data.candles"
:indicators="data.indicators"
@@ -507,6 +711,9 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
:show-boll="showBoll"
:timeframe="timeframe"
:tooltip-fields="tooltipFields"
:center-ts="jumpTs"
:trade-markers="tradeMarkers"
@center-miss="onCenterMiss"
/>
<div v-else class="flex h-full items-center justify-center text-sm text-[#9BA3AE]">无数据</div>
</div>
@@ -577,5 +784,64 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
</div>
</aside>
</div>
<!-- 交割单导入弹窗 -->
<div
v-if="showTradeImport"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4"
@click.self="showTradeImport = false"
>
<div class="w-full max-w-md rounded-lg border border-[#33353D] bg-[#16181D] p-4 shadow-2xl shadow-black/70">
<div class="flex items-center justify-between">
<span class="text-sm font-semibold text-[#E8EAED]">导入交割单(实盘买卖点)</span>
<button
type="button"
class="rounded p-1 text-[#9BA3AE] transition-colors hover:bg-[#26272E] hover:text-white"
title="关闭 (Esc)"
@click="showTradeImport = false"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12" /></svg>
</button>
</div>
<p class="mt-2 text-xs leading-5 text-[#9BA3AE]">
上传券商导出的交割单(江海证券通达信版:「查询 → 交割单 → 输出」App/同花顺版:「交割单 → 导出/发送邮箱」)。
CSV / Excel / txt / HTML 均可编码与列名自动识别同日成交合并为一个标注B=买入、S=卖出、T=当日买卖都有),鼠标悬停字母可查看数量与均价。
</p>
<label
class="mt-3 flex cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed px-4 py-6 text-center transition-colors"
:class="importing ? 'border-[#26272E] opacity-60' : 'border-[#3A3D46] hover:border-blue-500/60'"
>
<span class="text-[13px] text-[#C3C9D2]">{{ importing ? '解析导入中…' : '点击选择交割单文件' }}</span>
<span class="mt-1 text-xs text-[#7A818C]">支持 .csv / .txt / .xls / .xlsx / .html20MB 以内</span>
<input
type="file"
class="hidden"
accept=".csv,.txt,.xls,.xlsx,.htm,.html"
:disabled="importing"
@change="onTradeFile"
/>
</label>
<!-- 导入结果反馈 -->
<div v-if="importResult" class="mt-3 rounded-md border border-blue-500/30 bg-blue-500/10 p-2.5 text-xs leading-5 text-[#C3C9D2]">
<div>
新增 <span class="font-semibold text-blue-300">{{ importResult.inserted }}</span> 笔成交,覆盖 {{ importResult.stocks }} 只股票;
重复跳过 {{ importResult.skipped_dup }} 笔,其他跳过 {{ importResult.skipped_other }} 笔。
</div>
<div v-if="importResult.bad.length" class="mt-1 text-red-400">
{{ importResult.bad.join('') }}
</div>
</div>
<div v-if="importErr" class="mt-3 rounded-md border border-red-500/40 bg-red-500/10 p-2.5 text-xs text-red-400">{{ importErr }}</div>
<!-- 危险操作:清空(作用于全部股票,不随当前股是否有成交隐藏入口) -->
<div class="mt-3 flex items-center justify-between border-t border-[#1E2026] pt-3">
<span class="text-xs text-[#7A818C]">清空全部股票的成交记录K 线买卖点将全部消失</span>
<button
type="button"
class="rounded-md border border-red-500/50 px-2.5 py-1 text-[13px] text-red-400 transition-colors hover:bg-red-500/15"
@click="onClearTrades"
>清空全部成交记录</button>
</div>
</div>
</div>
</div>
</template>