fix: Multiple bug fixes and improvements
- Fix Invalid Date display in Dashboard notifications - Fix timezone offset (8 hours) in Trading Records time display - Fix position closing failures due to commission discrepancies (fetch actual exchange position size for reduce_only orders) - Fix IBKR connection error 'no current event loop in thread' by ensuring asyncio event loop exists - Fix duplicate orders on same candle by extending signal deduplication to close signals - Add responsive design for Profile page (mobile-friendly) - Remove unused strategy_code module and database table - Fix LLM service to support multiple providers (OpenRouter, OpenAI, DeepSeek, Grok, Google) - Add auto-detection of configured LLM provider based on API key availability - Fix AI code generation to use unified LLMService with proper provider selection - Fix crypto symbol format handling (ETH/USDT no longer becomes ETH/USDT/USDT) - Fix Commission display showing '0E-8' in Trading Records - Fix P&L display for signal-only trades (show '--' for unrealized P&L) - Fix OAuth login not updating last_login_at for new users - Add migration script for notification_settings column - Update env.example with new LLM provider configurations - Remove ESLint rule that was not defined in config
This commit is contained in:
@@ -290,7 +290,30 @@ export default {
|
||||
},
|
||||
formatTime (timestamp) {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(timestamp * 1000)
|
||||
// 支持多种时间格式:ISO字符串、秒级时间戳、毫秒级时间戳
|
||||
let date
|
||||
if (typeof timestamp === 'number') {
|
||||
// 数字类型:判断是秒级还是毫秒级时间戳
|
||||
date = new Date(timestamp < 1e12 ? timestamp * 1000 : timestamp)
|
||||
} else if (typeof timestamp === 'string') {
|
||||
// 字符串类型
|
||||
if (/^\d+$/.test(timestamp)) {
|
||||
// 纯数字字符串(时间戳)
|
||||
const ts = parseInt(timestamp, 10)
|
||||
date = new Date(ts < 1e12 ? ts * 1000 : ts)
|
||||
} else {
|
||||
// ISO 日期字符串或其他格式
|
||||
date = new Date(timestamp)
|
||||
}
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 检查日期是否有效
|
||||
if (isNaN(date.getTime())) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
@@ -311,7 +334,24 @@ export default {
|
||||
},
|
||||
formatFullTime (timestamp) {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(timestamp * 1000)
|
||||
// 支持多种时间格式:ISO字符串、秒级时间戳、毫秒级时间戳
|
||||
let date
|
||||
if (typeof timestamp === 'number') {
|
||||
date = new Date(timestamp < 1e12 ? timestamp * 1000 : timestamp)
|
||||
} else if (typeof timestamp === 'string') {
|
||||
if (/^\d+$/.test(timestamp)) {
|
||||
const ts = parseInt(timestamp, 10)
|
||||
date = new Date(ts < 1e12 ? ts * 1000 : ts)
|
||||
} else {
|
||||
date = new Date(timestamp)
|
||||
}
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return ''
|
||||
}
|
||||
return date.toLocaleString()
|
||||
},
|
||||
formatMessageHtml (message) {
|
||||
|
||||
@@ -40,11 +40,9 @@ if (typeof window !== 'undefined') {
|
||||
// mount axios to `Vue.$http` and `this.$http`
|
||||
Vue.use(VueAxios)
|
||||
// use pro-layout components
|
||||
/* eslint-disable vue/component-definition-name-casing */
|
||||
Vue.component('pro-layout', ProLayout)
|
||||
Vue.component('page-container', PageHeaderWrapper)
|
||||
Vue.component('page-header-wrapper', PageHeaderWrapper)
|
||||
/* eslint-enable vue/component-definition-name-casing */
|
||||
|
||||
window.umi_plugin_ant_themeVar = themePluginConfig.theme
|
||||
|
||||
|
||||
@@ -379,9 +379,9 @@
|
||||
{{ getSignalTypeText(text) }}
|
||||
</span>
|
||||
</template>
|
||||
<template slot="profit" slot-scope="text">
|
||||
<span :class="text >= 0 ? 'positive' : 'negative'">
|
||||
{{ text >= 0 ? '+' : '' }}${{ formatNumber(text) }}
|
||||
<template slot="profit" slot-scope="text, record">
|
||||
<span :class="formatProfitValue(text, record) !== '--' ? (text >= 0 ? 'positive' : 'negative') : ''">
|
||||
{{ formatProfitValue(text, record) }}
|
||||
</span>
|
||||
</template>
|
||||
<template slot="time" slot-scope="text">
|
||||
@@ -958,6 +958,30 @@ export default {
|
||||
if (num === undefined || num === null) return '0.00'
|
||||
return Number(num).toLocaleString('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits })
|
||||
},
|
||||
// 格式化盈亏值(处理信号模式下没有实盘的情况)
|
||||
formatProfitValue (value, record) {
|
||||
if (value === null || value === undefined) return '--'
|
||||
|
||||
const numValue = parseFloat(value)
|
||||
|
||||
// 如果值为0且是开仓信号(open_long/open_short),显示--
|
||||
const openTypes = ['open_long', 'open_short', 'add_long', 'add_short']
|
||||
if (numValue === 0 && record && openTypes.includes(record.type)) {
|
||||
return '--'
|
||||
}
|
||||
|
||||
// 如果值极小(科学计数法如0E-8),视为0
|
||||
if (Math.abs(numValue) < 0.000001) {
|
||||
if (record && openTypes.includes(record.type)) {
|
||||
return '--'
|
||||
}
|
||||
return '$0.00'
|
||||
}
|
||||
|
||||
// 正常显示
|
||||
const sign = numValue >= 0 ? '+' : ''
|
||||
return `${sign}$${this.formatNumber(numValue)}`
|
||||
},
|
||||
formatCompactNumber (num) {
|
||||
if (num === undefined || num === null) return '0'
|
||||
const abs = Math.abs(num)
|
||||
|
||||
@@ -1640,4 +1640,396 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Mobile Responsive Styles ====================
|
||||
@media screen and (max-width: 768px) {
|
||||
.profile-page {
|
||||
padding: 12px;
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
|
||||
.anticon {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// Profile cards row - stack vertically
|
||||
.profile-cards-row {
|
||||
flex-direction: column;
|
||||
|
||||
.profile-card-col {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.right-cards-col {
|
||||
.right-cards-row {
|
||||
flex-direction: column;
|
||||
|
||||
.ant-col {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Profile card adjustments
|
||||
.profile-card {
|
||||
border-radius: 10px;
|
||||
|
||||
.avatar-section {
|
||||
padding: 16px 0;
|
||||
|
||||
/deep/ .ant-avatar {
|
||||
width: 80px !important;
|
||||
height: 80px !important;
|
||||
line-height: 80px !important;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 18px;
|
||||
margin: 12px 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
.info-item {
|
||||
padding: 10px 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.anticon {
|
||||
font-size: 14px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Credits card adjustments
|
||||
.credits-card {
|
||||
border-radius: 10px;
|
||||
|
||||
.credits-header {
|
||||
.credits-title {
|
||||
font-size: 15px;
|
||||
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.credits-body {
|
||||
padding: 16px 0;
|
||||
|
||||
.credits-amount {
|
||||
.amount-value {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.vip-status {
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.credits-actions {
|
||||
.ant-btn {
|
||||
height: 34px;
|
||||
padding: 0 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.credits-hint {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// Referral card adjustments
|
||||
.referral-card {
|
||||
border-radius: 10px;
|
||||
|
||||
.referral-header {
|
||||
.referral-title {
|
||||
font-size: 15px;
|
||||
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.referral-body {
|
||||
padding: 10px 0;
|
||||
|
||||
.referral-stats {
|
||||
.stat-item {
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.referral-link-section {
|
||||
.link-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.link-box {
|
||||
/deep/ .ant-input {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.referral-hint {
|
||||
font-size: 11px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Edit card adjustments
|
||||
.edit-card {
|
||||
border-radius: 10px;
|
||||
margin-top: 12px !important;
|
||||
|
||||
/deep/ .ant-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/deep/ .ant-tabs-nav {
|
||||
.ant-tabs-tab {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow horizontal scroll for tabs on mobile
|
||||
/deep/ .ant-tabs-nav-scroll {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-form,
|
||||
.password-form {
|
||||
max-width: 100%;
|
||||
|
||||
/deep/ .ant-form-item-label {
|
||||
padding-bottom: 4px;
|
||||
|
||||
label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .ant-input,
|
||||
/deep/ .ant-input-password {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.email-hint {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
// Password form - verification code section
|
||||
.password-form {
|
||||
/deep/ .ant-alert {
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// Notification settings form
|
||||
.notification-settings-form {
|
||||
/deep/ .ant-alert {
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 16px !important;
|
||||
}
|
||||
|
||||
/deep/ .ant-form {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/deep/ .ant-checkbox-group {
|
||||
.ant-row {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
|
||||
.ant-col {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
font-size: 11px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/deep/ .ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
/deep/ .ant-form-item:last-child {
|
||||
.ant-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
|
||||
& + .ant-btn {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tables in tabs
|
||||
/deep/ .ant-table-wrapper {
|
||||
overflow-x: auto;
|
||||
|
||||
.ant-table {
|
||||
min-width: 500px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Referral user cell in table
|
||||
.referral-user-cell {
|
||||
gap: 8px;
|
||||
|
||||
/deep/ .ant-avatar {
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
line-height: 28px !important;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.nickname {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extra small devices (phones in portrait)
|
||||
@media screen and (max-width: 480px) {
|
||||
.profile-page {
|
||||
padding: 8px;
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
gap: 8px;
|
||||
|
||||
.anticon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Credits card
|
||||
.credits-card {
|
||||
.credits-body {
|
||||
.credits-amount {
|
||||
.amount-value {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.credits-actions {
|
||||
.ant-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Referral card
|
||||
.referral-card {
|
||||
.referral-body {
|
||||
.referral-stats {
|
||||
.stat-item {
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Edit card
|
||||
.edit-card {
|
||||
/deep/ .ant-tabs-nav {
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// Notification settings - stack checkboxes
|
||||
.notification-settings-form {
|
||||
/deep/ .ant-checkbox-group {
|
||||
.ant-row {
|
||||
.ant-col {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,11 +27,14 @@
|
||||
<template slot="value" slot-scope="text">
|
||||
${{ parseFloat(text).toFixed(2) }}
|
||||
</template>
|
||||
<template slot="profit" slot-scope="text">
|
||||
<template slot="profit" slot-scope="text, record">
|
||||
<span :style="{ color: text > 0 ? '#52c41a' : text < 0 ? '#f5222d' : '#666' }">
|
||||
{{ formatMoney(text) }}
|
||||
{{ formatProfit(text, record) }}
|
||||
</span>
|
||||
</template>
|
||||
<template slot="commission" slot-scope="text">
|
||||
{{ formatCommission(text) }}
|
||||
</template>
|
||||
<template slot="time" slot-scope="text, record">
|
||||
{{ formatTime(record.created_at || text) }}
|
||||
</template>
|
||||
@@ -103,7 +106,8 @@ export default {
|
||||
title: this.$t('trading-assistant.table.commission'),
|
||||
dataIndex: 'commission',
|
||||
key: 'commission',
|
||||
width: 100
|
||||
width: 100,
|
||||
scopedSlots: { customRender: 'commission' }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -242,6 +246,46 @@ export default {
|
||||
// 正数显示+,负数显示-
|
||||
const sign = value >= 0 ? '+' : '-'
|
||||
return `${sign}$${Math.abs(value).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||
},
|
||||
// 格式化盈亏(处理信号模式下没有实盘的情况)
|
||||
formatProfit (value, record) {
|
||||
// 如果是信号模式(没有实盘交易),profit为0或null时显示--
|
||||
// 判断依据:如果是开仓信号且profit为0,或者record.is_signal_only为true
|
||||
if (value === null || value === undefined) return '--'
|
||||
|
||||
const numValue = parseFloat(value)
|
||||
|
||||
// 如果值为0且是开仓信号(open_long/open_short),显示--
|
||||
// 因为开仓时还没有盈亏
|
||||
const openTypes = ['open_long', 'open_short', 'add_long', 'add_short']
|
||||
if (numValue === 0 && record && openTypes.includes(record.type)) {
|
||||
return '--'
|
||||
}
|
||||
|
||||
// 如果值极小(科学计数法如0E-8),视为0
|
||||
if (Math.abs(numValue) < 0.000001) {
|
||||
// 开仓类型显示--,平仓类型显示$0.00
|
||||
if (record && openTypes.includes(record.type)) {
|
||||
return '--'
|
||||
}
|
||||
return '$0.00'
|
||||
}
|
||||
|
||||
return this.formatMoney(numValue)
|
||||
},
|
||||
// 格式化手续费(避免科学计数法如0E-8)
|
||||
formatCommission (value) {
|
||||
if (value === null || value === undefined) return '--'
|
||||
|
||||
const numValue = parseFloat(value)
|
||||
|
||||
// 如果值极小(科学计数法如0E-8),显示为0或--
|
||||
if (isNaN(numValue) || Math.abs(numValue) < 0.000001) {
|
||||
return '--'
|
||||
}
|
||||
|
||||
// 正常格式化显示
|
||||
return `$${numValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 6 })}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user