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:
TIANHE
2026-01-24 03:22:14 +08:00
parent 7de1570b3a
commit f4e5a9f8e0
22 changed files with 1358 additions and 464 deletions
@@ -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) {
-2
View File
@@ -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
+27 -3
View File
@@ -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)
+392
View File
@@ -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 })}`
}
}
}