Improve global market dashboard UX and settings

- Add global market dashboard APIs/assets and improve data robustness (incl. crypto heatmap by market cap)

- Enhance global market UI (map+heatmap layout, loading behavior, formatting, theme tweaks)

- Fix Settings LLM Provider select to render label/value options correctly

- Rename Indicator Community to Official Community and move it to the bottom

- Add search fallback when Google quota is exhausted
This commit is contained in:
TIANHE
2026-01-24 23:26:26 +08:00
parent f4e5a9f8e0
commit 35d7ac5e1b
15 changed files with 4744 additions and 75 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+79
View File
@@ -0,0 +1,79 @@
/**
* Global Market Dashboard API
*/
import request from '@/utils/request'
const BASE_URL = '/api/global-market'
/**
* Get global market overview (indices, forex, crypto, commodities)
* Includes geo coordinates for world map display
*/
export function getMarketOverview () {
return request({
url: `${BASE_URL}/overview`,
method: 'get'
})
}
/**
* Get market heatmap data (crypto, stock sectors, forex)
*/
export function getMarketHeatmap () {
return request({
url: `${BASE_URL}/heatmap`,
method: 'get'
})
}
/**
* Get financial news - separated by language (cn/en)
* @param {string} lang - Language filter: 'cn', 'en', or 'all' (default)
*/
export function getMarketNews (lang = 'all') {
return request({
url: `${BASE_URL}/news`,
method: 'get',
params: { lang }
})
}
/**
* Get economic calendar with impact indicators
*/
export function getEconomicCalendar () {
return request({
url: `${BASE_URL}/calendar`,
method: 'get'
})
}
/**
* Get market sentiment (Fear & Greed Index, VIX)
*/
export function getMarketSentiment () {
return request({
url: `${BASE_URL}/sentiment`,
method: 'get'
})
}
/**
* Get trading opportunities based on technical analysis
*/
export function getTradingOpportunities () {
return request({
url: `${BASE_URL}/opportunities`,
method: 'get'
})
}
/**
* Force refresh all market data (clears cache)
*/
export function refreshMarketData () {
return request({
url: `${BASE_URL}/refresh`,
method: 'post'
})
}
+14 -7
View File
@@ -16,6 +16,13 @@ export const asyncRouterMap = [
component: () => import('@/views/dashboard'),
meta: { title: 'menu.dashboard', keepAlive: true, icon: 'dashboard', permission: ['dashboard'] }
},
// 全球金融面板
{
path: '/global-market',
name: 'GlobalMarket',
component: () => import('@/views/global-market'),
meta: { title: 'menu.dashboard.globalMarket', keepAlive: true, icon: 'global', permission: ['dashboard'] }
},
// AI 分析
{
path: '/ai-analysis/:pageNo([1-9]\\d*)?',
@@ -44,13 +51,6 @@ export const asyncRouterMap = [
component: () => import('@/views/portfolio'),
meta: { title: 'menu.dashboard.portfolio', keepAlive: true, icon: 'fund', permission: ['dashboard'] }
},
// 指标社区(keepAlive disabled intentionally for iframe page)
{
path: '/indicator-community',
name: 'IndicatorCommunity',
component: () => import('@/views/indicator-community'),
meta: { title: 'menu.dashboard.community', keepAlive: false, icon: 'shop', permission: ['dashboard'] }
},
// 系统设置 (admin only)
{
path: '/settings',
@@ -71,6 +71,13 @@ export const asyncRouterMap = [
name: 'Profile',
component: () => import('@/views/profile'),
meta: { title: 'menu.myProfile', keepAlive: false, icon: 'user', permission: ['dashboard'] }
},
// 官方社区(keepAlive disabled intentionally for iframe page
{
path: '/indicator-community',
name: 'IndicatorCommunity',
component: () => import('@/views/indicator-community'),
meta: { title: 'menu.dashboard.community', keepAlive: false, icon: 'shop', permission: ['dashboard'] }
}
// other
+62 -2
View File
@@ -26,9 +26,10 @@ const locale = {
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'AI Analysis',
'menu.dashboard.indicator': 'Indicator Analysis',
'menu.dashboard.community': 'Indicator Community',
'menu.dashboard.community': 'Official Community',
'menu.dashboard.tradingAssistant': 'Trading Assistant',
'menu.dashboard.portfolio': 'Portfolio',
'menu.dashboard.globalMarket': 'Global Market',
'menu.settings': 'Settings',
'menu.dashboard.aiTradingAssistant': 'AI Trading Assistant',
'menu.dashboard.signalRobot': 'Signal Robot',
@@ -2353,6 +2354,8 @@ const locale = {
'portfolio.form.deselectAll': 'Deselect All',
'portfolio.form.selectedCount': '{count} of {total} selected',
'portfolio.form.pleaseSelectPositions': 'Please select at least one position to monitor',
'portfolio.form.notificationFromProfile': 'Notifications will be sent to addresses configured in your profile',
'portfolio.form.goToProfile': 'Go to settings',
'portfolio.message.loadFailed': 'Failed to load data',
'portfolio.message.saveSuccess': 'Saved successfully',
'portfolio.message.saveFailed': 'Failed to save',
@@ -2581,7 +2584,64 @@ const locale = {
'settings.desc.CREDITS_REFERRAL_BONUS': 'Credits awarded to referrer when someone signs up with their referral code',
'settings.desc.VERIFICATION_CODE_MAX_ATTEMPTS': 'Maximum attempts to verify a code before lockout',
'settings.desc.VERIFICATION_CODE_LOCK_MINUTES': 'Lockout duration after exceeding max attempts',
'settings.desc.RECHARGE_TELEGRAM_URL': 'Telegram customer service URL for recharge inquiries'
'settings.desc.RECHARGE_TELEGRAM_URL': 'Telegram customer service URL for recharge inquiries',
// Global Market
'globalMarket.title': 'Global Market Dashboard',
'globalMarket.lastUpdate': 'Last Update',
'globalMarket.refresh': 'Refresh',
'globalMarket.name': 'Name',
'globalMarket.price': 'Price',
'globalMarket.change': 'Change',
'globalMarket.trend': 'Trend',
'globalMarket.pair': 'Pair',
'globalMarket.unit': 'Unit',
'globalMarket.refreshSuccess': 'Data refreshed successfully',
'globalMarket.refreshError': 'Failed to refresh data',
'globalMarket.fetchError': 'Failed to fetch data',
'globalMarket.loading': 'Loading...',
'globalMarket.fearGreedIndex': 'Fear & Greed Index',
'globalMarket.fearGreedTip': 'Fear & Greed Index measures market sentiment, 0 = Extreme Fear, 100 = Extreme Greed',
'globalMarket.volatilityIndex': 'Volatility Index',
'globalMarket.vixTitle': 'VIX Volatility Index',
'globalMarket.vixTip': 'VIX measures expected market volatility, higher values indicate more market fear',
'globalMarket.extremeFear': 'Extreme Fear',
'globalMarket.fear': 'Fear',
'globalMarket.neutral': 'Neutral',
'globalMarket.greed': 'Greed',
'globalMarket.extremeGreed': 'Extreme Greed',
'globalMarket.marketOverview': 'Market Overview',
'globalMarket.indices': 'Indices',
'globalMarket.forex': 'Forex',
'globalMarket.crypto': 'Crypto',
'globalMarket.commodities': 'Commodities',
'globalMarket.heatmap': 'Market Heatmap',
'globalMarket.cryptoHeatmap': 'Crypto Heatmap',
'globalMarket.sectorHeatmap': 'Sector Heatmap',
'globalMarket.forexHeatmap': 'Forex Heatmap',
'globalMarket.opportunities': 'Trading Opportunities',
'globalMarket.noOpportunities': 'No significant opportunities detected',
'globalMarket.financialNews': 'Financial News',
'globalMarket.noNews': 'No news available',
'globalMarket.economicCalendar': 'Economic Calendar',
'globalMarket.noEvents': 'No events scheduled',
'globalMarket.actual': 'Actual',
'globalMarket.forecast': 'Forecast',
'globalMarket.previous': 'Previous',
'globalMarket.signal.bullish': 'Bullish Momentum',
'globalMarket.signal.bearish': 'Bearish Momentum',
'globalMarket.signal.overbought': 'Overbought Warning',
'globalMarket.signal.oversold': 'Oversold Opportunity',
'globalMarket.worldMap': 'Global Markets Map',
'globalMarket.rising': 'Rising',
'globalMarket.falling': 'Falling',
'globalMarket.bullish': 'Bullish',
'globalMarket.bearish': 'Bearish',
'globalMarket.expectedImpact': 'Expected Impact',
'globalMarket.aboveForecast': 'Above Forecast',
'globalMarket.belowForecast': 'Below Forecast',
'globalMarket.upcomingEvents': 'Upcoming Events',
'globalMarket.releasedEvents': 'Released Data'
}
export default {
+62 -2
View File
@@ -25,10 +25,11 @@ const locale = {
'menu.home': '主页',
'menu.dashboard': '仪表盘',
'menu.dashboard.indicator': '指标分析',
'menu.dashboard.community': '指标社区',
'menu.dashboard.community': '官方社区',
'menu.dashboard.analysis': 'AI 分析',
'menu.dashboard.tradingAssistant': '交易助手',
'menu.dashboard.portfolio': '资产监测',
'menu.dashboard.globalMarket': '全球金融',
'menu.settings': '系统设置',
'menu.dashboard.aiTradingAssistant': 'AI交易助手',
'menu.dashboard.signalRobot': '信号机器人',
@@ -2163,6 +2164,8 @@ const locale = {
'portfolio.form.deselectAll': '全不选',
'portfolio.form.selectedCount': '已选 {count}/{total}',
'portfolio.form.pleaseSelectPositions': '请至少选择一个持仓进行监控',
'portfolio.form.notificationFromProfile': '通知将发送到您在个人中心配置的地址',
'portfolio.form.goToProfile': '前往配置',
'portfolio.message.loadFailed': '加载数据失败',
'portfolio.message.saveSuccess': '保存成功',
'portfolio.message.saveFailed': '保存失败',
@@ -2391,7 +2394,64 @@ const locale = {
'settings.desc.CREDITS_REFERRAL_BONUS': '用户通过邀请链接成功邀请新用户时,邀请人获得的积分奖励',
'settings.desc.VERIFICATION_CODE_MAX_ATTEMPTS': '验证码验证失败的最大尝试次数,超过后将锁定',
'settings.desc.VERIFICATION_CODE_LOCK_MINUTES': '验证码验证失败次数超过限制后的锁定时长(分钟)',
'settings.desc.RECHARGE_TELEGRAM_URL': '用户点击充值时跳转的Telegram客服链接'
'settings.desc.RECHARGE_TELEGRAM_URL': '用户点击充值时跳转的Telegram客服链接',
// Global Market
'globalMarket.title': '全球金融面板',
'globalMarket.lastUpdate': '最后更新',
'globalMarket.refresh': '刷新数据',
'globalMarket.name': '名称',
'globalMarket.price': '价格',
'globalMarket.change': '涨跌',
'globalMarket.trend': '走势',
'globalMarket.pair': '货币对',
'globalMarket.unit': '单位',
'globalMarket.refreshSuccess': '数据刷新成功',
'globalMarket.refreshError': '数据刷新失败',
'globalMarket.fetchError': '获取数据失败',
'globalMarket.loading': '加载中...',
'globalMarket.fearGreedIndex': '恐惧贪婪指数',
'globalMarket.fearGreedTip': '恐惧贪婪指数衡量市场情绪,0表示极度恐惧,100表示极度贪婪',
'globalMarket.volatilityIndex': '波动率指数',
'globalMarket.vixTitle': 'VIX 波动率指数',
'globalMarket.vixTip': 'VIX指数衡量市场预期波动率,数值越高表示市场越恐慌',
'globalMarket.extremeFear': '极度恐惧',
'globalMarket.fear': '恐惧',
'globalMarket.neutral': '中性',
'globalMarket.greed': '贪婪',
'globalMarket.extremeGreed': '极度贪婪',
'globalMarket.marketOverview': '全球市场概览',
'globalMarket.indices': '主要指数',
'globalMarket.forex': '外汇市场',
'globalMarket.crypto': '加密货币',
'globalMarket.commodities': '大宗商品',
'globalMarket.heatmap': '市场热力图',
'globalMarket.cryptoHeatmap': '加密货币热力图',
'globalMarket.sectorHeatmap': '板块热力图',
'globalMarket.forexHeatmap': '外汇热力图',
'globalMarket.opportunities': '交易机会',
'globalMarket.noOpportunities': '暂无明显交易机会',
'globalMarket.financialNews': '财经资讯',
'globalMarket.noNews': '暂无新闻',
'globalMarket.economicCalendar': '财经日历',
'globalMarket.noEvents': '暂无事件',
'globalMarket.actual': '实际',
'globalMarket.forecast': '预期',
'globalMarket.previous': '前值',
'globalMarket.signal.bullish': '看涨趋势',
'globalMarket.signal.bearish': '看跌趋势',
'globalMarket.signal.overbought': '超买警告',
'globalMarket.signal.oversold': '超卖机会',
'globalMarket.worldMap': '全球市场地图',
'globalMarket.rising': '上涨',
'globalMarket.falling': '下跌',
'globalMarket.bullish': '利多',
'globalMarket.bearish': '利空',
'globalMarket.expectedImpact': '预期影响',
'globalMarket.aboveForecast': '高于预期',
'globalMarket.belowForecast': '低于预期',
'globalMarket.upcomingEvents': '即将公布',
'globalMarket.releasedEvents': '已公布数据'
}
export default {
File diff suppressed because it is too large Load Diff
+67 -31
View File
@@ -727,9 +727,9 @@
>
<template #message>
<span>
{{ $t('portfolio.form.notificationFromProfile') || '通知将发送到您在个人中心配置的地址' }}
{{ $t('portfolio.form.notificationFromProfile') }}
<router-link to="/profile" style="margin-left: 8px">
<a-icon type="setting" /> {{ $t('portfolio.form.goToProfile') || '前往配置' }}
<a-icon type="setting" /> {{ $t('portfolio.form.goToProfile') }}
</router-link>
</span>
</template>
@@ -806,9 +806,9 @@
>
<template #message>
<span>
{{ $t('portfolio.form.notificationFromProfile') || '通知将发送到您在个人中心配置的地址' }}
{{ $t('portfolio.form.notificationFromProfile') }}
<router-link to="/profile" style="margin-left: 8px">
<a-icon type="setting" /> {{ $t('portfolio.form.goToProfile') || '前往配置' }}
<a-icon type="setting" /> {{ $t('portfolio.form.goToProfile') }}
</router-link>
</span>
</template>
@@ -836,15 +836,21 @@
:key="pos.id"
class="position-checkbox-item"
>
<a-checkbox :value="pos.id">
<span class="position-checkbox-label">
<a-tag :color="getMarketColor(pos.market)" size="small">{{ pos.market }}</a-tag>
<span class="symbol">{{ pos.symbol }}</span>
<span class="name">{{ pos.name }}</span>
<span :class="['pnl', pos.pnl >= 0 ? 'positive' : 'negative']">
{{ pos.pnl >= 0 ? '+' : '' }}{{ formatNumber(pos.pnl_percent) }}%
</span>
</span>
<a-checkbox :value="pos.id" class="position-checkbox">
<div class="position-checkbox-label">
<div class="position-left">
<a-tag :color="getMarketColor(pos.market)" size="small">{{ pos.market }}</a-tag>
<span class="symbol">{{ pos.symbol }}</span>
</div>
<div class="position-middle">
<span class="name" :title="pos.name">{{ pos.name }}</span>
</div>
<div class="position-right">
<span :class="['pnl', pos.pnl >= 0 ? 'positive' : 'negative']">
{{ pos.pnl >= 0 ? '+' : '' }}{{ formatNumber(pos.pnl_percent) }}%
</span>
</div>
</div>
</a-checkbox>
</div>
</a-checkbox-group>
@@ -2615,34 +2621,64 @@ export default {
}
}
.position-checkbox {
width: 100%;
// Override Ant Design checkbox label width
::v-deep .ant-checkbox + span {
width: calc(100% - 24px);
padding-left: 8px;
padding-right: 0;
}
}
.position-checkbox-label {
display: inline-flex;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
width: 100%;
.symbol {
font-weight: 600;
color: #262626;
min-width: 60px;
.position-left {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
.symbol {
font-weight: 600;
color: #262626;
min-width: 50px;
}
}
.name {
color: #8c8c8c;
font-size: 12px;
.position-middle {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px;
.name {
color: #8c8c8c;
font-size: 12px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.pnl {
font-weight: 500;
font-size: 12px;
.position-right {
flex-shrink: 0;
margin-left: auto;
&.positive { color: @green; }
&.negative { color: @red; }
.pnl {
font-weight: 500;
font-size: 12px;
white-space: nowrap;
&.positive { color: @green; }
&.negative { color: @red; }
}
}
}
@@ -2676,8 +2712,8 @@ export default {
}
.position-checkbox-label {
.symbol { color: #d1d4dc; }
.name { color: #868993; }
.position-left .symbol { color: #d1d4dc; }
.position-middle .name { color: #868993; }
}
.position-select-actions {
+21 -2
View File
@@ -171,8 +171,12 @@
v-decorator="[item.key, { initialValue: getFieldValue(groupKey, item.key) || item.default }]"
:placeholder="item.default ? `${$t('settings.default')}: ${item.default}` : $t('settings.pleaseSelect')"
>
<a-select-option v-for="opt in item.options" :key="opt" :value="opt">
{{ opt }}
<a-select-option
v-for="opt in getSelectOptions(item.options)"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-select-option>
</a-select>
</template>
@@ -249,6 +253,21 @@ export default {
this.loadSettings()
},
methods: {
// schema options
// - string[]: ['openrouter','openai', ...]
// - {value,label}[]: [{value:'openrouter',label:'OpenRouter'}, ...]
getSelectOptions (options) {
const arr = Array.isArray(options) ? options : []
return arr.map(opt => {
if (opt && typeof opt === 'object') {
return {
value: opt.value != null ? String(opt.value) : '',
label: opt.label != null ? String(opt.label) : String(opt.value || '')
}
}
return { value: String(opt), label: String(opt) }
}).filter(o => o.value !== '')
},
async loadSettings () {
this.loading = true
try {