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:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user