@@ -0,0 +1,46 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取配置项定义
|
||||
*/
|
||||
export function getSettingsSchema () {
|
||||
return request({
|
||||
url: '/api/settings/schema',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置值
|
||||
*/
|
||||
export function getSettingsValues () {
|
||||
return request({
|
||||
url: '/api/settings/values',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置
|
||||
* @param {Object} data - 配置数据
|
||||
*/
|
||||
export function saveSettings (data) {
|
||||
return request({
|
||||
url: '/api/settings/save',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试API连接
|
||||
* @param {string} service - 服务名称 (openrouter, finnhub, etc.)
|
||||
* @param {Object} params - 额外参数
|
||||
*/
|
||||
export function testConnection (service, params = {}) {
|
||||
return request({
|
||||
url: '/api/settings/test-connection',
|
||||
method: 'post',
|
||||
data: { service, ...params }
|
||||
})
|
||||
}
|
||||
@@ -43,6 +43,13 @@ export const asyncRouterMap = [
|
||||
name: 'IndicatorCommunity',
|
||||
component: () => import('@/views/indicator-community'),
|
||||
meta: { title: 'menu.dashboard.community', keepAlive: false, icon: 'shop', permission: ['dashboard'] }
|
||||
},
|
||||
// 系统设置
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: () => import('@/views/settings'),
|
||||
meta: { title: 'menu.settings', keepAlive: false, icon: 'setting', permission: ['dashboard'] }
|
||||
}
|
||||
|
||||
// other
|
||||
|
||||
@@ -200,17 +200,17 @@ export default {
|
||||
// Static footer config (local OSS build)
|
||||
menuFooterConfig: {
|
||||
contact: {
|
||||
support_url: 'https://github.com/',
|
||||
feature_request_url: 'https://github.com/',
|
||||
email: 'support@example.com',
|
||||
live_chat_url: 'https://github.com/'
|
||||
support_url: 'https://t.me/worldinbroker',
|
||||
feature_request_url: 'https://github.com/brokermr810/QuantDinger/issues',
|
||||
email: 'brokermr810@gmail.com',
|
||||
live_chat_url: 'https://t.me/worldinbroker'
|
||||
},
|
||||
social_accounts: [
|
||||
{ name: 'GitHub', icon: 'github', url: 'https://github.com/' },
|
||||
{ name: 'X', icon: 'x', url: 'https://x.com/' },
|
||||
{ name: 'Discord', icon: 'discord', url: 'https://discord.com/' },
|
||||
{ name: 'Telegram', icon: 'telegram', url: 'https://t.me/' },
|
||||
{ name: 'YouTube', icon: 'youtube', url: 'https://www.youtube.com/' }
|
||||
{ name: 'GitHub', icon: 'github', url: 'https://github.com/brokermr810/QuantDinger' },
|
||||
{ name: 'X', icon: 'x', url: 'https://x.com/HenryCryption' },
|
||||
{ name: 'Discord', icon: 'discord', url: 'https://discord.gg/cn6HVE2KC' },
|
||||
{ name: 'Telegram', icon: 'telegram', url: 'https://t.me/worldinbroker' },
|
||||
{ name: 'YouTube', icon: 'youtube', url: 'https://youtube.com/@quantdinger' }
|
||||
],
|
||||
legal: {
|
||||
user_agreement: '',
|
||||
|
||||
@@ -19,6 +19,7 @@ const locale = {
|
||||
'menu.dashboard.indicator': 'Indicator Analysis',
|
||||
'menu.dashboard.community': 'Indicator Community',
|
||||
'menu.dashboard.tradingAssistant': 'Trading Assistant',
|
||||
'menu.settings': 'Settings',
|
||||
'menu.dashboard.aiTradingAssistant': 'AI Trading Assistant',
|
||||
'menu.dashboard.signalRobot': 'Signal Robot',
|
||||
'menu.dashboard.monitor': 'Monitor',
|
||||
@@ -937,6 +938,41 @@ const locale = {
|
||||
'dashboard.enabled': 'Enabled',
|
||||
'dashboard.pnlHistory': 'P&L History',
|
||||
'dashboard.strategyPerformance': 'Strategy Performance',
|
||||
'dashboard.strategyRanking': 'Strategy Ranking',
|
||||
// New KPI labels
|
||||
'dashboard.winRate': 'Win Rate',
|
||||
'dashboard.profitFactor': 'Profit Factor',
|
||||
'dashboard.maxDrawdown': 'Max Drawdown',
|
||||
'dashboard.totalTrades': 'Total Trades',
|
||||
'dashboard.runningStrategies': 'Running Strategies',
|
||||
'dashboard.equityCurve': 'Equity Curve',
|
||||
'dashboard.strategyAllocation': 'Strategy Allocation',
|
||||
'dashboard.drawdownCurve': 'Drawdown Curve',
|
||||
'dashboard.drawdown': 'Drawdown',
|
||||
'dashboard.hourlyDistribution': 'Hourly Distribution',
|
||||
// Chart labels
|
||||
'dashboard.dailyPnl': 'Daily P&L',
|
||||
'dashboard.cumulativePnl': 'Cumulative P&L',
|
||||
'dashboard.tradeCount': 'Trade Count',
|
||||
'dashboard.profit': 'Profit',
|
||||
'dashboard.noData': 'No Data',
|
||||
'dashboard.noStrategyData': 'No strategy data',
|
||||
// Ranking labels
|
||||
'dashboard.ranking.totalProfit': 'Total Profit',
|
||||
'dashboard.ranking.roi': 'ROI',
|
||||
'dashboard.ranking.trades': 'Trades',
|
||||
// Units and labels
|
||||
'dashboard.unit.trades': '',
|
||||
'dashboard.unit.strategies': '',
|
||||
'dashboard.label.avgDaily': 'Avg Daily',
|
||||
'dashboard.label.avgProfit': 'Avg Profit',
|
||||
'dashboard.label.win': 'W',
|
||||
'dashboard.label.lose': 'L',
|
||||
'dashboard.label.trade': 'Trades',
|
||||
'dashboard.label.indicator': 'Indicator',
|
||||
'dashboard.label.totalPnl': 'Total P&L',
|
||||
'dashboard.label.maxDrawdownPoint': 'Max DD',
|
||||
'dashboard.profitCalendar': 'Profit Calendar',
|
||||
'dashboard.recentTrades': 'Recent Trades',
|
||||
'dashboard.currentPositions': 'Current Positions',
|
||||
'dashboard.table.time': 'Time',
|
||||
@@ -969,6 +1005,12 @@ const locale = {
|
||||
'dashboard.orderTable.deleteConfirm': 'Delete this record?',
|
||||
'dashboard.orderTable.deleteSuccess': 'Deleted',
|
||||
'dashboard.orderTable.deleteFailed': 'Delete failed',
|
||||
'dashboard.newOrderNotify': 'New Order Alert',
|
||||
'dashboard.newOrderDesc': 'New order execution record',
|
||||
'dashboard.soundEnabled': 'Sound notification enabled',
|
||||
'dashboard.soundDisabled': 'Sound notification disabled',
|
||||
'dashboard.clickToMute': 'Click to mute',
|
||||
'dashboard.clickToUnmute': 'Click to unmute',
|
||||
'dashboard.signalType.openLong': 'Open Long',
|
||||
'dashboard.signalType.openShort': 'Open Short',
|
||||
'dashboard.signalType.closeLong': 'Close Long',
|
||||
@@ -1817,7 +1859,124 @@ const locale = {
|
||||
'signal-robot.indicator.volume': 'Volume',
|
||||
'signal-robot.indicator.changePct1h': '1H Change %',
|
||||
'signal-robot.indicator.changePct24h': '24H Change %',
|
||||
'signal-robot.indicator.signalLine': 'Signal Line'
|
||||
'signal-robot.indicator.signalLine': 'Signal Line',
|
||||
|
||||
// Settings
|
||||
'settings.title': 'System Settings',
|
||||
'settings.description': 'Configure application settings, API keys, and system preferences',
|
||||
'settings.save': 'Save Settings',
|
||||
'settings.reset': 'Reset',
|
||||
'settings.saveSuccess': 'Settings saved successfully',
|
||||
'settings.saveFailed': 'Failed to save settings',
|
||||
'settings.loadFailed': 'Failed to load settings',
|
||||
'settings.default': 'Default',
|
||||
'settings.pleaseSelect': 'Please select',
|
||||
'settings.inputApiKey': 'Enter key',
|
||||
'settings.getApi': 'Get API',
|
||||
// Settings link texts
|
||||
'settings.link.getApi': 'Get API',
|
||||
'settings.link.getApiKey': 'Get API Key',
|
||||
'settings.link.getToken': 'Get Token',
|
||||
'settings.link.createBot': 'Create Bot',
|
||||
'settings.link.viewModels': 'View Models',
|
||||
'settings.link.freeRegister': 'Free Register',
|
||||
'settings.link.supportedExchanges': 'Supported Exchanges',
|
||||
'settings.link.applyApi': 'Apply API',
|
||||
'settings.link.createSearchEngine': 'Create Search Engine',
|
||||
'settings.restartRequired': 'Settings saved. Some changes require Python service restart to take effect.',
|
||||
'settings.copyRestartCmd': 'Copy restart command',
|
||||
'settings.copySuccess': 'Copied',
|
||||
'settings.copyFailed': 'Copy failed',
|
||||
// Settings groups
|
||||
'settings.group.auth': 'Authentication',
|
||||
'settings.group.server': 'Server Configuration',
|
||||
'settings.group.worker': 'Order Worker',
|
||||
'settings.group.notification': 'Signal Notification',
|
||||
'settings.group.smtp': 'Email SMTP',
|
||||
'settings.group.twilio': 'Twilio SMS',
|
||||
'settings.group.strategy': 'Strategy Execution',
|
||||
'settings.group.proxy': 'Proxy Configuration',
|
||||
'settings.group.app': 'Application',
|
||||
'settings.group.ai': 'AI/LLM Configuration',
|
||||
'settings.group.market': 'Market Presets',
|
||||
'settings.group.data_source': 'Data Sources',
|
||||
'settings.group.search': 'Search Configuration',
|
||||
// Settings fields - Auth
|
||||
'settings.field.SECRET_KEY': 'Secret Key',
|
||||
'settings.field.ADMIN_USER': 'Admin Username',
|
||||
'settings.field.ADMIN_PASSWORD': 'Admin Password',
|
||||
// Settings fields - Server
|
||||
'settings.field.PYTHON_API_HOST': 'Listen Address',
|
||||
'settings.field.PYTHON_API_PORT': 'Port',
|
||||
'settings.field.PYTHON_API_DEBUG': 'Debug Mode',
|
||||
// Settings fields - Worker
|
||||
'settings.field.ENABLE_PENDING_ORDER_WORKER': 'Enable Order Worker',
|
||||
'settings.field.PENDING_ORDER_STALE_SEC': 'Order Stale Timeout (sec)',
|
||||
// Settings fields - Notification
|
||||
'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL',
|
||||
'settings.field.SIGNAL_WEBHOOK_TOKEN': 'Webhook Token',
|
||||
'settings.field.SIGNAL_NOTIFY_TIMEOUT_SEC': 'Notify Timeout (sec)',
|
||||
'settings.field.TELEGRAM_BOT_TOKEN': 'Telegram Bot Token',
|
||||
// Settings fields - SMTP
|
||||
'settings.field.SMTP_HOST': 'SMTP Host',
|
||||
'settings.field.SMTP_PORT': 'SMTP Port',
|
||||
'settings.field.SMTP_USER': 'SMTP Username',
|
||||
'settings.field.SMTP_PASSWORD': 'SMTP Password',
|
||||
'settings.field.SMTP_FROM': 'From Address',
|
||||
'settings.field.SMTP_USE_TLS': 'Use TLS',
|
||||
'settings.field.SMTP_USE_SSL': 'Use SSL',
|
||||
// Settings fields - Twilio
|
||||
'settings.field.TWILIO_ACCOUNT_SID': 'Account SID',
|
||||
'settings.field.TWILIO_AUTH_TOKEN': 'Auth Token',
|
||||
'settings.field.TWILIO_FROM_NUMBER': 'From Number',
|
||||
// Settings fields - Strategy
|
||||
'settings.field.DISABLE_RESTORE_RUNNING_STRATEGIES': 'Disable Auto Restore',
|
||||
'settings.field.STRATEGY_TICK_INTERVAL_SEC': 'Tick Interval (sec)',
|
||||
'settings.field.PRICE_CACHE_TTL_SEC': 'Price Cache TTL (sec)',
|
||||
// Settings fields - Proxy
|
||||
'settings.field.PROXY_PORT': 'Proxy Port',
|
||||
'settings.field.PROXY_HOST': 'Proxy Host',
|
||||
'settings.field.PROXY_SCHEME': 'Proxy Scheme',
|
||||
'settings.field.PROXY_URL': 'Full Proxy URL',
|
||||
// Settings fields - App
|
||||
'settings.field.CORS_ORIGINS': 'CORS Origins',
|
||||
'settings.field.RATE_LIMIT': 'Rate Limit (per min)',
|
||||
'settings.field.ENABLE_CACHE': 'Enable Cache',
|
||||
'settings.field.ENABLE_REQUEST_LOG': 'Enable Request Log',
|
||||
'settings.field.ENABLE_AI_ANALYSIS': 'Enable AI Analysis',
|
||||
// Settings fields - AI
|
||||
'settings.field.OPENROUTER_API_KEY': 'OpenRouter API Key',
|
||||
'settings.field.OPENROUTER_API_URL': 'OpenRouter API URL',
|
||||
'settings.field.OPENROUTER_MODEL': 'Default Model',
|
||||
'settings.field.OPENROUTER_TEMPERATURE': 'Temperature',
|
||||
'settings.field.OPENROUTER_MAX_TOKENS': 'Max Tokens',
|
||||
'settings.field.OPENROUTER_TIMEOUT': 'Timeout (sec)',
|
||||
'settings.field.OPENROUTER_CONNECT_TIMEOUT': 'Connect Timeout (sec)',
|
||||
'settings.field.AI_MODELS_JSON': 'Models JSON',
|
||||
// Settings fields - Market
|
||||
'settings.field.MARKET_TYPES_JSON': 'Market Types JSON',
|
||||
'settings.field.TRADING_SUPPORTED_SYMBOLS_JSON': 'Supported Symbols JSON',
|
||||
// Settings fields - Data Source
|
||||
'settings.field.DATA_SOURCE_TIMEOUT': 'Timeout (sec)',
|
||||
'settings.field.DATA_SOURCE_RETRY': 'Retry Count',
|
||||
'settings.field.DATA_SOURCE_RETRY_BACKOFF': 'Retry Backoff (sec)',
|
||||
'settings.field.FINNHUB_API_KEY': 'Finnhub API Key',
|
||||
'settings.field.FINNHUB_TIMEOUT': 'Finnhub Timeout (sec)',
|
||||
'settings.field.FINNHUB_RATE_LIMIT': 'Finnhub Rate Limit',
|
||||
'settings.field.CCXT_DEFAULT_EXCHANGE': 'CCXT Default Exchange',
|
||||
'settings.field.CCXT_TIMEOUT': 'CCXT Timeout (ms)',
|
||||
'settings.field.CCXT_PROXY': 'CCXT Proxy',
|
||||
'settings.field.AKSHARE_TIMEOUT': 'Akshare Timeout (sec)',
|
||||
'settings.field.YFINANCE_TIMEOUT': 'YFinance Timeout (sec)',
|
||||
'settings.field.TIINGO_API_KEY': 'Tiingo API Key',
|
||||
'settings.field.TIINGO_TIMEOUT': 'Tiingo Timeout (sec)',
|
||||
// Settings fields - Search
|
||||
'settings.field.SEARCH_PROVIDER': 'Search Provider',
|
||||
'settings.field.SEARCH_MAX_RESULTS': 'Max Results',
|
||||
'settings.field.SEARCH_GOOGLE_API_KEY': 'Google API Key',
|
||||
'settings.field.SEARCH_GOOGLE_CX': 'Google CX',
|
||||
'settings.field.SEARCH_BING_API_KEY': 'Bing API Key',
|
||||
'settings.field.INTERNAL_API_KEY': 'Internal API Key'
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ const locale = {
|
||||
'menu.dashboard.community': '指標社區',
|
||||
'menu.dashboard.analysis': 'AI 分析',
|
||||
'menu.dashboard.tradingAssistant': '交易助手',
|
||||
'menu.settings': '系统设置',
|
||||
'menu.dashboard.aiTradingAssistant': 'AI交易助手',
|
||||
'menu.dashboard.signalRobot': '信號機器人',
|
||||
'menu.dashboard.monitor': '監控頁',
|
||||
@@ -846,6 +847,41 @@ const locale = {
|
||||
'dashboard.enabled': '已啟用',
|
||||
'dashboard.pnlHistory': '歷史盈虧',
|
||||
'dashboard.strategyPerformance': '策略盈虧佔比',
|
||||
'dashboard.drawdown': '回撤曲線',
|
||||
'dashboard.strategyRanking': '策略排行榜',
|
||||
// New KPI labels
|
||||
'dashboard.winRate': '勝率',
|
||||
'dashboard.profitFactor': '盈虧比',
|
||||
'dashboard.maxDrawdown': '最大回撤',
|
||||
'dashboard.totalTrades': '總交易',
|
||||
'dashboard.runningStrategies': '運行中策略',
|
||||
'dashboard.equityCurve': '權益曲線',
|
||||
'dashboard.strategyAllocation': '策略分佈',
|
||||
'dashboard.drawdownCurve': '回撤曲線',
|
||||
'dashboard.hourlyDistribution': '交易時段',
|
||||
// Chart labels
|
||||
'dashboard.dailyPnl': '日盈虧',
|
||||
'dashboard.cumulativePnl': '累計盈虧',
|
||||
'dashboard.tradeCount': '交易次數',
|
||||
'dashboard.profit': '盈虧',
|
||||
'dashboard.noData': '暫無數據',
|
||||
'dashboard.noStrategyData': '暫無策略數據',
|
||||
// Ranking labels
|
||||
'dashboard.ranking.totalProfit': '總收益',
|
||||
'dashboard.ranking.roi': '收益率',
|
||||
'dashboard.ranking.trades': '交易數',
|
||||
// Units and labels
|
||||
'dashboard.unit.trades': '筆',
|
||||
'dashboard.unit.strategies': '個',
|
||||
'dashboard.label.avgDaily': '日均',
|
||||
'dashboard.label.avgProfit': '平均盈利',
|
||||
'dashboard.label.win': '勝',
|
||||
'dashboard.label.lose': '負',
|
||||
'dashboard.label.trade': '交易',
|
||||
'dashboard.label.indicator': '指標',
|
||||
'dashboard.label.totalPnl': '總盈虧',
|
||||
'dashboard.label.maxDrawdownPoint': '最大回撤',
|
||||
'dashboard.profitCalendar': '收益日曆',
|
||||
'dashboard.recentTrades': '最近交易',
|
||||
'dashboard.currentPositions': '當前持倉',
|
||||
'dashboard.table.time': '時間',
|
||||
@@ -878,6 +914,12 @@ const locale = {
|
||||
'dashboard.orderTable.deleteConfirm': '確認刪除這條記錄?',
|
||||
'dashboard.orderTable.deleteSuccess': '刪除成功',
|
||||
'dashboard.orderTable.deleteFailed': '刪除失敗',
|
||||
'dashboard.newOrderNotify': '新訂單提醒',
|
||||
'dashboard.newOrderDesc': '有新的訂單執行記錄',
|
||||
'dashboard.soundEnabled': '聲音提醒已開啟',
|
||||
'dashboard.soundDisabled': '聲音提醒已關閉',
|
||||
'dashboard.clickToMute': '點擊關閉聲音提醒',
|
||||
'dashboard.clickToUnmute': '點擊開啟聲音提醒',
|
||||
'dashboard.signalType.openLong': '開多',
|
||||
'dashboard.signalType.openShort': '開空',
|
||||
'dashboard.signalType.closeLong': '平多',
|
||||
@@ -1521,7 +1563,124 @@ const locale = {
|
||||
'message': '-',
|
||||
'layouts.usermenu.dialog.title': '信息',
|
||||
'layouts.usermenu.dialog.content': '您確定要注銷嗎?',
|
||||
'layouts.userLayout.title': '於不確定中,尋見真理'
|
||||
'layouts.userLayout.title': '於不確定中,尋見真理',
|
||||
|
||||
// Settings
|
||||
'settings.title': '系統設置',
|
||||
'settings.description': '配置應用設置、API密鑰和系統偏好',
|
||||
'settings.save': '保存設置',
|
||||
'settings.reset': '重置',
|
||||
'settings.saveSuccess': '設置保存成功',
|
||||
'settings.saveFailed': '保存設置失敗',
|
||||
'settings.loadFailed': '加載配置失敗',
|
||||
'settings.default': '默認',
|
||||
'settings.pleaseSelect': '請選擇',
|
||||
'settings.inputApiKey': '請輸入密鑰',
|
||||
'settings.getApi': '獲取API',
|
||||
// Settings link texts
|
||||
'settings.link.getApi': '獲取API',
|
||||
'settings.link.getApiKey': '獲取API Key',
|
||||
'settings.link.getToken': '獲取Token',
|
||||
'settings.link.createBot': '創建Bot',
|
||||
'settings.link.viewModels': '查看模型列表',
|
||||
'settings.link.freeRegister': '免費註冊',
|
||||
'settings.link.supportedExchanges': '支持的交易所',
|
||||
'settings.link.applyApi': '申請API',
|
||||
'settings.link.createSearchEngine': '創建搜索引擎',
|
||||
'settings.restartRequired': '配置已保存,部分配置需要重啟Python服務才能生效',
|
||||
'settings.copyRestartCmd': '複製重啟命令',
|
||||
'settings.copySuccess': '複製成功',
|
||||
'settings.copyFailed': '複製失敗',
|
||||
// Settings groups
|
||||
'settings.group.auth': '認證配置',
|
||||
'settings.group.server': '服務器配置',
|
||||
'settings.group.worker': '訂單處理配置',
|
||||
'settings.group.notification': '信號通知配置',
|
||||
'settings.group.smtp': '郵件SMTP配置',
|
||||
'settings.group.twilio': 'Twilio短信配置',
|
||||
'settings.group.strategy': '策略執行配置',
|
||||
'settings.group.proxy': '代理配置',
|
||||
'settings.group.app': '應用配置',
|
||||
'settings.group.ai': 'AI/LLM配置',
|
||||
'settings.group.market': '市場預設',
|
||||
'settings.group.data_source': '數據源配置',
|
||||
'settings.group.search': '搜索配置',
|
||||
// Settings fields - Auth
|
||||
'settings.field.SECRET_KEY': 'Secret Key',
|
||||
'settings.field.ADMIN_USER': '管理員用戶名',
|
||||
'settings.field.ADMIN_PASSWORD': '管理員密碼',
|
||||
// Settings fields - Server
|
||||
'settings.field.PYTHON_API_HOST': '監聽地址',
|
||||
'settings.field.PYTHON_API_PORT': '端口',
|
||||
'settings.field.PYTHON_API_DEBUG': '調試模式',
|
||||
// Settings fields - Worker
|
||||
'settings.field.ENABLE_PENDING_ORDER_WORKER': '啟用訂單處理Worker',
|
||||
'settings.field.PENDING_ORDER_STALE_SEC': '訂單超時時間(秒)',
|
||||
// Settings fields - Notification
|
||||
'settings.field.SIGNAL_WEBHOOK_URL': 'Webhook URL',
|
||||
'settings.field.SIGNAL_WEBHOOK_TOKEN': 'Webhook Token',
|
||||
'settings.field.SIGNAL_NOTIFY_TIMEOUT_SEC': '通知超時(秒)',
|
||||
'settings.field.TELEGRAM_BOT_TOKEN': 'Telegram Bot Token',
|
||||
// Settings fields - SMTP
|
||||
'settings.field.SMTP_HOST': 'SMTP服務器',
|
||||
'settings.field.SMTP_PORT': 'SMTP端口',
|
||||
'settings.field.SMTP_USER': 'SMTP用戶名',
|
||||
'settings.field.SMTP_PASSWORD': 'SMTP密碼',
|
||||
'settings.field.SMTP_FROM': '發件人地址',
|
||||
'settings.field.SMTP_USE_TLS': '使用TLS',
|
||||
'settings.field.SMTP_USE_SSL': '使用SSL',
|
||||
// Settings fields - Twilio
|
||||
'settings.field.TWILIO_ACCOUNT_SID': 'Account SID',
|
||||
'settings.field.TWILIO_AUTH_TOKEN': 'Auth Token',
|
||||
'settings.field.TWILIO_FROM_NUMBER': '發送號碼',
|
||||
// Settings fields - Strategy
|
||||
'settings.field.DISABLE_RESTORE_RUNNING_STRATEGIES': '禁用自動恢復策略',
|
||||
'settings.field.STRATEGY_TICK_INTERVAL_SEC': '策略Tick間隔(秒)',
|
||||
'settings.field.PRICE_CACHE_TTL_SEC': '價格緩存TTL(秒)',
|
||||
// Settings fields - Proxy
|
||||
'settings.field.PROXY_PORT': '代理端口',
|
||||
'settings.field.PROXY_HOST': '代理主機',
|
||||
'settings.field.PROXY_SCHEME': '代理協議',
|
||||
'settings.field.PROXY_URL': '完整代理URL',
|
||||
// Settings fields - App
|
||||
'settings.field.CORS_ORIGINS': 'CORS來源',
|
||||
'settings.field.RATE_LIMIT': '速率限制(每分鐘)',
|
||||
'settings.field.ENABLE_CACHE': '啟用緩存',
|
||||
'settings.field.ENABLE_REQUEST_LOG': '啟用請求日誌',
|
||||
'settings.field.ENABLE_AI_ANALYSIS': '啟用AI分析',
|
||||
// Settings fields - AI
|
||||
'settings.field.OPENROUTER_API_KEY': 'OpenRouter API Key',
|
||||
'settings.field.OPENROUTER_API_URL': 'OpenRouter API URL',
|
||||
'settings.field.OPENROUTER_MODEL': '默認模型',
|
||||
'settings.field.OPENROUTER_TEMPERATURE': 'Temperature',
|
||||
'settings.field.OPENROUTER_MAX_TOKENS': 'Max Tokens',
|
||||
'settings.field.OPENROUTER_TIMEOUT': '超時時間(秒)',
|
||||
'settings.field.OPENROUTER_CONNECT_TIMEOUT': '連接超時(秒)',
|
||||
'settings.field.AI_MODELS_JSON': '模型列表(JSON)',
|
||||
// Settings fields - Market
|
||||
'settings.field.MARKET_TYPES_JSON': '市場類型(JSON)',
|
||||
'settings.field.TRADING_SUPPORTED_SYMBOLS_JSON': '支持的交易對(JSON)',
|
||||
// Settings fields - Data Source
|
||||
'settings.field.DATA_SOURCE_TIMEOUT': '數據源超時(秒)',
|
||||
'settings.field.DATA_SOURCE_RETRY': '重試次數',
|
||||
'settings.field.DATA_SOURCE_RETRY_BACKOFF': '重試退避(秒)',
|
||||
'settings.field.FINNHUB_API_KEY': 'Finnhub API Key',
|
||||
'settings.field.FINNHUB_TIMEOUT': 'Finnhub超時(秒)',
|
||||
'settings.field.FINNHUB_RATE_LIMIT': 'Finnhub速率限制',
|
||||
'settings.field.CCXT_DEFAULT_EXCHANGE': 'CCXT默認交易所',
|
||||
'settings.field.CCXT_TIMEOUT': 'CCXT超時(ms)',
|
||||
'settings.field.CCXT_PROXY': 'CCXT代理',
|
||||
'settings.field.AKSHARE_TIMEOUT': 'Akshare超時(秒)',
|
||||
'settings.field.YFINANCE_TIMEOUT': 'YFinance超時(秒)',
|
||||
'settings.field.TIINGO_API_KEY': 'Tiingo API Key',
|
||||
'settings.field.TIINGO_TIMEOUT': 'Tiingo超時(秒)',
|
||||
// Settings fields - Search
|
||||
'settings.field.SEARCH_PROVIDER': '搜索提供商',
|
||||
'settings.field.SEARCH_MAX_RESULTS': '最大結果數',
|
||||
'settings.field.SEARCH_GOOGLE_API_KEY': 'Google API Key',
|
||||
'settings.field.SEARCH_GOOGLE_CX': 'Google CX',
|
||||
'settings.field.SEARCH_BING_API_KEY': 'Bing API Key',
|
||||
'settings.field.INTERNAL_API_KEY': '內部API Key'
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,621 @@
|
||||
<template>
|
||||
<div class="settings-page" :class="{ 'theme-dark': isDarkTheme }">
|
||||
<!-- 重启提示 -->
|
||||
<a-alert
|
||||
v-if="showRestartTip"
|
||||
class="restart-alert"
|
||||
type="warning"
|
||||
showIcon
|
||||
closable
|
||||
@close="showRestartTip = false"
|
||||
>
|
||||
<template slot="message">
|
||||
<span>{{ $t('settings.restartRequired') }}</span>
|
||||
<a-button size="small" type="link" @click="copyRestartCommand">
|
||||
{{ $t('settings.copyRestartCmd') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-alert>
|
||||
|
||||
<div class="settings-header">
|
||||
<h2 class="page-title">
|
||||
<a-icon type="setting" />
|
||||
<span>{{ $t('settings.title') }}</span>
|
||||
</h2>
|
||||
<p class="page-desc">{{ $t('settings.description') }}</p>
|
||||
</div>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<div class="settings-content">
|
||||
<a-collapse v-model="activeKeys" :bordered="false" class="settings-collapse">
|
||||
<a-collapse-panel v-for="(group, groupKey) in schema" :key="groupKey">
|
||||
<template slot="header">
|
||||
<span class="panel-header">
|
||||
<span class="panel-title">{{ getGroupTitle(groupKey, group.title) }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="extra">
|
||||
<a-icon :type="getGroupIcon(groupKey)" class="panel-icon" />
|
||||
</template>
|
||||
|
||||
<a-form :form="form" layout="vertical" class="settings-form">
|
||||
<a-row :gutter="24">
|
||||
<a-col
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="12"
|
||||
:lg="12"
|
||||
v-for="item in group.items"
|
||||
:key="item.key">
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<span class="form-label-with-link">
|
||||
<span>{{ getItemLabel(groupKey, item) }}</span>
|
||||
<a
|
||||
v-if="item.link"
|
||||
:href="item.link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="api-link"
|
||||
@click.stop
|
||||
>
|
||||
<a-icon type="link" />
|
||||
{{ getLinkText(item.link_text) }}
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
<!-- 文本输入 -->
|
||||
<template v-if="item.type === 'text'">
|
||||
<a-input
|
||||
v-decorator="[item.key, { initialValue: getFieldValue(groupKey, item.key) }]"
|
||||
:placeholder="item.default ? `${$t('settings.default')}: ${item.default}` : ''"
|
||||
allowClear
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<template v-else-if="item.type === 'password'">
|
||||
<div class="password-field">
|
||||
<a-input
|
||||
v-decorator="[item.key, { initialValue: getFieldValue(groupKey, item.key) }]"
|
||||
:type="passwordVisible[item.key] ? 'text' : 'password'"
|
||||
:placeholder="$t('settings.inputApiKey')"
|
||||
allowClear
|
||||
>
|
||||
<a-icon
|
||||
slot="suffix"
|
||||
:type="passwordVisible[item.key] ? 'eye' : 'eye-invisible'"
|
||||
@click="togglePasswordVisible(item.key)"
|
||||
style="cursor: pointer"
|
||||
/>
|
||||
</a-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 数字输入 -->
|
||||
<template v-else-if="item.type === 'number'">
|
||||
<a-input-number
|
||||
v-decorator="[item.key, { initialValue: getNumberValue(groupKey, item.key, item.default) }]"
|
||||
:placeholder="item.default ? `${$t('settings.default')}: ${item.default}` : ''"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 布尔开关 -->
|
||||
<template v-else-if="item.type === 'boolean'">
|
||||
<a-switch
|
||||
v-decorator="[item.key, { valuePropName: 'checked', initialValue: getBoolValue(groupKey, item.key, item.default) }]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 下拉选择 -->
|
||||
<template v-else-if="item.type === 'select'">
|
||||
<a-select
|
||||
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>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<div class="field-default" v-if="item.default && item.type !== 'boolean' && item.type !== 'password'">
|
||||
{{ $t('settings.default') }}: {{ item.default }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<div class="settings-footer">
|
||||
<a-button @click="handleReset" :disabled="saving">
|
||||
<a-icon type="undo" />
|
||||
{{ $t('settings.reset') }}
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleSave" :loading="saving">
|
||||
<a-icon type="save" />
|
||||
{{ $t('settings.save') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSettingsSchema, getSettingsValues, saveSettings } from '@/api/settings'
|
||||
import { baseMixin } from '@/store/app-mixin'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
mixins: [baseMixin],
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
saving: false,
|
||||
schema: {},
|
||||
values: {},
|
||||
activeKeys: ['ai', 'data_source', 'app', 'auth'],
|
||||
passwordVisible: {},
|
||||
showRestartTip: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isDarkTheme () {
|
||||
return this.navTheme === 'dark' || this.navTheme === 'realdark'
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
mounted () {
|
||||
this.loadSettings()
|
||||
},
|
||||
methods: {
|
||||
async loadSettings () {
|
||||
this.loading = true
|
||||
try {
|
||||
const [schemaRes, valuesRes] = await Promise.all([
|
||||
getSettingsSchema(),
|
||||
getSettingsValues()
|
||||
])
|
||||
|
||||
if (schemaRes.code === 1) {
|
||||
this.schema = schemaRes.data
|
||||
}
|
||||
|
||||
if (valuesRes.code === 1) {
|
||||
this.values = valuesRes.data
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(this.$t('settings.loadFailed'))
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
getGroupIcon (groupKey) {
|
||||
const icons = {
|
||||
auth: 'lock',
|
||||
server: 'cloud-server',
|
||||
worker: 'schedule',
|
||||
notification: 'notification',
|
||||
smtp: 'mail',
|
||||
twilio: 'phone',
|
||||
strategy: 'fund',
|
||||
proxy: 'global',
|
||||
app: 'appstore',
|
||||
ai: 'robot',
|
||||
market: 'stock',
|
||||
data_source: 'database',
|
||||
search: 'search'
|
||||
}
|
||||
return icons[groupKey] || 'setting'
|
||||
},
|
||||
|
||||
getGroupTitle (groupKey, defaultTitle) {
|
||||
const key = `settings.group.${groupKey}`
|
||||
const translated = this.$t(key)
|
||||
return translated !== key ? translated : defaultTitle
|
||||
},
|
||||
|
||||
getItemLabel (groupKey, item) {
|
||||
const key = `settings.field.${item.key}`
|
||||
const translated = this.$t(key)
|
||||
return translated !== key ? translated : item.label
|
||||
},
|
||||
|
||||
getLinkText (linkText) {
|
||||
if (!linkText) return this.$t('settings.getApi')
|
||||
// 如果是翻译键(以 settings.link. 开头),则翻译
|
||||
if (linkText.startsWith('settings.link.')) {
|
||||
const translated = this.$t(linkText)
|
||||
return translated !== linkText ? translated : linkText
|
||||
}
|
||||
return linkText
|
||||
},
|
||||
|
||||
getFieldValue (groupKey, key) {
|
||||
const groupValues = this.values[groupKey] || {}
|
||||
return groupValues[key] || ''
|
||||
},
|
||||
|
||||
togglePasswordVisible (key) {
|
||||
this.$set(this.passwordVisible, key, !this.passwordVisible[key])
|
||||
},
|
||||
|
||||
getNumberValue (groupKey, key, defaultVal) {
|
||||
const val = this.getFieldValue(groupKey, key)
|
||||
if (val === '' || val === null || val === undefined) {
|
||||
return defaultVal ? parseFloat(defaultVal) : null
|
||||
}
|
||||
return parseFloat(val)
|
||||
},
|
||||
|
||||
getBoolValue (groupKey, key, defaultVal) {
|
||||
const val = this.getFieldValue(groupKey, key)
|
||||
if (val === '' || val === null || val === undefined) {
|
||||
return defaultVal === 'True' || defaultVal === 'true' || defaultVal === true
|
||||
}
|
||||
return val === 'True' || val === 'true' || val === true
|
||||
},
|
||||
|
||||
handleReset () {
|
||||
this.form.resetFields()
|
||||
this.loadSettings()
|
||||
},
|
||||
|
||||
copyRestartCommand () {
|
||||
const cmd = 'cd backend_api_python && py run.py'
|
||||
navigator.clipboard.writeText(cmd).then(() => {
|
||||
this.$message.success(this.$t('settings.copySuccess'))
|
||||
}).catch(() => {
|
||||
this.$message.error(this.$t('settings.copyFailed'))
|
||||
})
|
||||
},
|
||||
|
||||
async handleSave () {
|
||||
this.form.validateFields(async (err, formValues) => {
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
|
||||
this.saving = true
|
||||
try {
|
||||
// 按组整理数据
|
||||
const data = {}
|
||||
for (const groupKey of Object.keys(this.schema)) {
|
||||
data[groupKey] = {}
|
||||
const group = this.schema[groupKey]
|
||||
for (const item of group.items) {
|
||||
if (item.key in formValues) {
|
||||
let value = formValues[item.key]
|
||||
// 布尔值转字符串
|
||||
if (item.type === 'boolean') {
|
||||
value = value ? 'True' : 'False'
|
||||
}
|
||||
data[groupKey][item.key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await saveSettings(data)
|
||||
if (res.code === 1) {
|
||||
this.$message.success(res.msg || this.$t('settings.saveSuccess'))
|
||||
// 显示重启提示
|
||||
if (res.data && res.data.requires_restart) {
|
||||
this.showRestartTip = true
|
||||
}
|
||||
// 重新加载配置
|
||||
this.loadSettings()
|
||||
} else {
|
||||
this.$message.error(res.msg || this.$t('settings.saveFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(this.$t('settings.saveFailed') + ': ' + error.message)
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@primary-color: #1890ff;
|
||||
@success-color: #52c41a;
|
||||
@border-radius: 12px;
|
||||
@card-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
|
||||
.settings-page {
|
||||
padding: 24px;
|
||||
min-height: calc(100vh - 120px);
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
|
||||
.restart-alert {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 8px 0;
|
||||
color: #1e3a5f;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.anticon {
|
||||
font-size: 28px;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
|
||||
.settings-collapse {
|
||||
background: transparent;
|
||||
|
||||
/deep/ .ant-collapse-item {
|
||||
margin-bottom: 16px;
|
||||
border: none;
|
||||
border-radius: @border-radius;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: @card-shadow;
|
||||
|
||||
.ant-collapse-header {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e3a5f;
|
||||
padding: 16px 24px;
|
||||
padding-left: 48px;
|
||||
background: linear-gradient(135deg, #fff 0%, #f8fafc 100%);
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ant-collapse-arrow {
|
||||
color: @primary-color;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-icon {
|
||||
font-size: 18px;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: none;
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
/deep/ .ant-form-item-label {
|
||||
padding-bottom: 4px;
|
||||
|
||||
label {
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-label-with-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.api-link {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: @primary-color;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: rgba(24, 144, 255, 0.08);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(24, 144, 255, 0.15);
|
||||
color: darken(@primary-color, 10%);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .ant-input,
|
||||
/deep/ .ant-input-number,
|
||||
/deep/ .ant-select-selection {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/deep/ .ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.password-field {
|
||||
.field-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: @success-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.field-default {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 208px;
|
||||
right: 0;
|
||||
padding: 16px 24px;
|
||||
background: #fff;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
z-index: 100;
|
||||
|
||||
.ant-btn {
|
||||
min-width: 100px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 暗黑主题
|
||||
&.theme-dark {
|
||||
background: linear-gradient(180deg, #0d1117 0%, #161b22 100%);
|
||||
|
||||
.restart-alert {
|
||||
background: #2d333b;
|
||||
border-color: #b08800;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
.page-title {
|
||||
color: #e0e6ed;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
color: #8b949e;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-collapse {
|
||||
/deep/ .ant-collapse-item {
|
||||
background: #1e222d;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
|
||||
|
||||
.ant-collapse-header {
|
||||
background: linear-gradient(135deg, #252a36 0%, #1e222d 100%);
|
||||
color: #e0e6ed;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
|
||||
.panel-header .panel-title {
|
||||
color: #e0e6ed;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
background: #1e222d;
|
||||
|
||||
.ant-collapse-content-box {
|
||||
background: #1e222d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
/deep/ .ant-form-item-label {
|
||||
label {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.form-label-with-link .api-link {
|
||||
background: rgba(24, 144, 255, 0.15);
|
||||
color: #58a6ff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(24, 144, 255, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .ant-input,
|
||||
/deep/ .ant-input-password,
|
||||
/deep/ .ant-input-number,
|
||||
/deep/ .ant-select-selection {
|
||||
background: #0d1117;
|
||||
border-color: #30363d;
|
||||
color: #c9d1d9;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .ant-input-number-input {
|
||||
background: transparent;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
/deep/ .ant-select-arrow {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.field-default {
|
||||
color: #6e7681;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
background: #1e222d;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 768px) {
|
||||
.settings-page {
|
||||
padding: 16px;
|
||||
|
||||
.settings-footer {
|
||||
left: 0;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -211,6 +211,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 颜色变量
|
||||
@primary-color: #1890ff;
|
||||
@success-color: #0ecb81;
|
||||
@danger-color: #f6465d;
|
||||
|
||||
.position-records {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
@@ -222,6 +227,9 @@ export default {
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
padding: 40px 0;
|
||||
background: linear-gradient(135deg, rgba(248, 250, 252, 0.5) 0%, rgba(241, 245, 249, 0.5) 100%);
|
||||
border-radius: 12px;
|
||||
border: 2px dashed #e0e6ed;
|
||||
}
|
||||
|
||||
::v-deep .ant-table {
|
||||
@@ -232,21 +240,21 @@ export default {
|
||||
// 自定义细滚动条
|
||||
::v-deep .ant-table-body {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin; // Firefox - 细滚动条
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent; // Firefox - 滑块颜色和轨道颜色
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px; // 横向滚动条高度
|
||||
width: 6px; // 纵向滚动条宽度
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent; // 轨道背景透明
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2); // 滑块颜色
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3); // 悬停时颜色
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,16 +303,51 @@ export default {
|
||||
}
|
||||
|
||||
::v-deep .ant-table-thead > tr > th {
|
||||
background: #fafafa;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
color: #475569;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding: 12px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
::v-deep .ant-table-tbody > tr > td {
|
||||
padding: 12px 16px;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
color: #334155;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
strong {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .ant-table-tbody > tr {
|
||||
&:hover > td {
|
||||
background: #f0f7ff !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 方向标签美化
|
||||
::v-deep .ant-tag {
|
||||
border-radius: 6px;
|
||||
padding: 2px 10px;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
border: none;
|
||||
|
||||
&[color="green"] {
|
||||
background: linear-gradient(135deg, rgba(14, 203, 129, 0.15) 0%, rgba(14, 203, 129, 0.08) 100%);
|
||||
color: @success-color;
|
||||
border: 1px solid rgba(14, 203, 129, 0.3);
|
||||
}
|
||||
|
||||
&[color="red"] {
|
||||
background: linear-gradient(135deg, rgba(246, 70, 93, 0.15) 0%, rgba(246, 70, 93, 0.08) 100%);
|
||||
color: @danger-color;
|
||||
border: 1px solid rgba(246, 70, 93, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// 暗黑主题适配
|
||||
@@ -350,13 +393,35 @@ export default {
|
||||
}
|
||||
|
||||
.profit {
|
||||
color: #52c41a;
|
||||
font-weight: 600;
|
||||
color: @success-color;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: linear-gradient(135deg, rgba(14, 203, 129, 0.12) 0%, rgba(14, 203, 129, 0.06) 100%);
|
||||
border-radius: 6px;
|
||||
|
||||
&::before {
|
||||
content: '▲';
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.loss {
|
||||
color: #ff4d4f;
|
||||
font-weight: 600;
|
||||
color: @danger-color;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: linear-gradient(135deg, rgba(246, 70, 93, 0.12) 0%, rgba(246, 70, 93, 0.06) 100%);
|
||||
border-radius: 6px;
|
||||
|
||||
&::before {
|
||||
content: '▼';
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
|
||||
@@ -241,19 +241,29 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 颜色变量
|
||||
@primary-color: #1890ff;
|
||||
@success-color: #0ecb81;
|
||||
@danger-color: #f6465d;
|
||||
|
||||
.trading-records {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
padding: 0;
|
||||
overflow-x: visible;
|
||||
overflow-y: visible;
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
padding: 40px 0;
|
||||
background: linear-gradient(135deg, rgba(248, 250, 252, 0.5) 0%, rgba(241, 245, 249, 0.5) 100%);
|
||||
border-radius: 12px;
|
||||
border: 2px dashed #e0e6ed;
|
||||
}
|
||||
|
||||
::v-deep .ant-spin-nested-loading {
|
||||
overflow-x: visible;
|
||||
}
|
||||
@@ -264,7 +274,6 @@ export default {
|
||||
|
||||
::v-deep .ant-table-wrapper {
|
||||
overflow-x: visible;
|
||||
// 自定义细滚动条
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
&::-webkit-scrollbar {
|
||||
@@ -296,27 +305,25 @@ export default {
|
||||
::v-deep .ant-table-body {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
// 自定义细滚动条
|
||||
scrollbar-width: thin; // Firefox - 细滚动条
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent; // Firefox - 滑块颜色和轨道颜色
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px; // 横向滚动条高度
|
||||
width: 6px; // 纵向滚动条宽度
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent; // 轨道背景透明
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2); // 滑块颜色
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3); // 悬停时颜色
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表格容器的滚动条样式
|
||||
::v-deep .ant-table-container {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
@@ -337,7 +344,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 所有可能的表格滚动容器的滚动条样式
|
||||
::v-deep .ant-table-content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
@@ -358,23 +364,109 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 防止单元格折行,触发横向滚动
|
||||
::v-deep .ant-table-thead > tr > th,
|
||||
::v-deep .ant-table-tbody > tr > td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
::v-deep .ant-table-thead > tr > th {
|
||||
background: #fafafa;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
color: #475569;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding: 12px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
::v-deep .ant-table-tbody > tr > td {
|
||||
padding: 12px 16px;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
color: #334155;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
::v-deep .ant-table-tbody > tr {
|
||||
&:hover > td {
|
||||
background: #f0f7ff !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 交易类型标签美化
|
||||
::v-deep .ant-tag {
|
||||
border-radius: 6px;
|
||||
padding: 3px 10px;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&[color="green"], &[color="cyan"], &[color="lime"] {
|
||||
background: linear-gradient(135deg, rgba(14, 203, 129, 0.15) 0%, rgba(14, 203, 129, 0.08) 100%);
|
||||
color: @success-color;
|
||||
border: 1px solid rgba(14, 203, 129, 0.3);
|
||||
}
|
||||
|
||||
&[color="red"], &[color="magenta"] {
|
||||
background: linear-gradient(135deg, rgba(246, 70, 93, 0.15) 0%, rgba(246, 70, 93, 0.08) 100%);
|
||||
color: @danger-color;
|
||||
border: 1px solid rgba(246, 70, 93, 0.3);
|
||||
}
|
||||
|
||||
&[color="orange"] {
|
||||
background: linear-gradient(135deg, rgba(250, 173, 20, 0.15) 0%, rgba(250, 173, 20, 0.08) 100%);
|
||||
color: #d48806;
|
||||
border: 1px solid rgba(250, 173, 20, 0.3);
|
||||
}
|
||||
|
||||
&[color="blue"] {
|
||||
background: linear-gradient(135deg, rgba(24, 144, 255, 0.15) 0%, rgba(24, 144, 255, 0.08) 100%);
|
||||
color: @primary-color;
|
||||
border: 1px solid rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// 分页器美化
|
||||
::v-deep .ant-pagination {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.ant-pagination-item {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: @primary-color;
|
||||
|
||||
a {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-pagination-item-active {
|
||||
background: linear-gradient(135deg, @primary-color 0%, #40a9ff 100%);
|
||||
border-color: @primary-color;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-prev,
|
||||
.ant-pagination-next {
|
||||
.ant-pagination-item-link {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: @primary-color;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗黑主题适配
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user