2025-12-29 03:06:49 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="ai-analysis-container" :class="{ 'theme-dark': isDarkTheme }" :style="{ '--primary-color': primaryColor }">
|
2026-01-31 02:59:49 +08:00
|
|
|
|
<!-- 全宽主内容区域 -->
|
|
|
|
|
|
<div class="main-content-full">
|
|
|
|
|
|
<!-- 顶部指数条 -->
|
|
|
|
|
|
<div class="top-index-bar">
|
|
|
|
|
|
<!-- 情绪指标 - 独立加载 -->
|
|
|
|
|
|
<template v-if="loadingSentiment">
|
|
|
|
|
|
<div class="indicator-box skeleton-box">
|
|
|
|
|
|
<span class="skeleton-text short"></span>
|
|
|
|
|
|
<span class="skeleton-text"></span>
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</div>
|
2026-01-31 02:59:49 +08:00
|
|
|
|
<div class="indicator-box skeleton-box">
|
|
|
|
|
|
<span class="skeleton-text short"></span>
|
|
|
|
|
|
<span class="skeleton-text"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="indicator-box skeleton-box">
|
|
|
|
|
|
<span class="skeleton-text short"></span>
|
|
|
|
|
|
<span class="skeleton-text"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="indicator-box fear-greed" :class="getFearGreedClass(marketData.fearGreed)">
|
|
|
|
|
|
<span class="ind-label">{{ $t('globalMarket.fearGreedShort') }}</span>
|
|
|
|
|
|
<span class="ind-value">{{ marketData.fearGreed || '--' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="indicator-box vix" :class="getVixLevel(marketData.vix)">
|
|
|
|
|
|
<span class="ind-label">VIX</span>
|
|
|
|
|
|
<span class="ind-value">{{ marketData.vix || '--' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="indicator-box dxy">
|
|
|
|
|
|
<span class="ind-label">DXY</span>
|
|
|
|
|
|
<span class="ind-value">{{ marketData.dxy || '--' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
<!-- 全球指数滚动条 - 独立加载 -->
|
|
|
|
|
|
<div class="indices-marquee">
|
|
|
|
|
|
<template v-if="loadingIndices">
|
|
|
|
|
|
<div class="indices-loading">
|
|
|
|
|
|
<a-icon type="loading" /> {{ $t('common.loading') || '加载中...' }}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</div>
|
2026-01-31 02:59:49 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="marketData.indices.length > 0">
|
|
|
|
|
|
<div class="marquee-track">
|
|
|
|
|
|
<div class="index-item" v-for="idx in marketData.indices" :key="'a-'+idx.symbol">
|
|
|
|
|
|
<span class="idx-flag">{{ idx.flag }}</span>
|
|
|
|
|
|
<span class="idx-symbol">{{ idx.symbol }}</span>
|
|
|
|
|
|
<span class="idx-price">{{ formatPrice(idx.price) }}</span>
|
|
|
|
|
|
<span class="idx-change" :class="idx.change >= 0 ? 'up' : 'down'">
|
|
|
|
|
|
<a-icon :type="idx.change >= 0 ? 'caret-up' : 'caret-down'" />
|
|
|
|
|
|
{{ Math.abs(idx.change).toFixed(2) }}%
|
|
|
|
|
|
</span>
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</div>
|
2026-01-31 02:59:49 +08:00
|
|
|
|
<div class="index-item" v-for="idx in marketData.indices" :key="'b-'+idx.symbol">
|
|
|
|
|
|
<span class="idx-flag">{{ idx.flag }}</span>
|
|
|
|
|
|
<span class="idx-symbol">{{ idx.symbol }}</span>
|
|
|
|
|
|
<span class="idx-price">{{ formatPrice(idx.price) }}</span>
|
|
|
|
|
|
<span class="idx-change" :class="idx.change >= 0 ? 'up' : 'down'">
|
|
|
|
|
|
<a-icon :type="idx.change >= 0 ? 'caret-up' : 'caret-down'" />
|
|
|
|
|
|
{{ Math.abs(idx.change).toFixed(2) }}%
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-31 02:59:49 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="indices-empty">--</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-button type="link" size="small" class="refresh-btn" :loading="loadingMarket" @click="loadMarketData">
|
|
|
|
|
|
<a-icon type="sync" :spin="loadingMarket" />
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 主体三栏布局 -->
|
|
|
|
|
|
<div class="main-body">
|
|
|
|
|
|
<!-- 左侧:热力图 + 财经日历 -->
|
|
|
|
|
|
<div class="left-panel">
|
|
|
|
|
|
<!-- 热力图 - 独立加载 -->
|
|
|
|
|
|
<div class="heatmap-box">
|
|
|
|
|
|
<div class="box-header">
|
|
|
|
|
|
<a-radio-group v-model="heatmapType" size="small" button-style="solid">
|
|
|
|
|
|
<a-radio-button value="crypto">{{ $t('globalMarket.cryptoHeatmap') }}</a-radio-button>
|
|
|
|
|
|
<a-radio-button value="commodities">{{ $t('globalMarket.commoditiesHeatmap') }}</a-radio-button>
|
|
|
|
|
|
<a-radio-button value="sectors">{{ $t('globalMarket.sectorHeatmap') }}</a-radio-button>
|
|
|
|
|
|
<a-radio-button value="forex">{{ $t('globalMarket.forexHeatmap') }}</a-radio-button>
|
|
|
|
|
|
</a-radio-group>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="heatmap-grid">
|
|
|
|
|
|
<template v-if="loadingHeatmap">
|
|
|
|
|
|
<div v-for="i in 12" :key="'skel-'+i" class="heat-cell skeleton-cell">
|
|
|
|
|
|
<span class="skeleton-text short"></span>
|
|
|
|
|
|
<span class="skeleton-text"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="currentHeatmap.length > 0">
|
|
|
|
|
|
<div v-for="(item, i) in currentHeatmap.slice(0, 12)" :key="i" class="heat-cell" :style="getHeatmapStyle(item.value)">
|
|
|
|
|
|
<span class="heat-name">{{ getHeatmapName(item) }}</span>
|
|
|
|
|
|
<span class="heat-price" v-if="item.price">{{ formatHeatmapPrice(item.price) }}</span>
|
|
|
|
|
|
<span class="heat-val">{{ item.value >= 0 ? '+' : '' }}{{ formatNum(item.value) }}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="heatmap-empty">{{ $t('common.noData') || '暂无数据' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</div>
|
2026-01-31 02:59:49 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 财经日历 - 独立加载 -->
|
|
|
|
|
|
<div class="calendar-box">
|
|
|
|
|
|
<div class="box-header">
|
|
|
|
|
|
<span class="box-title"><a-icon type="calendar" /> {{ $t('globalMarket.calendar') }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="calendar-list">
|
|
|
|
|
|
<template v-if="loadingCalendar">
|
|
|
|
|
|
<div v-for="i in 5" :key="'cal-skel-'+i" class="cal-item skeleton-item">
|
|
|
|
|
|
<span class="skeleton-text short"></span>
|
|
|
|
|
|
<span class="skeleton-text short"></span>
|
|
|
|
|
|
<span class="skeleton-text"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="marketData.calendar.length > 0">
|
|
|
|
|
|
<div v-for="evt in marketData.calendar.slice(0, 10)" :key="evt.id" class="cal-item" :class="evt.importance">
|
|
|
|
|
|
<span class="cal-date">{{ formatCalendarDate(evt.date) }}</span>
|
|
|
|
|
|
<span class="cal-time">{{ evt.time || '--:--' }}</span>
|
|
|
|
|
|
<span class="cal-flag">{{ getCountryFlag(evt.country) }}</span>
|
|
|
|
|
|
<span class="cal-name">{{ isZhLocale ? evt.name : evt.name_en }}</span>
|
|
|
|
|
|
<span class="cal-impact" :class="getImpactClass(evt)">
|
|
|
|
|
|
<a-icon v-if="getImpactClass(evt) === 'bullish'" type="arrow-up" />
|
|
|
|
|
|
<a-icon v-else-if="getImpactClass(evt) === 'bearish'" type="arrow-down" />
|
|
|
|
|
|
<a-icon v-else type="minus" />
|
|
|
|
|
|
{{ evt.actual || evt.forecast || '--' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="cal-empty">{{ $t('globalMarket.noEvents') }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧:工具栏 + AI 分析 -->
|
|
|
|
|
|
<div class="right-panel">
|
|
|
|
|
|
<!-- 分析工具栏 -->
|
|
|
|
|
|
<div class="analysis-toolbar">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model="selectedSymbol"
|
|
|
|
|
|
:placeholder="$t('dashboard.analysis.empty.selectSymbol')"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
show-search
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
:filter-option="filterSymbolOption"
|
|
|
|
|
|
@change="handleSymbolChange"
|
|
|
|
|
|
class="symbol-selector"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-select-option
|
|
|
|
|
|
v-for="stock in (watchlist || [])"
|
|
|
|
|
|
:key="`${stock.market}-${stock.symbol}`"
|
|
|
|
|
|
:value="`${stock.market}:${stock.symbol}`"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="symbol-option">
|
|
|
|
|
|
<a-tag :color="getMarketColor(stock.market)" size="small">{{ getMarketName(stock.market) }}</a-tag>
|
|
|
|
|
|
<strong style="margin-left: 6px;">{{ stock.symbol }}</strong>
|
|
|
|
|
|
<span v-if="stock.name" class="symbol-name">{{ stock.name }}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
<a-select-option key="add-stock-option" value="__add_stock_option__" class="add-stock-option">
|
|
|
|
|
|
<div style="text-align: center; padding: 4px 0; color: #1890ff;">
|
|
|
|
|
|
<a-icon type="plus" style="margin-right: 4px;" />{{ $t('dashboard.analysis.watchlist.add') }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
icon="thunderbolt"
|
|
|
|
|
|
@click="startFastAnalysis"
|
|
|
|
|
|
:loading="analyzing"
|
|
|
|
|
|
:disabled="!selectedSymbol"
|
|
|
|
|
|
class="analyze-button">
|
|
|
|
|
|
{{ $t('fastAnalysis.startAnalysis') }}
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button size="large" icon="history" @click="showHistoryModal = true; loadHistoryList()" class="history-button">
|
|
|
|
|
|
{{ $t('fastAnalysis.history') }}
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 分析结果区域 -->
|
|
|
|
|
|
<div class="analysis-main">
|
|
|
|
|
|
<div v-if="!analysisResult && !analyzing" class="analysis-placeholder">
|
|
|
|
|
|
<div class="placeholder-content">
|
|
|
|
|
|
<div class="placeholder-icon"><a-icon type="robot" /></div>
|
|
|
|
|
|
<h3>{{ $t('fastAnalysis.selectTip') }}</h3>
|
|
|
|
|
|
<p>{{ $t('fastAnalysis.selectHint') }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<FastAnalysisReport
|
|
|
|
|
|
v-if="analysisResult || analyzing"
|
|
|
|
|
|
:result="analysisResult"
|
|
|
|
|
|
:loading="analyzing"
|
|
|
|
|
|
:error="analysisError"
|
|
|
|
|
|
@retry="startFastAnalysis"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧自选股面板 -->
|
|
|
|
|
|
<div class="watchlist-panel">
|
|
|
|
|
|
<div class="panel-header">
|
|
|
|
|
|
<span class="panel-title"><a-icon type="star" theme="filled" /> {{ $t('dashboard.analysis.watchlist.title') }}</span>
|
|
|
|
|
|
<a-button type="link" size="small" @click="showAddStockModal = true">
|
|
|
|
|
|
<a-icon type="plus" />
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="watchlist-list">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="stock in (watchlist || [])"
|
|
|
|
|
|
:key="`${stock.market}-${stock.symbol}`"
|
|
|
|
|
|
class="watchlist-item"
|
|
|
|
|
|
:class="{ active: selectedSymbol === `${stock.market}:${stock.symbol}` }"
|
|
|
|
|
|
@click="selectWatchlistItem(stock)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="item-main">
|
|
|
|
|
|
<span class="item-symbol">{{ stock.symbol }}</span>
|
|
|
|
|
|
<span class="item-name">{{ stock.name || getMarketName(stock.market) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="item-price" v-if="watchlistPrices[`${stock.market}:${stock.symbol}`]">
|
|
|
|
|
|
<span class="price-value">{{ formatPrice(watchlistPrices[`${stock.market}:${stock.symbol}`].price) }}</span>
|
|
|
|
|
|
<span class="price-change" :class="(watchlistPrices[`${stock.market}:${stock.symbol}`]?.change || 0) >= 0 ? 'up' : 'down'">
|
|
|
|
|
|
{{ (watchlistPrices[`${stock.market}:${stock.symbol}`]?.change || 0) >= 0 ? '+' : '' }}{{ formatNum(watchlistPrices[`${stock.market}:${stock.symbol}`]?.change) }}%
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-icon type="close" class="item-remove" @click.stop="removeFromWatchlist(stock)" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="!watchlist || watchlist.length === 0" class="watchlist-empty">
|
|
|
|
|
|
<a-icon type="inbox" />
|
|
|
|
|
|
<p>{{ $t('dashboard.analysis.empty.noWatchlist') }}</p>
|
|
|
|
|
|
<a-button type="primary" size="small" @click="showAddStockModal = true">
|
|
|
|
|
|
<a-icon type="plus" /> {{ $t('dashboard.analysis.watchlist.add') }}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</a-button>
|
2026-01-31 02:59:49 +08:00
|
|
|
|
</div>
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 添加股票弹窗 -->
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
:title="$t('dashboard.analysis.modal.addStock.title')"
|
|
|
|
|
|
:visible="showAddStockModal"
|
|
|
|
|
|
@ok="handleAddStock"
|
|
|
|
|
|
@cancel="handleCloseAddStockModal"
|
|
|
|
|
|
:confirmLoading="addingStock"
|
|
|
|
|
|
width="600px"
|
|
|
|
|
|
:okText="$t('dashboard.analysis.modal.addStock.confirm')"
|
|
|
|
|
|
:cancelText="$t('dashboard.analysis.modal.addStock.cancel')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="add-stock-modal-content">
|
|
|
|
|
|
<!-- Tab标签 -->
|
|
|
|
|
|
<a-tabs v-model="selectedMarketTab" @change="handleMarketTabChange" class="market-tabs">
|
|
|
|
|
|
<a-tab-pane
|
|
|
|
|
|
v-for="marketType in marketTypes"
|
|
|
|
|
|
:key="marketType.value"
|
|
|
|
|
|
:tab="$t(marketType.i18nKey || `dashboard.analysis.market.${marketType.value}`)"
|
|
|
|
|
|
>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
</a-tabs>
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
<!-- 搜索/输入框 -->
|
2025-12-29 03:06:49 +08:00
|
|
|
|
<div class="symbol-search-section">
|
|
|
|
|
|
<a-input-search
|
|
|
|
|
|
v-model="symbolSearchKeyword"
|
|
|
|
|
|
:placeholder="$t('dashboard.analysis.modal.addStock.searchOrInputPlaceholder')"
|
|
|
|
|
|
@search="handleSearchOrInput"
|
|
|
|
|
|
@change="handleSymbolSearchInput"
|
|
|
|
|
|
:loading="searchingSymbols"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-button slot="enterButton" type="primary" icon="search">
|
|
|
|
|
|
{{ $t('dashboard.analysis.modal.addStock.search') }}
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-input-search>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 搜索结果 -->
|
|
|
|
|
|
<div v-if="symbolSearchResults.length > 0" class="search-results-section">
|
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
<a-icon type="search" style="margin-right: 4px;" />
|
|
|
|
|
|
{{ $t('dashboard.analysis.modal.addStock.searchResults') }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-list
|
|
|
|
|
|
:data-source="symbolSearchResults"
|
|
|
|
|
|
:loading="searchingSymbols"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
class="symbol-list"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-list-item slot="renderItem" slot-scope="item" class="symbol-list-item" @click="selectSymbol(item)">
|
|
|
|
|
|
<a-list-item-meta>
|
|
|
|
|
|
<template slot="title">
|
|
|
|
|
|
<div class="symbol-item-content">
|
|
|
|
|
|
<span class="symbol-code">{{ item.symbol }}</span>
|
|
|
|
|
|
<span class="symbol-name">{{ item.name }}</span>
|
|
|
|
|
|
<a-tag v-if="item.exchange" size="small" color="blue" style="margin-left: 8px;">
|
|
|
|
|
|
{{ item.exchange }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 热门标的 -->
|
|
|
|
|
|
<div class="hot-symbols-section">
|
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
|
<a-icon type="fire" style="color: #ff4d4f; margin-right: 4px;" />
|
|
|
|
|
|
{{ $t('dashboard.analysis.modal.addStock.hotSymbols') }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-spin :spinning="loadingHotSymbols">
|
|
|
|
|
|
<a-list
|
|
|
|
|
|
v-if="hotSymbols.length > 0"
|
|
|
|
|
|
:data-source="hotSymbols"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
class="symbol-list"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-list-item slot="renderItem" slot-scope="item" class="symbol-list-item" @click="selectSymbol(item)">
|
|
|
|
|
|
<a-list-item-meta>
|
|
|
|
|
|
<template slot="title">
|
|
|
|
|
|
<div class="symbol-item-content">
|
|
|
|
|
|
<span class="symbol-code">{{ item.symbol }}</span>
|
|
|
|
|
|
<span class="symbol-name">{{ item.name }}</span>
|
|
|
|
|
|
<a-tag v-if="item.exchange" size="small" color="orange" style="margin-left: 8px;">
|
|
|
|
|
|
{{ item.exchange }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
<a-empty v-else :description="$t('dashboard.analysis.modal.addStock.noHotSymbols')" :image="false" />
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 选中的标的显示 -->
|
|
|
|
|
|
<div v-if="selectedSymbolForAdd" class="selected-symbol-section">
|
|
|
|
|
|
<a-alert
|
|
|
|
|
|
:message="$t('dashboard.analysis.modal.addStock.selectedSymbol')"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
closable
|
|
|
|
|
|
@close="selectedSymbolForAdd = null"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template slot="description">
|
|
|
|
|
|
<div class="selected-symbol-info">
|
|
|
|
|
|
<a-tag :color="getMarketColor(selectedSymbolForAdd.market)" style="margin-right: 8px;">
|
|
|
|
|
|
{{ $t(`dashboard.analysis.market.${selectedSymbolForAdd.market}`) }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
<strong>{{ selectedSymbolForAdd.symbol }}</strong>
|
|
|
|
|
|
<span v-if="selectedSymbolForAdd.name" style="color: #999; margin-left: 8px;">{{ selectedSymbolForAdd.name }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-alert>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 历史分析列表弹窗 -->
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
:title="$t('dashboard.analysis.modal.history.title')"
|
|
|
|
|
|
:visible="showHistoryModal"
|
|
|
|
|
|
@cancel="showHistoryModal = false"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
width="800px"
|
|
|
|
|
|
:bodyStyle="{ maxHeight: '60vh', overflowY: 'auto' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-spin :spinning="historyLoading">
|
|
|
|
|
|
<a-list
|
|
|
|
|
|
:data-source="historyList"
|
|
|
|
|
|
:pagination="{
|
|
|
|
|
|
current: historyPage,
|
|
|
|
|
|
pageSize: historyPageSize,
|
|
|
|
|
|
total: historyTotal,
|
|
|
|
|
|
onChange: (page) => { historyPage = page; loadHistoryList() },
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
pageSizeOptions: ['10', '20', '50'],
|
|
|
|
|
|
onShowSizeChange: (current, size) => { historyPageSize = size; historyPage = 1; loadHistoryList() }
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-list-item slot="renderItem" slot-scope="item">
|
|
|
|
|
|
<a-list-item-meta>
|
|
|
|
|
|
<template slot="title">
|
|
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<a-tag :color="getMarketColor(item.market)" style="margin-right: 8px;">
|
|
|
|
|
|
{{ getMarketName(item.market) }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
<strong>{{ item.symbol }}</strong>
|
2026-01-31 02:59:49 +08:00
|
|
|
|
<a-tag
|
|
|
|
|
|
:color="item.decision === 'BUY' ? 'green' : (item.decision === 'SELL' ? 'red' : 'blue')"
|
|
|
|
|
|
style="margin-left: 12px;"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.decision }}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</a-tag>
|
2026-01-31 02:59:49 +08:00
|
|
|
|
<span style="color: #999; margin-left: 8px; font-size: 12px;">
|
|
|
|
|
|
{{ $t('fastAnalysis.confidence') }}: {{ item.confidence }}%
|
|
|
|
|
|
</span>
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon="eye"
|
|
|
|
|
|
@click="viewHistoryResult(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ $t('dashboard.analysis.modal.history.viewResult') }}
|
|
|
|
|
|
</a-button>
|
2026-01-14 05:29:55 +08:00
|
|
|
|
<a-popconfirm
|
|
|
|
|
|
:title="$t('dashboard.analysis.modal.history.deleteConfirm')"
|
|
|
|
|
|
:ok-text="$t('common.confirm')"
|
|
|
|
|
|
:cancel-text="$t('common.cancel')"
|
|
|
|
|
|
@confirm="deleteHistoryItem(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon="delete"
|
|
|
|
|
|
style="color: #ff4d4f;"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ $t('dashboard.analysis.modal.history.delete') }}
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-popconfirm>
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template slot="description">
|
2026-01-31 02:59:49 +08:00
|
|
|
|
<div style="color: #666; font-size: 12px;">
|
|
|
|
|
|
<span v-if="item.price">${{ formatNumber(item.price) }}</span>
|
|
|
|
|
|
<span v-if="item.summary" style="margin-left: 8px;">{{ item.summary.substring(0, 80) }}{{ item.summary.length > 80 ? '...' : '' }}</span>
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</div>
|
2026-01-31 02:59:49 +08:00
|
|
|
|
<div v-if="item.created_at" style="color: #999; font-size: 12px; margin-top: 4px;">
|
|
|
|
|
|
{{ formatIsoTime(item.created_at) }}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-list-item-meta>
|
|
|
|
|
|
</a-list-item>
|
|
|
|
|
|
</a-list>
|
|
|
|
|
|
<a-empty v-if="!historyLoading && historyList.length === 0" :description="$t('dashboard.analysis.empty.noHistory')" />
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import { mapGetters, mapState } from 'vuex'
|
|
|
|
|
|
import { getUserInfo } from '@/api/login'
|
2026-01-31 02:59:49 +08:00
|
|
|
|
import { getWatchlist, addWatchlist, removeWatchlist, getWatchlistPrices, getMarketTypes, searchSymbols, getHotSymbols } from '@/api/market'
|
|
|
|
|
|
import { fastAnalyze, getAllAnalysisHistory, deleteAnalysisHistory } from '@/api/fast-analysis'
|
|
|
|
|
|
import { getMarketSentiment, getMarketOverview, getMarketHeatmap, getEconomicCalendar } from '@/api/global-market'
|
|
|
|
|
|
import FastAnalysisReport from './components/FastAnalysisReport.vue'
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'Analysis',
|
|
|
|
|
|
components: {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
FastAnalysisReport
|
2025-12-29 03:06:49 +08:00
|
|
|
|
},
|
|
|
|
|
|
data () {
|
|
|
|
|
|
return {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
loadingMarket: false,
|
|
|
|
|
|
heatmapType: 'crypto',
|
|
|
|
|
|
marketData: {
|
|
|
|
|
|
fearGreed: null,
|
|
|
|
|
|
vix: null,
|
|
|
|
|
|
dxy: null,
|
|
|
|
|
|
indices: [],
|
|
|
|
|
|
heatmap: { crypto: [], commodities: [], sectors: [], forex: [] },
|
|
|
|
|
|
calendar: []
|
|
|
|
|
|
},
|
|
|
|
|
|
// 独立加载状态 - 渐进式加载
|
|
|
|
|
|
loadingSentiment: false,
|
|
|
|
|
|
loadingIndices: false,
|
|
|
|
|
|
loadingHeatmap: false,
|
|
|
|
|
|
loadingCalendar: false,
|
|
|
|
|
|
watchlistPriceTimer: null,
|
|
|
|
|
|
watchlistPrices: {},
|
|
|
|
|
|
localUserInfo: {},
|
2025-12-29 03:06:49 +08:00
|
|
|
|
loadingUserInfo: false,
|
|
|
|
|
|
userId: 1,
|
|
|
|
|
|
watchlist: [],
|
|
|
|
|
|
loadingWatchlist: false,
|
|
|
|
|
|
showAddStockModal: false,
|
|
|
|
|
|
addingStock: false,
|
2026-01-31 02:59:49 +08:00
|
|
|
|
selectedSymbol: undefined,
|
|
|
|
|
|
analyzing: false,
|
|
|
|
|
|
analysisResult: null,
|
|
|
|
|
|
analysisError: null,
|
2025-12-29 03:06:49 +08:00
|
|
|
|
showHistoryModal: false,
|
|
|
|
|
|
historyList: [],
|
|
|
|
|
|
historyLoading: false,
|
|
|
|
|
|
historyPage: 1,
|
|
|
|
|
|
historyPageSize: 20,
|
|
|
|
|
|
historyTotal: 0,
|
|
|
|
|
|
marketTypes: [],
|
2026-01-31 02:59:49 +08:00
|
|
|
|
selectedMarketTab: '',
|
|
|
|
|
|
symbolSearchKeyword: '',
|
|
|
|
|
|
symbolSearchResults: [],
|
|
|
|
|
|
searchingSymbols: false,
|
|
|
|
|
|
hotSymbols: [],
|
|
|
|
|
|
loadingHotSymbols: false,
|
|
|
|
|
|
selectedSymbolForAdd: null,
|
|
|
|
|
|
searchTimer: null,
|
|
|
|
|
|
hasSearched: false
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
...mapGetters(['userInfo']),
|
|
|
|
|
|
...mapState({
|
|
|
|
|
|
navTheme: state => state.app.theme,
|
|
|
|
|
|
primaryColor: state => state.app.color || '#1890ff'
|
|
|
|
|
|
}),
|
|
|
|
|
|
isDarkTheme () {
|
|
|
|
|
|
return this.navTheme === 'dark' || this.navTheme === 'realdark'
|
|
|
|
|
|
},
|
2026-01-31 02:59:49 +08:00
|
|
|
|
isZhLocale () {
|
|
|
|
|
|
return this.$i18n.locale === 'zh-CN'
|
|
|
|
|
|
},
|
|
|
|
|
|
currentHeatmap () {
|
|
|
|
|
|
return this.marketData.heatmap[this.heatmapType] || []
|
|
|
|
|
|
},
|
2025-12-29 03:06:49 +08:00
|
|
|
|
storeUserInfo () {
|
|
|
|
|
|
return this.userInfo || {}
|
|
|
|
|
|
},
|
|
|
|
|
|
mergedUserInfo () {
|
|
|
|
|
|
return this.localUserInfo && this.localUserInfo.email ? this.localUserInfo : this.storeUserInfo
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
created () {
|
|
|
|
|
|
this.loadUserInfo()
|
2026-01-31 02:59:49 +08:00
|
|
|
|
this.loadMarketTypes()
|
2025-12-29 03:06:49 +08:00
|
|
|
|
this.loadWatchlist()
|
2026-01-31 02:59:49 +08:00
|
|
|
|
this.loadMarketData()
|
2025-12-29 03:06:49 +08:00
|
|
|
|
},
|
|
|
|
|
|
mounted () {
|
|
|
|
|
|
this.startWatchlistPriceRefresh()
|
|
|
|
|
|
},
|
|
|
|
|
|
beforeDestroy () {
|
|
|
|
|
|
if (this.watchlistPriceTimer) {
|
|
|
|
|
|
clearInterval(this.watchlistPriceTimer)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
filterSymbolOption (input, option) {
|
|
|
|
|
|
const value = option.componentOptions?.propsData?.value || ''
|
2026-01-31 02:59:49 +08:00
|
|
|
|
if (value === '__add_stock_option__') return true
|
2025-12-29 03:06:49 +08:00
|
|
|
|
return value.toLowerCase().includes(input.toLowerCase())
|
|
|
|
|
|
},
|
|
|
|
|
|
handleSymbolChange (value) {
|
|
|
|
|
|
if (value === '__add_stock_option__') {
|
|
|
|
|
|
this.showAddStockModal = true
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.selectedSymbol = undefined
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.selectedSymbol = value
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// Clear previous result when symbol changes
|
|
|
|
|
|
this.analysisResult = null
|
|
|
|
|
|
this.analysisError = null
|
|
|
|
|
|
},
|
|
|
|
|
|
selectWatchlistItem (stock) {
|
|
|
|
|
|
this.selectedSymbol = `${stock.market}:${stock.symbol}`
|
|
|
|
|
|
this.analysisResult = null
|
|
|
|
|
|
this.analysisError = null
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadMarketData () {
|
|
|
|
|
|
// 渐进式加载:每个数据块独立加载,先出来的先显示
|
|
|
|
|
|
this.loadingMarket = true
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 加载情绪指标(恐惧贪婪、VIX、DXY)- 通常最快
|
|
|
|
|
|
this.loadSentimentData()
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 加载全球指数 - 可能较慢
|
|
|
|
|
|
this.loadIndicesData()
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 加载热力图数据
|
|
|
|
|
|
this.loadHeatmapData()
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 加载财经日历
|
|
|
|
|
|
this.loadCalendarData()
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadSentimentData () {
|
|
|
|
|
|
this.loadingSentiment = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getMarketSentiment()
|
|
|
|
|
|
if (res?.code === 1 && res?.data) {
|
|
|
|
|
|
this.marketData.fearGreed = res.data.fear_greed?.value || null
|
|
|
|
|
|
this.marketData.vix = res.data.vix?.value || null
|
|
|
|
|
|
this.marketData.dxy = res.data.dxy?.value || null
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Load sentiment failed:', e)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loadingSentiment = false
|
|
|
|
|
|
this.checkAllLoaded()
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-01-31 02:59:49 +08:00
|
|
|
|
async loadIndicesData () {
|
|
|
|
|
|
this.loadingIndices = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getMarketOverview()
|
|
|
|
|
|
if (res?.code === 1 && res?.data) {
|
|
|
|
|
|
this.marketData.indices = res.data.indices || []
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Load indices failed:', e)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loadingIndices = false
|
|
|
|
|
|
this.checkAllLoaded()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadHeatmapData () {
|
|
|
|
|
|
this.loadingHeatmap = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getMarketHeatmap()
|
|
|
|
|
|
if (res?.code === 1 && res?.data) {
|
|
|
|
|
|
this.marketData.heatmap = {
|
|
|
|
|
|
crypto: res.data.crypto || [],
|
|
|
|
|
|
commodities: res.data.commodities || [],
|
|
|
|
|
|
sectors: res.data.sectors || [],
|
|
|
|
|
|
forex: res.data.forex || []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Load heatmap failed:', e)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loadingHeatmap = false
|
|
|
|
|
|
this.checkAllLoaded()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadCalendarData () {
|
|
|
|
|
|
this.loadingCalendar = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getEconomicCalendar()
|
|
|
|
|
|
if (res?.code === 1) {
|
|
|
|
|
|
this.marketData.calendar = res.data || []
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Load calendar failed:', e)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loadingCalendar = false
|
|
|
|
|
|
this.checkAllLoaded()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
checkAllLoaded () {
|
|
|
|
|
|
// 当所有数据都加载完成时,关闭总loading状态
|
|
|
|
|
|
if (!this.loadingSentiment && !this.loadingIndices && !this.loadingHeatmap && !this.loadingCalendar) {
|
|
|
|
|
|
this.loadingMarket = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
getFearGreedClass (val) {
|
|
|
|
|
|
if (!val) return ''
|
|
|
|
|
|
if (val <= 25) return 'extreme-fear'
|
|
|
|
|
|
if (val <= 45) return 'fear'
|
|
|
|
|
|
if (val <= 55) return 'neutral'
|
|
|
|
|
|
if (val <= 75) return 'greed'
|
|
|
|
|
|
return 'extreme-greed'
|
|
|
|
|
|
},
|
|
|
|
|
|
getVixLevel (val) {
|
|
|
|
|
|
if (!val) return ''
|
|
|
|
|
|
if (val < 15) return 'low'
|
|
|
|
|
|
if (val < 25) return 'medium'
|
|
|
|
|
|
return 'high'
|
|
|
|
|
|
},
|
|
|
|
|
|
formatNum (num, digits = 2) {
|
|
|
|
|
|
if (num === undefined || num === null || isNaN(num)) return '--'
|
|
|
|
|
|
return Number(num).toFixed(digits)
|
|
|
|
|
|
},
|
|
|
|
|
|
getHeatmapStyle (value) {
|
|
|
|
|
|
const v = parseFloat(value) || 0
|
|
|
|
|
|
const intensity = Math.min(Math.abs(v) / 5, 1)
|
|
|
|
|
|
if (v >= 0) {
|
|
|
|
|
|
return { background: `rgba(34, 197, 94, ${0.15 + intensity * 0.6})`, color: v > 2 ? '#fff' : '#166534' }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return { background: `rgba(239, 68, 68, ${0.15 + intensity * 0.6})`, color: v < -2 ? '#fff' : '#991b1b' }
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
getCountryFlag (country) {
|
|
|
|
|
|
const flags = { US: '🇺🇸', CN: '🇨🇳', EU: '🇪🇺', JP: '🇯🇵', UK: '🇬🇧', DE: '🇩🇪', AU: '🇦🇺', CA: '🇨🇦' }
|
|
|
|
|
|
return flags[country] || '🌍'
|
|
|
|
|
|
},
|
|
|
|
|
|
formatCalendarDate (dateStr) {
|
|
|
|
|
|
if (!dateStr) return ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const date = new Date(dateStr)
|
|
|
|
|
|
const today = new Date()
|
|
|
|
|
|
const tomorrow = new Date(today)
|
|
|
|
|
|
tomorrow.setDate(tomorrow.getDate() + 1)
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否是今天或明天
|
|
|
|
|
|
if (date.toDateString() === today.toDateString()) {
|
|
|
|
|
|
return this.isZhLocale ? '今天' : 'Today'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (date.toDateString() === tomorrow.toDateString()) {
|
|
|
|
|
|
return this.isZhLocale ? '明天' : 'Tmrw'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示月/日
|
|
|
|
|
|
const month = date.getMonth() + 1
|
|
|
|
|
|
const day = date.getDate()
|
|
|
|
|
|
return `${month}/${day}`
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return dateStr
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
formatPrice (price) {
|
|
|
|
|
|
if (!price) return '--'
|
|
|
|
|
|
if (price >= 10000) return (price / 1000).toFixed(1) + 'K'
|
|
|
|
|
|
if (price >= 1000) return price.toFixed(0)
|
|
|
|
|
|
return price.toFixed(2)
|
|
|
|
|
|
},
|
|
|
|
|
|
formatHeatmapPrice (price) {
|
|
|
|
|
|
if (!price) return ''
|
|
|
|
|
|
if (price >= 10000) return '$' + (price / 1000).toFixed(1) + 'K'
|
|
|
|
|
|
if (price >= 1000) return '$' + price.toFixed(0)
|
|
|
|
|
|
if (price >= 1) return '$' + price.toFixed(2)
|
|
|
|
|
|
return '$' + price.toFixed(4)
|
|
|
|
|
|
},
|
|
|
|
|
|
getHeatmapName (item) {
|
|
|
|
|
|
// sectors, commodities, forex 都需要多语言适配
|
|
|
|
|
|
if (this.heatmapType === 'sectors' || this.heatmapType === 'commodities' || this.heatmapType === 'forex') {
|
|
|
|
|
|
return this.isZhLocale ? (item.name_cn || item.name) : (item.name_en || item.name)
|
|
|
|
|
|
}
|
|
|
|
|
|
return item.name
|
|
|
|
|
|
},
|
|
|
|
|
|
getImpactClass (evt) {
|
|
|
|
|
|
return evt.actual_impact || evt.expected_impact || 'neutral'
|
|
|
|
|
|
},
|
2025-12-29 03:06:49 +08:00
|
|
|
|
getMarketColor (market) {
|
|
|
|
|
|
const colors = {
|
|
|
|
|
|
'USStock': 'green',
|
|
|
|
|
|
'Crypto': 'purple',
|
|
|
|
|
|
'Forex': 'gold',
|
2025-12-30 18:47:42 +08:00
|
|
|
|
'Futures': 'cyan',
|
|
|
|
|
|
'AShare': 'blue',
|
|
|
|
|
|
'HShare': 'orange'
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
return colors[market] || 'default'
|
|
|
|
|
|
},
|
|
|
|
|
|
getCurrencySymbol (market) {
|
|
|
|
|
|
const dollarMarkets = ['USStock', 'Crypto', 'Forex', 'Futures']
|
|
|
|
|
|
return dollarMarkets.includes(market) ? '$' : '¥'
|
|
|
|
|
|
},
|
2026-01-31 02:59:49 +08:00
|
|
|
|
async startFastAnalysis () {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
if (!this.selectedSymbol) {
|
|
|
|
|
|
this.$message.warning(this.$t('dashboard.analysis.message.selectSymbol'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.analyzing = true
|
2026-01-31 02:59:49 +08:00
|
|
|
|
this.analysisError = null
|
2026-01-14 05:29:55 +08:00
|
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
|
const [market, symbol] = this.selectedSymbol.split(':')
|
|
|
|
|
|
const language = this.$store.getters.lang || 'zh-CN'
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
const res = await fastAnalyze({
|
2025-12-29 03:06:49 +08:00
|
|
|
|
market: market,
|
|
|
|
|
|
symbol: symbol,
|
|
|
|
|
|
language: language,
|
2026-01-14 05:29:55 +08:00
|
|
|
|
timeframe: '1D'
|
2025-12-29 03:06:49 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (res && res.code === 1 && res.data) {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
this.analysisResult = res.data
|
|
|
|
|
|
this.$message.success(this.$t('dashboard.analysis.message.analysisComplete'))
|
2025-12-29 03:06:49 +08:00
|
|
|
|
} else {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
throw new Error(res?.msg || '分析失败')
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
console.error('Fast analysis failed:', error)
|
|
|
|
|
|
this.analysisError = error?.response?.data?.msg || error?.message || this.$t('dashboard.analysis.message.analysisFailed')
|
|
|
|
|
|
this.$message.error(this.analysisError)
|
|
|
|
|
|
} finally {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
this.analyzing = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadHistoryList () {
|
|
|
|
|
|
this.historyLoading = true
|
|
|
|
|
|
try {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
const res = await getAllAnalysisHistory({
|
2025-12-29 03:06:49 +08:00
|
|
|
|
page: this.historyPage,
|
|
|
|
|
|
pagesize: this.historyPageSize
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (res && res.code === 1 && res.data) {
|
|
|
|
|
|
this.historyList = res.data.list || []
|
|
|
|
|
|
this.historyTotal = res.data.total || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
this.$message.error(this.$t('dashboard.analysis.message.loadHistoryFailed') || '加载历史记录失败')
|
2025-12-29 03:06:49 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
this.historyLoading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-01-31 02:59:49 +08:00
|
|
|
|
async viewHistoryResult (item) {
|
|
|
|
|
|
// 如果有完整结果,直接显示
|
|
|
|
|
|
if (item.full_result) {
|
|
|
|
|
|
this.analysisResult = item.full_result
|
|
|
|
|
|
this.selectedSymbol = `${item.market}:${item.symbol}`
|
|
|
|
|
|
this.showHistoryModal = false
|
2025-12-29 03:06:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// 否则使用历史记录中的基本信息构建显示
|
|
|
|
|
|
this.analysisResult = {
|
|
|
|
|
|
decision: item.decision,
|
|
|
|
|
|
confidence: item.confidence,
|
|
|
|
|
|
summary: item.summary,
|
|
|
|
|
|
market_data: {
|
|
|
|
|
|
current_price: item.price,
|
|
|
|
|
|
change_24h: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
trading_plan: {
|
|
|
|
|
|
entry_price: item.price,
|
|
|
|
|
|
stop_loss: item.price * 0.95,
|
|
|
|
|
|
take_profit: item.price * 1.05
|
|
|
|
|
|
},
|
|
|
|
|
|
scores: item.scores || {},
|
|
|
|
|
|
reasons: item.reasons || [],
|
|
|
|
|
|
risks: [],
|
|
|
|
|
|
indicators: item.indicators || {},
|
|
|
|
|
|
memory_id: item.id,
|
|
|
|
|
|
analysis_time_ms: 0
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
2026-01-31 02:59:49 +08:00
|
|
|
|
this.selectedSymbol = `${item.market}:${item.symbol}`
|
|
|
|
|
|
this.showHistoryModal = false
|
2025-12-29 03:06:49 +08:00
|
|
|
|
},
|
2026-01-14 05:29:55 +08:00
|
|
|
|
async deleteHistoryItem (item) {
|
|
|
|
|
|
try {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
const res = await deleteAnalysisHistory(item.id)
|
2026-01-14 05:29:55 +08:00
|
|
|
|
if (res && res.code === 1) {
|
|
|
|
|
|
this.$message.success(this.$t('dashboard.analysis.message.deleteSuccess'))
|
|
|
|
|
|
this.loadHistoryList()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.error(res?.msg || this.$t('dashboard.analysis.message.deleteFailed'))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.$message.error(this.$t('dashboard.analysis.message.deleteFailed'))
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-12-29 03:06:49 +08:00
|
|
|
|
formatTime (timestamp) {
|
|
|
|
|
|
if (!timestamp) return '-'
|
|
|
|
|
|
const date = new Date(timestamp * 1000)
|
|
|
|
|
|
return date.toLocaleString('zh-CN')
|
|
|
|
|
|
},
|
2026-01-31 02:59:49 +08:00
|
|
|
|
formatIsoTime (isoString) {
|
|
|
|
|
|
if (!isoString) return '-'
|
|
|
|
|
|
const date = new Date(isoString)
|
|
|
|
|
|
return date.toLocaleString('zh-CN')
|
|
|
|
|
|
},
|
2025-12-29 03:06:49 +08:00
|
|
|
|
getStatusColor (status) {
|
|
|
|
|
|
const colors = {
|
|
|
|
|
|
'pending': 'orange',
|
|
|
|
|
|
'processing': 'blue',
|
|
|
|
|
|
'completed': 'green',
|
|
|
|
|
|
'failed': 'red'
|
|
|
|
|
|
}
|
|
|
|
|
|
return colors[status] || 'default'
|
|
|
|
|
|
},
|
|
|
|
|
|
getStatusText (status) {
|
|
|
|
|
|
const statusMap = {
|
|
|
|
|
|
'pending': 'dashboard.analysis.status.pending',
|
|
|
|
|
|
'processing': 'dashboard.analysis.status.processing',
|
|
|
|
|
|
'completed': 'dashboard.analysis.status.completed',
|
|
|
|
|
|
'failed': 'dashboard.analysis.status.failed'
|
|
|
|
|
|
}
|
|
|
|
|
|
const key = statusMap[status]
|
|
|
|
|
|
return key ? this.$t(key) : status
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadUserInfo () {
|
|
|
|
|
|
this.loadingUserInfo = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (this.storeUserInfo && this.storeUserInfo.email) {
|
|
|
|
|
|
this.localUserInfo = this.storeUserInfo
|
|
|
|
|
|
this.userId = this.storeUserInfo.id
|
|
|
|
|
|
this.loadingUserInfo = false
|
|
|
|
|
|
this.loadWatchlist()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await getUserInfo()
|
|
|
|
|
|
if (res && res.code === 1 && res.data) {
|
|
|
|
|
|
this.localUserInfo = res.data
|
|
|
|
|
|
this.userId = res.data.id
|
|
|
|
|
|
this.$store.commit('SET_INFO', res.data)
|
|
|
|
|
|
this.loadWatchlist()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// Silent fail
|
2025-12-29 03:06:49 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
this.loadingUserInfo = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadWatchlist () {
|
|
|
|
|
|
if (!this.userId) return
|
|
|
|
|
|
this.loadingWatchlist = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getWatchlist({ userid: this.userId })
|
|
|
|
|
|
if (res && res.code === 1 && res.data) {
|
|
|
|
|
|
this.watchlist = res.data.map(item => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
price: 0,
|
|
|
|
|
|
change: 0,
|
|
|
|
|
|
changePercent: 0
|
|
|
|
|
|
}))
|
|
|
|
|
|
await this.loadWatchlistPrices()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// Silent fail
|
2025-12-29 03:06:49 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
this.loadingWatchlist = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadWatchlistPrices () {
|
|
|
|
|
|
if (!this.watchlist || this.watchlist.length === 0) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const watchlistData = this.watchlist.map(item => ({
|
|
|
|
|
|
market: item.market,
|
|
|
|
|
|
symbol: item.symbol
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
const res = await getWatchlistPrices({
|
|
|
|
|
|
watchlist: watchlistData
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (res && res.code === 1 && res.data) {
|
|
|
|
|
|
const priceMap = {}
|
2026-01-31 02:59:49 +08:00
|
|
|
|
const pricesObj = {}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
res.data.forEach(item => {
|
|
|
|
|
|
priceMap[`${item.market}-${item.symbol}`] = item
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// 同时填充 watchlistPrices 对象(使用 : 作为键)
|
|
|
|
|
|
pricesObj[`${item.market}:${item.symbol}`] = {
|
|
|
|
|
|
price: item.price || 0,
|
|
|
|
|
|
change: item.changePercent || 0
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
})
|
2026-01-31 02:59:49 +08:00
|
|
|
|
this.watchlistPrices = pricesObj
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
this.watchlist = this.watchlist.map(item => {
|
|
|
|
|
|
const key = `${item.market}-${item.symbol}`
|
|
|
|
|
|
const priceData = priceMap[key]
|
|
|
|
|
|
if (priceData) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
price: priceData.price || 0,
|
|
|
|
|
|
change: priceData.change || 0,
|
|
|
|
|
|
changePercent: priceData.changePercent || 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return item
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// Silent fail
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
startWatchlistPriceRefresh () {
|
|
|
|
|
|
this.watchlistPriceTimer = setInterval(() => {
|
|
|
|
|
|
if (this.watchlist && this.watchlist.length > 0) {
|
|
|
|
|
|
this.loadWatchlistPrices()
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 30000)
|
|
|
|
|
|
|
|
|
|
|
|
if (this.watchlist && this.watchlist.length > 0) {
|
|
|
|
|
|
this.loadWatchlistPrices()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async handleAddStock () {
|
|
|
|
|
|
let market = ''
|
|
|
|
|
|
let symbol = ''
|
|
|
|
|
|
let name = ''
|
|
|
|
|
|
|
|
|
|
|
|
if (this.selectedSymbolForAdd) {
|
|
|
|
|
|
market = this.selectedSymbolForAdd.market
|
|
|
|
|
|
symbol = this.selectedSymbolForAdd.symbol.toUpperCase()
|
|
|
|
|
|
name = this.selectedSymbolForAdd.name || ''
|
|
|
|
|
|
} else if (this.symbolSearchKeyword && this.symbolSearchKeyword.trim()) {
|
|
|
|
|
|
if (!this.selectedMarketTab) {
|
|
|
|
|
|
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseSelectMarket'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
market = this.selectedMarketTab
|
|
|
|
|
|
symbol = this.symbolSearchKeyword.trim().toUpperCase()
|
|
|
|
|
|
name = ''
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseSelectOrEnterSymbol'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.addingStock = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await addWatchlist({
|
|
|
|
|
|
userid: this.userId,
|
|
|
|
|
|
market: market,
|
|
|
|
|
|
symbol: symbol,
|
|
|
|
|
|
name: name
|
|
|
|
|
|
})
|
|
|
|
|
|
if (res && res.code === 1) {
|
|
|
|
|
|
this.$message.success(this.$t('dashboard.analysis.message.addStockSuccess'))
|
|
|
|
|
|
this.handleCloseAddStockModal()
|
|
|
|
|
|
await this.loadWatchlist()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.error(res?.msg || this.$t('dashboard.analysis.message.addStockFailed'))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
const errorMsg = error?.response?.data?.msg || error?.message || this.$t('dashboard.analysis.message.addStockFailed')
|
|
|
|
|
|
this.$message.error(errorMsg)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.addingStock = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
handleCloseAddStockModal () {
|
|
|
|
|
|
this.showAddStockModal = false
|
|
|
|
|
|
this.selectedSymbolForAdd = null
|
|
|
|
|
|
this.symbolSearchKeyword = ''
|
|
|
|
|
|
this.symbolSearchResults = []
|
|
|
|
|
|
this.hasSearched = false
|
|
|
|
|
|
this.selectedMarketTab = this.marketTypes.length > 0 ? this.marketTypes[0].value : ''
|
|
|
|
|
|
},
|
|
|
|
|
|
handleMarketTabChange (activeKey) {
|
|
|
|
|
|
this.selectedMarketTab = activeKey
|
|
|
|
|
|
this.symbolSearchKeyword = ''
|
|
|
|
|
|
this.symbolSearchResults = []
|
|
|
|
|
|
this.selectedSymbolForAdd = null
|
|
|
|
|
|
this.hasSearched = false
|
|
|
|
|
|
this.loadHotSymbols(activeKey)
|
|
|
|
|
|
},
|
|
|
|
|
|
handleSymbolSearchInput (e) {
|
|
|
|
|
|
const keyword = e.target.value
|
|
|
|
|
|
this.symbolSearchKeyword = keyword
|
|
|
|
|
|
|
|
|
|
|
|
if (this.searchTimer) {
|
|
|
|
|
|
clearTimeout(this.searchTimer)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!keyword || keyword.trim() === '') {
|
|
|
|
|
|
this.symbolSearchResults = []
|
|
|
|
|
|
this.hasSearched = false
|
|
|
|
|
|
this.selectedSymbolForAdd = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.searchTimer = setTimeout(() => {
|
|
|
|
|
|
this.searchSymbolsInModal(keyword)
|
|
|
|
|
|
}, 500)
|
|
|
|
|
|
},
|
|
|
|
|
|
handleSearchOrInput (keyword) {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
if (!keyword || !keyword.trim()) return
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
if (!this.selectedMarketTab) {
|
|
|
|
|
|
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseSelectMarket'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
if (this.symbolSearchResults.length > 0) return
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
if (this.hasSearched && this.symbolSearchResults.length === 0) {
|
|
|
|
|
|
this.handleDirectAdd()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.searchSymbolsInModal(keyword)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async searchSymbolsInModal (keyword) {
|
|
|
|
|
|
if (!keyword || keyword.trim() === '') {
|
|
|
|
|
|
this.symbolSearchResults = []
|
|
|
|
|
|
this.hasSearched = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.selectedMarketTab) {
|
|
|
|
|
|
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseSelectMarket'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.searchingSymbols = true
|
|
|
|
|
|
this.hasSearched = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await searchSymbols({
|
|
|
|
|
|
market: this.selectedMarketTab,
|
|
|
|
|
|
keyword: keyword.trim(),
|
|
|
|
|
|
limit: 20
|
|
|
|
|
|
})
|
|
|
|
|
|
if (res && res.code === 1 && res.data && res.data.length > 0) {
|
|
|
|
|
|
this.symbolSearchResults = res.data
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.symbolSearchResults = []
|
|
|
|
|
|
this.selectedSymbolForAdd = {
|
|
|
|
|
|
market: this.selectedMarketTab,
|
|
|
|
|
|
symbol: keyword.trim().toUpperCase(),
|
2026-01-31 02:59:49 +08:00
|
|
|
|
name: ''
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.symbolSearchResults = []
|
|
|
|
|
|
this.selectedSymbolForAdd = {
|
|
|
|
|
|
market: this.selectedMarketTab,
|
|
|
|
|
|
symbol: keyword.trim().toUpperCase(),
|
2026-01-31 02:59:49 +08:00
|
|
|
|
name: ''
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.searchingSymbols = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
handleDirectAdd () {
|
|
|
|
|
|
if (!this.symbolSearchKeyword || !this.symbolSearchKeyword.trim()) {
|
|
|
|
|
|
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseEnterSymbol'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.selectedMarketTab) {
|
|
|
|
|
|
this.$message.warning(this.$t('dashboard.analysis.modal.addStock.pleaseSelectMarket'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.selectedSymbolForAdd = {
|
|
|
|
|
|
market: this.selectedMarketTab,
|
|
|
|
|
|
symbol: this.symbolSearchKeyword.trim().toUpperCase(),
|
2026-01-31 02:59:49 +08:00
|
|
|
|
name: ''
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
selectSymbol (symbol) {
|
|
|
|
|
|
this.selectedSymbolForAdd = {
|
|
|
|
|
|
market: symbol.market,
|
|
|
|
|
|
symbol: symbol.symbol,
|
|
|
|
|
|
name: symbol.name || symbol.symbol
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadHotSymbols (market) {
|
|
|
|
|
|
if (!market) {
|
|
|
|
|
|
market = this.selectedMarketTab || (this.marketTypes.length > 0 ? this.marketTypes[0].value : '')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
if (!market) return
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
this.loadingHotSymbols = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getHotSymbols({
|
|
|
|
|
|
market: market,
|
|
|
|
|
|
limit: 10
|
|
|
|
|
|
})
|
|
|
|
|
|
if (res && res.code === 1 && res.data) {
|
|
|
|
|
|
this.hotSymbols = res.data
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.hotSymbols = []
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.hotSymbols = []
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loadingHotSymbols = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-01-31 02:59:49 +08:00
|
|
|
|
async removeFromWatchlist (stock) {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
if (!this.userId) return
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// 支持传入 stock 对象或单独的 symbol/market
|
|
|
|
|
|
const symbol = typeof stock === 'object' ? stock.symbol : stock
|
|
|
|
|
|
const market = typeof stock === 'object' ? stock.market : arguments[1]
|
2025-12-29 03:06:49 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const res = await removeWatchlist({
|
|
|
|
|
|
userid: this.userId,
|
2026-01-31 02:59:49 +08:00
|
|
|
|
symbol: symbol,
|
|
|
|
|
|
market: market
|
2025-12-29 03:06:49 +08:00
|
|
|
|
})
|
|
|
|
|
|
if (res && res.code === 1) {
|
|
|
|
|
|
this.$message.success(this.$t('dashboard.analysis.message.removeStockSuccess'))
|
|
|
|
|
|
await this.loadWatchlist()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.error(res?.msg || this.$t('dashboard.analysis.message.removeStockFailed'))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.$message.error(this.$t('dashboard.analysis.message.removeStockFailed'))
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
getMarketName (market) {
|
|
|
|
|
|
return this.$t(`dashboard.analysis.market.${market}`) || market
|
|
|
|
|
|
},
|
|
|
|
|
|
formatNumber (num) {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
if (typeof num === 'string') return num
|
2025-12-29 03:06:49 +08:00
|
|
|
|
return num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadMarketTypes () {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getMarketTypes()
|
|
|
|
|
|
if (res && res.code === 1 && res.data && Array.isArray(res.data)) {
|
|
|
|
|
|
this.marketTypes = res.data.map(item => ({
|
|
|
|
|
|
value: item.value,
|
|
|
|
|
|
i18nKey: item.i18nKey || `dashboard.analysis.market.${item.value}`
|
|
|
|
|
|
}))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.marketTypes = [
|
|
|
|
|
|
{ value: 'USStock', i18nKey: 'dashboard.analysis.market.USStock' },
|
|
|
|
|
|
{ value: 'Crypto', i18nKey: 'dashboard.analysis.market.Crypto' },
|
|
|
|
|
|
{ value: 'Forex', i18nKey: 'dashboard.analysis.market.Forex' },
|
2025-12-30 18:27:55 +08:00
|
|
|
|
{ value: 'Futures', i18nKey: 'dashboard.analysis.market.Futures' },
|
2025-12-30 19:20:21 +08:00
|
|
|
|
{ value: 'HShare', i18nKey: 'dashboard.analysis.market.HShare' },
|
|
|
|
|
|
{ value: 'AShare', i18nKey: 'dashboard.analysis.market.AShare' }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.marketTypes = [
|
|
|
|
|
|
{ value: 'USStock', i18nKey: 'dashboard.analysis.market.USStock' },
|
|
|
|
|
|
{ value: 'Crypto', i18nKey: 'dashboard.analysis.market.Crypto' },
|
|
|
|
|
|
{ value: 'Forex', i18nKey: 'dashboard.analysis.market.Forex' },
|
2025-12-30 18:27:55 +08:00
|
|
|
|
{ value: 'Futures', i18nKey: 'dashboard.analysis.market.Futures' },
|
2025-12-30 19:20:21 +08:00
|
|
|
|
{ value: 'HShare', i18nKey: 'dashboard.analysis.market.HShare' },
|
|
|
|
|
|
{ value: 'AShare', i18nKey: 'dashboard.analysis.market.AShare' }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.marketTypes.length > 0 && !this.selectedMarketTab) {
|
|
|
|
|
|
this.selectedMarketTab = this.marketTypes[0].value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
watch: {
|
|
|
|
|
|
showAddStockModal (newVal) {
|
|
|
|
|
|
if (newVal) {
|
|
|
|
|
|
if (this.marketTypes.length > 0 && !this.selectedMarketTab) {
|
|
|
|
|
|
this.selectedMarketTab = this.marketTypes[0].value
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.selectedMarketTab) {
|
|
|
|
|
|
this.loadHotSymbols(this.selectedMarketTab)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.selectedSymbolForAdd = null
|
|
|
|
|
|
this.symbolSearchKeyword = ''
|
|
|
|
|
|
this.symbolSearchResults = []
|
|
|
|
|
|
this.hasSearched = false
|
|
|
|
|
|
if (this.searchTimer) {
|
|
|
|
|
|
clearTimeout(this.searchTimer)
|
|
|
|
|
|
this.searchTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
|
.ai-analysis-container {
|
|
|
|
|
|
display: flex;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
height: calc(100vh - 120px);
|
2025-12-29 03:06:49 +08:00
|
|
|
|
background: #f0f2f5;
|
|
|
|
|
|
overflow: hidden;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
width: 100%;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// 全宽主内容
|
|
|
|
|
|
.main-content-full {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
2026-01-31 02:59:49 +08:00
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// 顶部指数条
|
|
|
|
|
|
.top-index-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.indicator-box {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
display: flex;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
flex-direction: column;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
align-items: center;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
min-width: 50px;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.ind-label { font-size: 9px; color: #94a3b8; text-transform: uppercase; }
|
|
|
|
|
|
.ind-value { font-size: 13px; font-weight: 700; color: #1e293b; }
|
|
|
|
|
|
|
|
|
|
|
|
&.fear-greed.extreme-fear .ind-value { color: #dc2626; }
|
|
|
|
|
|
&.fear-greed.fear .ind-value { color: #ea580c; }
|
|
|
|
|
|
&.fear-greed.neutral .ind-value { color: #ca8a04; }
|
|
|
|
|
|
&.fear-greed.greed .ind-value { color: #65a30d; }
|
|
|
|
|
|
&.fear-greed.extreme-greed .ind-value { color: #16a34a; }
|
|
|
|
|
|
&.vix.low .ind-value { color: #16a34a; }
|
|
|
|
|
|
&.vix.medium .ind-value { color: #ca8a04; }
|
|
|
|
|
|
&.vix.high .ind-value { color: #dc2626; }
|
|
|
|
|
|
&.dxy .ind-value { color: #2563eb; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.indices-marquee {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
|
|
|
|
.marquee-track {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
animation: marquee 35s linear infinite;
|
|
|
|
|
|
width: max-content;
|
|
|
|
|
|
&:hover { animation-play-state: paused; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.index-item {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
gap: 4px;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
white-space: nowrap;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.idx-flag { font-size: 11px; }
|
|
|
|
|
|
.idx-symbol { color: #64748b; font-weight: 500; }
|
|
|
|
|
|
.idx-price { color: #1e293b; font-weight: 600; }
|
|
|
|
|
|
.idx-change {
|
|
|
|
|
|
font-weight: 600;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
gap: 1px;
|
|
|
|
|
|
&.up { color: #16a34a; }
|
|
|
|
|
|
&.down { color: #dc2626; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
@keyframes marquee {
|
|
|
|
|
|
0% { transform: translateX(0); }
|
|
|
|
|
|
100% { transform: translateX(-50%); }
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.refresh-btn {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
&:hover { color: #1e293b; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// 主体三栏布局
|
|
|
|
|
|
.main-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// 左侧面板:热力图 + 财经日历
|
|
|
|
|
|
.left-panel {
|
|
|
|
|
|
width: 280px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
overflow-y: auto;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.heatmap-box {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
|
|
|
|
|
|
.box-header {
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
::v-deep .ant-radio-group .ant-radio-button-wrapper {
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
padding: 0 6px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
line-height: 20px;
|
|
|
|
|
|
&.ant-radio-button-wrapper-checked {
|
|
|
|
|
|
background: var(--primary-color, #1890ff);
|
|
|
|
|
|
border-color: var(--primary-color, #1890ff);
|
|
|
|
|
|
color: #fff;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-31 02:59:49 +08:00
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.heatmap-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
.heat-cell {
|
|
|
|
|
|
padding: 6px 4px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 9px;
|
|
|
|
|
|
.heat-name { display: block; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 1px; }
|
|
|
|
|
|
.heat-price { display: block; font-size: 9px; opacity: 0.8; margin-bottom: 1px; }
|
|
|
|
|
|
.heat-val { font-weight: 700; font-size: 10px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.calendar-box {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
.box-header {
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
.box-title {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
.anticon { margin-right: 6px; color: var(--primary-color, #1890ff); }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.calendar-list {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar { width: 4px; }
|
|
|
|
|
|
&::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 2px; }
|
|
|
|
|
|
|
|
|
|
|
|
.cal-item {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
display: flex;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
|
border-bottom: 1px solid #f1f5f9;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
&:last-child { border-bottom: none; }
|
|
|
|
|
|
&.high { border-left: 3px solid #dc2626; padding-left: 8px; margin-left: -4px; }
|
|
|
|
|
|
&.medium { border-left: 3px solid #ca8a04; padding-left: 8px; margin-left: -4px; }
|
|
|
|
|
|
&.low { border-left: 3px solid #16a34a; padding-left: 8px; margin-left: -4px; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.cal-date {
|
|
|
|
|
|
font-size: 9px;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
min-width: 32px;
|
|
|
|
|
|
font-weight: 500;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.cal-time { color: #64748b; min-width: 36px; font-weight: 500; }
|
|
|
|
|
|
.cal-flag { font-size: 12px; }
|
|
|
|
|
|
.cal-name { flex: 1; color: #334155; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
|
|
.cal-impact {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
font-weight: 600;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
font-size: 10px;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
gap: 2px;
|
|
|
|
|
|
&.bullish { color: #16a34a; }
|
|
|
|
|
|
&.bearish { color: #dc2626; }
|
|
|
|
|
|
&.neutral { color: #94a3b8; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.cal-empty { text-align: center; color: #94a3b8; padding: 20px 0; font-size: 12px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// 右侧面板:工具栏 + 分析结果
|
|
|
|
|
|
// 中间分析面板
|
|
|
|
|
|
.right-panel {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.analysis-toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border-bottom: 1px solid #f1f5f9;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
border-radius: 8px 8px 0 0;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.symbol-selector {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
max-width: 320px;
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.analyze-button {
|
|
|
|
|
|
background: var(--primary-color, #1890ff);
|
|
|
|
|
|
border-color: var(--primary-color, #1890ff);
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.history-button {
|
|
|
|
|
|
border-color: #d9d9d9;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.analysis-main {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
|
|
|
|
|
|
.analysis-placeholder {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
min-height: 300px;
|
|
|
|
|
|
|
|
|
|
|
|
.placeholder-content {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
.placeholder-icon { font-size: 72px; color: var(--primary-color, #1890ff); opacity: 0.5; margin-bottom: 20px; }
|
|
|
|
|
|
h3 { font-size: 18px; color: #1e293b; margin-bottom: 8px; }
|
|
|
|
|
|
p { font-size: 14px; color: #64748b; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// 右侧自选股面板
|
|
|
|
|
|
.watchlist-panel {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
flex-shrink: 0;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
background: #fff;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.panel-header {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
border-bottom: 1px solid #f1f5f9;
|
|
|
|
|
|
background: #fafafa;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.panel-title {
|
|
|
|
|
|
font-size: 12px;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
font-weight: 600;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
color: #64748b;
|
|
|
|
|
|
.anticon { color: #facc15; margin-right: 6px; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.watchlist-list {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
padding: 8px;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
&::-webkit-scrollbar { width: 4px; }
|
|
|
|
|
|
&::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 2px; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
.watchlist-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
margin-bottom: 4px;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
&:hover { background: #f8fafc; }
|
|
|
|
|
|
&.active { background: #e6f7ff; border: 1px solid #91d5ff; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.item-main {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
flex: 1;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
min-width: 0;
|
|
|
|
|
|
.item-symbol { display: block; font-size: 12px; font-weight: 600; color: #1e293b; }
|
|
|
|
|
|
.item-name { display: block; font-size: 10px; color: #94a3b8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.item-price {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
text-align: right;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
font-family: 'SF Mono', Monaco, monospace;
|
|
|
|
|
|
.price-value { display: block; font-size: 11px; font-weight: 600; color: #1e293b; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
.price-change {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 10px;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
font-weight: 600;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
&.up { color: #16a34a; }
|
|
|
|
|
|
&.down { color: #dc2626; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.item-remove {
|
|
|
|
|
|
color: #cbd5e1;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
font-size: 12px;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 0.2s;
|
|
|
|
|
|
&:hover { color: #dc2626; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
&:hover .item-remove { opacity: 1; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.watchlist-empty {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 24px 12px;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
.anticon { font-size: 32px; margin-bottom: 8px; display: block; }
|
|
|
|
|
|
p { font-size: 12px; margin-bottom: 12px; }
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes pulse {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
0%, 100% { opacity: 1; }
|
|
|
|
|
|
50% { opacity: 0.5; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
/* Responsive */
|
2025-12-29 03:06:49 +08:00
|
|
|
|
@media (max-width: 992px) {
|
|
|
|
|
|
.ai-analysis-container {
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
min-height: calc(100vh - 64px);
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.main-content-full {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
height: auto;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
min-height: auto;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.top-index-bar {
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
padding: 8px;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.indicator-box {
|
|
|
|
|
|
min-width: 45px;
|
|
|
|
|
|
padding: 3px 6px;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.indices-marquee {
|
|
|
|
|
|
order: 10;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
width: 100%;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
margin-top: 8px;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.main-body {
|
|
|
|
|
|
flex-direction: column;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
padding: 12px;
|
2026-01-31 02:59:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.left-panel {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
flex-direction: row;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.heatmap-box, .calendar-box {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.calendar-box {
|
|
|
|
|
|
max-height: 200px;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.right-panel {
|
|
|
|
|
|
.analysis-toolbar {
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
.symbol-selector { width: 100% !important; max-width: none !important; }
|
|
|
|
|
|
.analyze-button, .history-button { flex: 1; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
2026-01-31 02:59:49 +08:00
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.watchlist-panel {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
|
order: -1;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
/* Dark Theme */
|
|
|
|
|
|
.ai-analysis-container.theme-dark {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
background: #131722;
|
|
|
|
|
|
color: #d1d4dc;
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.main-content-full {
|
2025-12-29 03:06:49 +08:00
|
|
|
|
background: #1e222d;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
2026-01-31 02:59:49 +08:00
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.top-index-bar {
|
|
|
|
|
|
background: #1e222d;
|
|
|
|
|
|
border-bottom-color: #363c4e;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.indicator-box {
|
|
|
|
|
|
background: #2a2e39;
|
|
|
|
|
|
border-color: #363c4e;
|
|
|
|
|
|
.ind-label { color: #868993; }
|
|
|
|
|
|
.ind-value { color: #d1d4dc; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.indices-marquee .index-item {
|
|
|
|
|
|
background: #2a2e39;
|
|
|
|
|
|
border-color: #363c4e;
|
|
|
|
|
|
.idx-symbol { color: #868993; }
|
|
|
|
|
|
.idx-price { color: #d1d4dc; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.refresh-btn {
|
|
|
|
|
|
color: #868993;
|
|
|
|
|
|
&:hover { color: #d1d4dc; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.watchlist-panel {
|
|
|
|
|
|
background: #2a2e39;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
border-color: #363c4e;
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.panel-header {
|
|
|
|
|
|
background: #1e222d;
|
|
|
|
|
|
border-bottom-color: #363c4e;
|
|
|
|
|
|
.panel-title { color: #868993; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.watchlist-list {
|
|
|
|
|
|
.watchlist-item {
|
|
|
|
|
|
&:hover { background: #363c4e; }
|
|
|
|
|
|
&.active { background: rgba(24, 144, 255, 0.1); border-color: #1890ff; }
|
|
|
|
|
|
.item-main {
|
|
|
|
|
|
.item-symbol { color: #d1d4dc; }
|
|
|
|
|
|
.item-name { color: #64748b; }
|
|
|
|
|
|
}
|
|
|
|
|
|
.item-price .price-value { color: #d1d4dc; }
|
|
|
|
|
|
}
|
|
|
|
|
|
.watchlist-empty { color: #64748b; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.watchlist-bar-legacy {
|
|
|
|
|
|
background: #1e222d;
|
|
|
|
|
|
border-bottom-color: #363c4e;
|
|
|
|
|
|
|
|
|
|
|
|
.stock-chip {
|
|
|
|
|
|
background: #2a2e39;
|
|
|
|
|
|
border-color: #363c4e;
|
|
|
|
|
|
&:hover, &.active { border-color: var(--primary-color, #1890ff); background: rgba(24, 144, 255, 0.1); }
|
|
|
|
|
|
.chip-symbol { color: #d1d4dc; }
|
|
|
|
|
|
.chip-price { color: #868993; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.left-panel {
|
|
|
|
|
|
.heatmap-box {
|
|
|
|
|
|
background: #2a2e39;
|
|
|
|
|
|
border-color: #363c4e;
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .ant-radio-group .ant-radio-button-wrapper {
|
|
|
|
|
|
background: #1e222d;
|
|
|
|
|
|
border-color: #363c4e;
|
|
|
|
|
|
color: #868993;
|
|
|
|
|
|
&:hover { color: #d1d4dc; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.calendar-box {
|
|
|
|
|
|
background: #2a2e39;
|
|
|
|
|
|
border-color: #363c4e;
|
|
|
|
|
|
|
|
|
|
|
|
.box-title { color: #868993; }
|
|
|
|
|
|
.cal-item {
|
|
|
|
|
|
border-bottom-color: #363c4e;
|
|
|
|
|
|
.cal-date { color: #64748b; }
|
|
|
|
|
|
.cal-time { color: #868993; }
|
|
|
|
|
|
.cal-name { color: #d1d4dc; }
|
|
|
|
|
|
}
|
|
|
|
|
|
.cal-empty { color: #64748b; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.right-panel {
|
|
|
|
|
|
background: #2a2e39;
|
|
|
|
|
|
border-color: #363c4e;
|
|
|
|
|
|
|
|
|
|
|
|
.analysis-toolbar {
|
|
|
|
|
|
background: #1e222d;
|
|
|
|
|
|
border-bottom-color: #363c4e;
|
|
|
|
|
|
|
|
|
|
|
|
.history-button {
|
|
|
|
|
|
background: #2a2e39;
|
|
|
|
|
|
border-color: #363c4e;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
color: #d1d4dc;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.analysis-main {
|
|
|
|
|
|
.analysis-placeholder .placeholder-content {
|
|
|
|
|
|
h3 { color: #d1d4dc; }
|
|
|
|
|
|
p { color: #868993; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
// 旧样式兼容
|
|
|
|
|
|
.watchlist-bar-compat {
|
|
|
|
|
|
background: #1e222d;
|
|
|
|
|
|
border-top-color: #363c4e;
|
|
|
|
|
|
|
|
|
|
|
|
.bar-label { color: #868993; }
|
|
|
|
|
|
|
|
|
|
|
|
.stock-chip {
|
|
|
|
|
|
background: #2a2e39;
|
|
|
|
|
|
border-color: #363c4e;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover, &.active {
|
|
|
|
|
|
border-color: var(--primary-color, #1890ff);
|
|
|
|
|
|
background: rgba(24, 144, 255, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chip-symbol { color: #d1d4dc; }
|
|
|
|
|
|
.chip-price { color: #868993; }
|
|
|
|
|
|
.chip-remove { color: #64748b; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .symbol-selector {
|
|
|
|
|
|
.ant-select-selection {
|
|
|
|
|
|
background-color: #2a2e39;
|
|
|
|
|
|
border-color: #363c4e;
|
|
|
|
|
|
color: #d1d4dc;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
/* Add Stock Modal */
|
2025-12-29 03:06:49 +08:00
|
|
|
|
.add-stock-modal-content {
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.market-tabs { margin-bottom: 16px; }
|
|
|
|
|
|
.symbol-search-section { margin-bottom: 24px; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
.search-results-section,
|
|
|
|
|
|
.hot-symbols-section {
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #262626;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.symbol-list {
|
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
border: 1px solid #e8e8e8;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
.symbol-list-item {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
transition: background-color 0.3s;
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
&:hover { background-color: #f5f5f5; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
|
|
|
|
|
.symbol-item-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
.symbol-code {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #262626;
|
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.symbol-name {
|
|
|
|
|
|
color: #595959;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selected-symbol-section {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
.selected-symbol-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
/* 骨架屏加载动画 - 渐进式加载 */
|
|
|
|
|
|
.skeleton-box {
|
|
|
|
|
|
.skeleton-text {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
height: 12px;
|
|
|
|
|
|
background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
|
|
|
|
|
|
background-size: 200% 100%;
|
|
|
|
|
|
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin: 3px 0;
|
2025-12-29 03:06:49 +08:00
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
&.short { width: 40px; height: 9px; }
|
2025-12-29 03:06:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 02:59:49 +08:00
|
|
|
|
.skeleton-cell {
|
|
|
|
|
|
background: #f8fafc !important;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 8px 4px;
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-text {
|
|
|
|
|
|
width: 80%;
|
|
|
|
|
|
height: 10px;
|
|
|
|
|
|
background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
|
|
|
|
|
|
background-size: 200% 100%;
|
|
|
|
|
|
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
margin: 2px 0;
|
|
|
|
|
|
|
|
|
|
|
|
&.short { width: 50%; height: 8px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-text {
|
|
|
|
|
|
height: 12px;
|
|
|
|
|
|
background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
|
|
|
|
|
|
background-size: 200% 100%;
|
|
|
|
|
|
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
|
|
|
|
&.short { flex: none; width: 40px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.indices-loading, .indices-empty {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
padding: 4px 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.indices-loading {
|
|
|
|
|
|
.anticon { margin-right: 6px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.heatmap-empty {
|
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes skeleton-pulse {
|
|
|
|
|
|
0% { background-position: 200% 0; }
|
|
|
|
|
|
100% { background-position: -200% 0; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 暗色主题下的骨架屏 */
|
|
|
|
|
|
.theme-dark {
|
|
|
|
|
|
.skeleton-box, .skeleton-cell, .skeleton-item {
|
|
|
|
|
|
.skeleton-text {
|
|
|
|
|
|
background: linear-gradient(90deg, #363c4e 25%, #424857 50%, #363c4e 75%);
|
|
|
|
|
|
background-size: 200% 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-cell {
|
|
|
|
|
|
background: #2a2e39 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.indices-loading, .indices-empty, .heatmap-empty {
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-29 03:06:49 +08:00
|
|
|
|
</style>
|