feat: Move notification settings to user profile and add i18n support
- Move notification config from system settings (.env) to user profile (database) - Add user-specific notification settings API endpoints (GET/PUT /api/user/notification-settings) - Add notification_settings column to qd_users table - Update portfolio and trading-assistant to use profile notification settings - Add notification settings UI in profile page with all channels (browser, telegram, email, phone, discord, webhook) - Add i18n translations for notification settings in 10 languages - Fix timestamp parsing in TradingRecords and portfolio (handle both ISO strings and Unix timestamps) - Fix header icons alignment for mobile responsive layout - Fix equity curve timestamp bug (handle datetime objects properly) - Remove unused login.js exports and clean up user.js store - Remove husky, commitlint and other dev dependencies - Clean up env.example by removing user-specific notification params - Update signal_notifier to prioritize user-specific tokens over global env vars
This commit is contained in:
@@ -391,7 +391,7 @@
|
||||
<a-icon type="eye" />
|
||||
<span>{{ $t('portfolio.monitors.title') }}</span>
|
||||
</h3>
|
||||
<a-button type="primary" @click="showAddMonitorModal = true">
|
||||
<a-button type="primary" @click="openAddMonitorModal">
|
||||
<a-icon type="plus" />
|
||||
{{ $t('portfolio.monitors.add') }}
|
||||
</a-button>
|
||||
@@ -401,7 +401,7 @@
|
||||
<div class="monitors-list">
|
||||
<div v-if="monitors.length === 0" class="empty-state small">
|
||||
<a-empty :description="$t('portfolio.monitors.empty')" :image="simpleImage">
|
||||
<a-button type="primary" size="small" @click="showAddMonitorModal = true">
|
||||
<a-button type="primary" size="small" @click="openAddMonitorModal">
|
||||
<a-icon type="plus" />
|
||||
{{ $t('portfolio.monitors.addFirst') }}
|
||||
</a-button>
|
||||
@@ -718,31 +718,22 @@
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Telegram Chat ID -->
|
||||
<a-form-item
|
||||
v-show="alertChannels.includes('telegram')"
|
||||
:label="$t('portfolio.form.telegramChatId')"
|
||||
<!-- Notification settings hint -->
|
||||
<a-alert
|
||||
v-if="alertChannels.includes('telegram') || alertChannels.includes('email')"
|
||||
type="info"
|
||||
showIcon
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<a-input
|
||||
v-model="alertTelegramChatId"
|
||||
:placeholder="$t('portfolio.form.enterTelegramChatId')"
|
||||
>
|
||||
<a-icon slot="prefix" type="message" />
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Email -->
|
||||
<a-form-item
|
||||
v-show="alertChannels.includes('email')"
|
||||
:label="$t('portfolio.form.emailAddress')"
|
||||
>
|
||||
<a-input
|
||||
v-model="alertEmail"
|
||||
:placeholder="$t('portfolio.form.enterEmail')"
|
||||
>
|
||||
<a-icon slot="prefix" type="mail" />
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<template #message>
|
||||
<span>
|
||||
{{ $t('portfolio.form.notificationFromProfile') || '通知将发送到您在个人中心配置的地址' }}
|
||||
<router-link to="/profile" style="margin-left: 8px">
|
||||
<a-icon type="setting" /> {{ $t('portfolio.form.goToProfile') || '前往配置' }}
|
||||
</router-link>
|
||||
</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
|
||||
<!-- 启用状态 -->
|
||||
<a-form-item :label="$t('portfolio.alerts.enabled')">
|
||||
@@ -806,21 +797,22 @@
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Telegram Chat ID -->
|
||||
<a-form-item :label="$t('portfolio.form.telegramChatId')" v-show="monitorChannels.includes('telegram')">
|
||||
<a-input
|
||||
v-model="monitorTelegramChatId"
|
||||
:placeholder="$t('portfolio.form.enterTelegramChatId')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Email -->
|
||||
<a-form-item :label="$t('portfolio.form.emailAddress')" v-show="monitorChannels.includes('email')">
|
||||
<a-input
|
||||
v-model="monitorEmail"
|
||||
:placeholder="$t('portfolio.form.enterEmail')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<!-- Notification settings hint -->
|
||||
<a-alert
|
||||
v-if="monitorChannels.includes('telegram') || monitorChannels.includes('email')"
|
||||
type="info"
|
||||
showIcon
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<template #message>
|
||||
<span>
|
||||
{{ $t('portfolio.form.notificationFromProfile') || '通知将发送到您在个人中心配置的地址' }}
|
||||
<router-link to="/profile" style="margin-left: 8px">
|
||||
<a-icon type="setting" /> {{ $t('portfolio.form.goToProfile') || '前往配置' }}
|
||||
</router-link>
|
||||
</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
|
||||
<!-- 监控范围 -->
|
||||
<a-form-item :label="$t('portfolio.form.monitorScope')">
|
||||
@@ -894,6 +886,7 @@ import {
|
||||
getGroups,
|
||||
searchSymbols, getMarketTypes
|
||||
} from '@/api/portfolio'
|
||||
import { getNotificationSettings } from '@/api/user'
|
||||
|
||||
export default {
|
||||
name: 'Portfolio',
|
||||
@@ -954,18 +947,23 @@ export default {
|
||||
alertPosition: null,
|
||||
// Alert channels (for reactive display)
|
||||
alertChannels: ['browser'],
|
||||
// Alert notification targets
|
||||
alertTelegramChatId: '',
|
||||
alertEmail: '',
|
||||
// Monitor channels (for reactive display)
|
||||
monitorChannels: ['browser'],
|
||||
// Monitor notification targets
|
||||
monitorTelegramChatId: '',
|
||||
monitorEmail: '',
|
||||
// Monitor scope (all or selected positions)
|
||||
monitorScope: 'all',
|
||||
// Selected positions for monitoring
|
||||
selectedMonitorPositions: []
|
||||
selectedMonitorPositions: [],
|
||||
// User's default notification settings (loaded from profile)
|
||||
userNotificationSettings: {
|
||||
default_channels: ['browser'],
|
||||
telegram_bot_token: '',
|
||||
telegram_chat_id: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
discord_webhook: '',
|
||||
webhook_url: '',
|
||||
webhook_token: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -1053,6 +1051,8 @@ export default {
|
||||
this.priceRefreshTimer = setInterval(() => {
|
||||
this.refreshPrices()
|
||||
}, 30000)
|
||||
// Load user's notification settings for default values
|
||||
this.loadUserNotificationSettings()
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.priceRefreshTimer) {
|
||||
@@ -1063,6 +1063,26 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadUserNotificationSettings () {
|
||||
// Load user's default notification settings
|
||||
try {
|
||||
const res = await getNotificationSettings()
|
||||
if (res.code === 1 && res.data) {
|
||||
this.userNotificationSettings = {
|
||||
default_channels: res.data.default_channels || ['browser'],
|
||||
telegram_bot_token: res.data.telegram_bot_token || '',
|
||||
telegram_chat_id: res.data.telegram_chat_id || '',
|
||||
email: res.data.email || '',
|
||||
phone: res.data.phone || '',
|
||||
discord_webhook: res.data.discord_webhook || '',
|
||||
webhook_url: res.data.webhook_url || '',
|
||||
webhook_token: res.data.webhook_token || ''
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail, use default values
|
||||
}
|
||||
},
|
||||
async loadData () {
|
||||
await Promise.all([
|
||||
this.loadPositions(),
|
||||
@@ -1314,12 +1334,10 @@ export default {
|
||||
this.editAlert(existingAlert)
|
||||
return
|
||||
}
|
||||
// 创建新的 Alert
|
||||
// 创建新的 Alert - 使用用户默认通知设置
|
||||
this.editingAlert = null
|
||||
this.alertPosition = pos
|
||||
this.alertChannels = ['browser']
|
||||
this.alertTelegramChatId = ''
|
||||
this.alertEmail = ''
|
||||
this.alertChannels = [...(this.userNotificationSettings.default_channels || ['browser'])]
|
||||
this.showAddAlertModal = true
|
||||
this.$nextTick(() => {
|
||||
this.alertForm.setFieldsValue({
|
||||
@@ -1341,10 +1359,8 @@ export default {
|
||||
current_price: 0,
|
||||
entry_price: 0
|
||||
}
|
||||
// Set channels and targets directly with v-model binding
|
||||
// Set channels directly with v-model binding
|
||||
this.alertChannels = [...(alert.notification_config?.channels || ['browser'])]
|
||||
this.alertTelegramChatId = alert.notification_config?.targets?.telegram || ''
|
||||
this.alertEmail = alert.notification_config?.targets?.email || ''
|
||||
// Show modal
|
||||
this.showAddAlertModal = true
|
||||
// Set form values for fields still using v-decorator
|
||||
@@ -1366,13 +1382,28 @@ export default {
|
||||
if (err) return
|
||||
this.savingAlert = true
|
||||
try {
|
||||
// 构建通知目标 - 使用 v-model 绑定的值
|
||||
// 构建通知目标 - 使用用户在个人中心配置的值
|
||||
const targets = {}
|
||||
if (this.alertChannels.includes('telegram') && this.alertTelegramChatId) {
|
||||
targets.telegram = this.alertTelegramChatId.trim()
|
||||
if (this.alertChannels.includes('telegram') && this.userNotificationSettings.telegram_chat_id) {
|
||||
targets.telegram = this.userNotificationSettings.telegram_chat_id
|
||||
if (this.userNotificationSettings.telegram_bot_token) {
|
||||
targets.telegram_bot_token = this.userNotificationSettings.telegram_bot_token
|
||||
}
|
||||
}
|
||||
if (this.alertChannels.includes('email') && this.alertEmail) {
|
||||
targets.email = this.alertEmail.trim()
|
||||
if (this.alertChannels.includes('email') && this.userNotificationSettings.email) {
|
||||
targets.email = this.userNotificationSettings.email
|
||||
}
|
||||
if (this.alertChannels.includes('phone') && this.userNotificationSettings.phone) {
|
||||
targets.phone = this.userNotificationSettings.phone
|
||||
}
|
||||
if (this.alertChannels.includes('discord') && this.userNotificationSettings.discord_webhook) {
|
||||
targets.discord = this.userNotificationSettings.discord_webhook
|
||||
}
|
||||
if (this.alertChannels.includes('webhook') && this.userNotificationSettings.webhook_url) {
|
||||
targets.webhook = this.userNotificationSettings.webhook_url
|
||||
if (this.userNotificationSettings.webhook_token) {
|
||||
targets.webhook_token = this.userNotificationSettings.webhook_token
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
@@ -1430,9 +1461,7 @@ export default {
|
||||
this.showAddAlertModal = false
|
||||
this.editingAlert = null
|
||||
this.alertPosition = null
|
||||
this.alertChannels = ['browser']
|
||||
this.alertTelegramChatId = ''
|
||||
this.alertEmail = ''
|
||||
this.alertChannels = [...(this.userNotificationSettings.default_channels || ['browser'])]
|
||||
this.alertForm.resetFields()
|
||||
},
|
||||
confirmDeleteAlert () {
|
||||
@@ -1468,6 +1497,19 @@ export default {
|
||||
}
|
||||
},
|
||||
// Monitor methods
|
||||
openAddMonitorModal () {
|
||||
// Initialize with user's default notification settings
|
||||
this.editingMonitor = null
|
||||
this.monitorChannels = [...(this.userNotificationSettings.default_channels || ['browser'])]
|
||||
this.monitorScope = 'all'
|
||||
this.selectedMonitorPositions = []
|
||||
this.showAddMonitorModal = true
|
||||
this.$nextTick(() => {
|
||||
if (this.monitorForm) {
|
||||
this.monitorForm.resetFields()
|
||||
}
|
||||
})
|
||||
},
|
||||
handleMonitorChannelsChange (channels) {
|
||||
this.monitorChannels = channels || []
|
||||
},
|
||||
@@ -1475,8 +1517,6 @@ export default {
|
||||
this.editingMonitor = monitor
|
||||
// Set channels directly with v-model binding
|
||||
this.monitorChannels = [...(monitor.notification_config?.channels || ['browser'])]
|
||||
this.monitorTelegramChatId = monitor.notification_config?.targets?.telegram || ''
|
||||
this.monitorEmail = monitor.notification_config?.targets?.email || ''
|
||||
// Update monitor scope and selected positions
|
||||
let positionIds = []
|
||||
if (monitor.position_ids) {
|
||||
@@ -1540,11 +1580,16 @@ export default {
|
||||
language: this.$store.getters.lang || 'en-US'
|
||||
},
|
||||
notification_config: {
|
||||
// Use v-model bound values instead of form values
|
||||
// Use user's profile notification settings
|
||||
channels: this.monitorChannels.length > 0 ? this.monitorChannels : ['browser'],
|
||||
targets: {
|
||||
telegram: this.monitorTelegramChatId || '',
|
||||
email: this.monitorEmail || ''
|
||||
telegram: this.userNotificationSettings.telegram_chat_id || '',
|
||||
telegram_bot_token: this.userNotificationSettings.telegram_bot_token || '',
|
||||
email: this.userNotificationSettings.email || '',
|
||||
phone: this.userNotificationSettings.phone || '',
|
||||
discord: this.userNotificationSettings.discord_webhook || '',
|
||||
webhook: this.userNotificationSettings.webhook_url || '',
|
||||
webhook_token: this.userNotificationSettings.webhook_token || ''
|
||||
}
|
||||
},
|
||||
is_active: true
|
||||
@@ -1644,9 +1689,7 @@ export default {
|
||||
this.showAddMonitorModal = false
|
||||
this.editingMonitor = null
|
||||
this.monitorForm.resetFields()
|
||||
this.monitorChannels = ['browser'] // Reset to default
|
||||
this.monitorTelegramChatId = '' // Reset telegram
|
||||
this.monitorEmail = '' // Reset email
|
||||
this.monitorChannels = [...(this.userNotificationSettings.default_channels || ['browser'])] // Reset to user default
|
||||
this.monitorScope = 'all' // Reset monitor scope
|
||||
this.selectedMonitorPositions = [] // Reset selected positions
|
||||
},
|
||||
@@ -1704,7 +1747,25 @@ export default {
|
||||
},
|
||||
formatTime (timestamp) {
|
||||
if (!timestamp) return '-'
|
||||
const d = new Date(timestamp * 1000)
|
||||
let d
|
||||
// 如果是数字(秒级时间戳),乘以 1000
|
||||
if (typeof timestamp === 'number') {
|
||||
d = new Date(timestamp * 1000)
|
||||
} else if (typeof timestamp === 'string') {
|
||||
// 如果是纯数字字符串(秒级时间戳)
|
||||
if (/^\d+$/.test(timestamp)) {
|
||||
d = new Date(parseInt(timestamp, 10) * 1000)
|
||||
} else {
|
||||
// ISO 日期字符串或其他格式,直接解析
|
||||
d = new Date(timestamp)
|
||||
}
|
||||
} else {
|
||||
return '-'
|
||||
}
|
||||
// 检查日期是否有效
|
||||
if (isNaN(d.getTime())) {
|
||||
return '-'
|
||||
}
|
||||
return d.toLocaleString()
|
||||
},
|
||||
getIntervalText (minutes) {
|
||||
|
||||
@@ -284,6 +284,174 @@
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- Notification Settings Tab (通知设置) -->
|
||||
<a-tab-pane key="notifications" :tab="$t('profile.notifications.title') || '通知设置'">
|
||||
<div class="notification-settings-form">
|
||||
<a-alert
|
||||
:message="$t('profile.notifications.hint') || '配置您的默认通知方式,在创建资产监控和预警时将自动使用这些设置'"
|
||||
type="info"
|
||||
showIcon
|
||||
style="margin-bottom: 24px"
|
||||
/>
|
||||
|
||||
<a-form :form="notificationForm" layout="vertical" style="max-width: 600px;">
|
||||
<!-- Default Channels -->
|
||||
<a-form-item :label="$t('profile.notifications.defaultChannels') || '默认通知渠道'">
|
||||
<a-checkbox-group
|
||||
v-decorator="['default_channels', { initialValue: notificationSettings.default_channels || ['browser'] }]"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-checkbox value="browser">
|
||||
<a-icon type="bell" /> {{ $t('profile.notifications.browser') || '站内通知' }}
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-checkbox value="telegram">
|
||||
<a-icon type="send" /> Telegram
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-checkbox value="email">
|
||||
<a-icon type="mail" /> {{ $t('profile.notifications.email') || '邮件' }}
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16" style="margin-top: 8px">
|
||||
<a-col :span="8">
|
||||
<a-checkbox value="phone">
|
||||
<a-icon type="phone" /> {{ $t('profile.notifications.phone') || '短信' }}
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-checkbox value="discord">
|
||||
<a-icon type="message" /> Discord
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-checkbox value="webhook">
|
||||
<a-icon type="api" /> Webhook
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Telegram Bot Token -->
|
||||
<a-form-item :label="$t('profile.notifications.telegramBotToken') || 'Telegram Bot Token'">
|
||||
<a-input-password
|
||||
v-decorator="['telegram_bot_token', { initialValue: notificationSettings.telegram_bot_token }]"
|
||||
:placeholder="$t('profile.notifications.telegramBotTokenPlaceholder') || '请输入您的 Telegram Bot Token'"
|
||||
>
|
||||
<a-icon slot="prefix" type="robot" />
|
||||
</a-input-password>
|
||||
<div class="field-hint">
|
||||
<a-icon type="info-circle" />
|
||||
<span>
|
||||
{{ $t('profile.notifications.telegramBotTokenHint') || '通过 @BotFather 创建机器人获取 Token' }}
|
||||
<a href="https://t.me/BotFather" target="_blank" rel="noopener noreferrer">@BotFather</a>
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Telegram Chat ID -->
|
||||
<a-form-item :label="$t('profile.notifications.telegramChatId') || 'Telegram Chat ID'">
|
||||
<a-input
|
||||
v-decorator="['telegram_chat_id', { initialValue: notificationSettings.telegram_chat_id }]"
|
||||
:placeholder="$t('profile.notifications.telegramPlaceholder') || '请输入您的 Telegram Chat ID(如 123456789)'"
|
||||
>
|
||||
<a-icon slot="prefix" type="message" />
|
||||
</a-input>
|
||||
<div class="field-hint">
|
||||
<a-icon type="info-circle" />
|
||||
<span>{{ $t('profile.notifications.telegramHint') || '发送 /start 给 @userinfobot 可获取您的 Chat ID' }}</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Notification Email -->
|
||||
<a-form-item :label="$t('profile.notifications.notifyEmail') || '通知邮箱'">
|
||||
<a-input
|
||||
v-decorator="['email', { initialValue: notificationSettings.email || profile.email }]"
|
||||
:placeholder="$t('profile.notifications.emailPlaceholder') || '接收通知的邮箱地址'"
|
||||
>
|
||||
<a-icon slot="prefix" type="mail" />
|
||||
</a-input>
|
||||
<div class="field-hint">
|
||||
<a-icon type="info-circle" />
|
||||
<span>{{ $t('profile.notifications.emailHint') || '默认使用账户邮箱,可设置其他邮箱接收通知' }}</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Phone Number (SMS) -->
|
||||
<a-form-item :label="$t('profile.notifications.phone') || '手机号(短信通知)'">
|
||||
<a-input
|
||||
v-decorator="['phone', { initialValue: notificationSettings.phone }]"
|
||||
:placeholder="$t('profile.notifications.phonePlaceholder') || '请输入手机号(如 +8613800138000)'"
|
||||
>
|
||||
<a-icon slot="prefix" type="phone" />
|
||||
</a-input>
|
||||
<div class="field-hint">
|
||||
<a-icon type="info-circle" />
|
||||
<span>{{ $t('profile.notifications.phoneHint') || '需要管理员配置 Twilio 服务后才能使用短信通知' }}</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Discord Webhook -->
|
||||
<a-form-item :label="$t('profile.notifications.discordWebhook') || 'Discord Webhook'">
|
||||
<a-input
|
||||
v-decorator="['discord_webhook', { initialValue: notificationSettings.discord_webhook }]"
|
||||
:placeholder="$t('profile.notifications.discordPlaceholder') || 'https://discord.com/api/webhooks/...'"
|
||||
>
|
||||
<a-icon slot="prefix" type="message" />
|
||||
</a-input>
|
||||
<div class="field-hint">
|
||||
<a-icon type="info-circle" />
|
||||
<span>{{ $t('profile.notifications.discordHint') || '在 Discord 服务器设置中创建 Webhook' }}</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Webhook URL -->
|
||||
<a-form-item :label="$t('profile.notifications.webhookUrl') || 'Webhook URL'">
|
||||
<a-input
|
||||
v-decorator="['webhook_url', { initialValue: notificationSettings.webhook_url }]"
|
||||
:placeholder="$t('profile.notifications.webhookPlaceholder') || 'https://your-server.com/webhook'"
|
||||
>
|
||||
<a-icon slot="prefix" type="api" />
|
||||
</a-input>
|
||||
<div class="field-hint">
|
||||
<a-icon type="info-circle" />
|
||||
<span>{{ $t('profile.notifications.webhookHint') || '自定义 Webhook 地址,将以 POST JSON 方式推送通知' }}</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Webhook Token -->
|
||||
<a-form-item :label="$t('profile.notifications.webhookToken') || 'Webhook Token(可选)'">
|
||||
<a-input-password
|
||||
v-decorator="['webhook_token', { initialValue: notificationSettings.webhook_token }]"
|
||||
:placeholder="$t('profile.notifications.webhookTokenPlaceholder') || '用于验证请求的 Bearer Token'"
|
||||
>
|
||||
<a-icon slot="prefix" type="key" />
|
||||
</a-input-password>
|
||||
<div class="field-hint">
|
||||
<a-icon type="info-circle" />
|
||||
<span>{{ $t('profile.notifications.webhookTokenHint') || '将作为 Authorization: Bearer Token 发送到 Webhook' }}</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="savingNotifications" @click="handleSaveNotifications">
|
||||
<a-icon type="save" />
|
||||
{{ $t('common.save') || '保存' }}
|
||||
</a-button>
|
||||
<a-button style="margin-left: 12px" @click="handleTestNotification" :loading="testingNotification">
|
||||
<a-icon type="experiment" />
|
||||
{{ $t('profile.notifications.testBtn') || '发送测试通知' }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- Referral List Tab (邀请列表) -->
|
||||
<a-tab-pane key="referrals" :tab="$t('profile.referral.listTab') || '邀请列表'">
|
||||
<a-table
|
||||
@@ -327,7 +495,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getProfile, updateProfile, getMyCreditsLog, getMyReferrals } from '@/api/user'
|
||||
import { getProfile, updateProfile, getMyCreditsLog, getMyReferrals, getNotificationSettings, updateNotificationSettings } from '@/api/user'
|
||||
import { getSettingsValues } from '@/api/settings'
|
||||
import { baseMixin } from '@/store/app-mixin'
|
||||
|
||||
@@ -383,7 +551,20 @@ export default {
|
||||
feature_costs: {},
|
||||
recharge_telegram_url: ''
|
||||
},
|
||||
rechargeTelegramUrl: 'https://t.me/your_support_bot'
|
||||
rechargeTelegramUrl: 'https://t.me/your_support_bot',
|
||||
// Notification settings
|
||||
notificationSettings: {
|
||||
default_channels: ['browser'],
|
||||
telegram_bot_token: '',
|
||||
telegram_chat_id: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
discord_webhook: '',
|
||||
webhook_url: '',
|
||||
webhook_token: ''
|
||||
},
|
||||
savingNotifications: false,
|
||||
testingNotification: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -456,11 +637,15 @@ export default {
|
||||
if (val === 'referrals' && (!this.referralData.list || this.referralData.list.length === 0)) {
|
||||
this.loadReferrals()
|
||||
}
|
||||
if (val === 'notifications' && !this.notificationSettings.telegram_chat_id && !this.notificationSettings.discord_webhook) {
|
||||
this.loadNotificationSettings()
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.profileForm = this.$form.createForm(this, { name: 'profile' })
|
||||
this.passwordForm = this.$form.createForm(this, { name: 'password' })
|
||||
this.notificationForm = this.$form.createForm(this, { name: 'notification' })
|
||||
},
|
||||
mounted () {
|
||||
this.loadProfile()
|
||||
@@ -486,6 +671,19 @@ export default {
|
||||
this.rechargeTelegramUrl = this.billing.recharge_telegram_url
|
||||
}
|
||||
}
|
||||
// 提取通知设置
|
||||
if (res.data.notification_settings) {
|
||||
this.notificationSettings = {
|
||||
default_channels: res.data.notification_settings.default_channels || ['browser'],
|
||||
telegram_bot_token: res.data.notification_settings.telegram_bot_token || '',
|
||||
telegram_chat_id: res.data.notification_settings.telegram_chat_id || '',
|
||||
email: res.data.notification_settings.email || res.data.email || '',
|
||||
phone: res.data.notification_settings.phone || '',
|
||||
discord_webhook: res.data.notification_settings.discord_webhook || '',
|
||||
webhook_url: res.data.notification_settings.webhook_url || '',
|
||||
webhook_token: res.data.notification_settings.webhook_token || ''
|
||||
}
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.profileForm.setFieldsValue({
|
||||
nickname: this.profile.nickname,
|
||||
@@ -781,6 +979,124 @@ export default {
|
||||
referral_bonus: this.$t('profile.creditsLog.actionReferralBonus') || '邀请奖励'
|
||||
}
|
||||
return labels[action] || action
|
||||
},
|
||||
|
||||
// Notification settings methods
|
||||
async loadNotificationSettings () {
|
||||
try {
|
||||
const res = await getNotificationSettings()
|
||||
if (res.code === 1 && res.data) {
|
||||
this.notificationSettings = {
|
||||
default_channels: res.data.default_channels || ['browser'],
|
||||
telegram_bot_token: res.data.telegram_bot_token || '',
|
||||
telegram_chat_id: res.data.telegram_chat_id || '',
|
||||
email: res.data.email || this.profile.email || '',
|
||||
phone: res.data.phone || '',
|
||||
discord_webhook: res.data.discord_webhook || '',
|
||||
webhook_url: res.data.webhook_url || '',
|
||||
webhook_token: res.data.webhook_token || ''
|
||||
}
|
||||
// Update form values
|
||||
this.$nextTick(() => {
|
||||
this.notificationForm.setFieldsValue({
|
||||
default_channels: this.notificationSettings.default_channels,
|
||||
telegram_bot_token: this.notificationSettings.telegram_bot_token,
|
||||
telegram_chat_id: this.notificationSettings.telegram_chat_id,
|
||||
email: this.notificationSettings.email,
|
||||
phone: this.notificationSettings.phone,
|
||||
discord_webhook: this.notificationSettings.discord_webhook,
|
||||
webhook_url: this.notificationSettings.webhook_url,
|
||||
webhook_token: this.notificationSettings.webhook_token
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// Use default values
|
||||
}
|
||||
},
|
||||
|
||||
handleSaveNotifications () {
|
||||
this.notificationForm.validateFields(async (err, values) => {
|
||||
if (err) return
|
||||
|
||||
this.savingNotifications = true
|
||||
try {
|
||||
const res = await updateNotificationSettings({
|
||||
default_channels: values.default_channels || ['browser'],
|
||||
telegram_bot_token: values.telegram_bot_token || '',
|
||||
telegram_chat_id: values.telegram_chat_id || '',
|
||||
email: values.email || '',
|
||||
phone: values.phone || '',
|
||||
discord_webhook: values.discord_webhook || '',
|
||||
webhook_url: values.webhook_url || '',
|
||||
webhook_token: values.webhook_token || ''
|
||||
})
|
||||
if (res.code === 1) {
|
||||
this.$message.success(this.$t('profile.notifications.saveSuccess') || '通知设置保存成功')
|
||||
this.notificationSettings = res.data || this.notificationSettings
|
||||
} else {
|
||||
this.$message.error(res.msg || '保存失败')
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error('保存失败')
|
||||
} finally {
|
||||
this.savingNotifications = false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async handleTestNotification () {
|
||||
const values = this.notificationForm.getFieldsValue()
|
||||
const channels = values.default_channels || []
|
||||
|
||||
if (channels.length === 0) {
|
||||
this.$message.warning(this.$t('profile.notifications.selectChannel') || '请至少选择一个通知渠道')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if required fields are filled
|
||||
if (channels.includes('telegram')) {
|
||||
if (!values.telegram_bot_token) {
|
||||
this.$message.warning(this.$t('profile.notifications.fillTelegramToken') || '请填写 Telegram Bot Token')
|
||||
return
|
||||
}
|
||||
if (!values.telegram_chat_id) {
|
||||
this.$message.warning(this.$t('profile.notifications.fillTelegram') || '请填写 Telegram Chat ID')
|
||||
return
|
||||
}
|
||||
}
|
||||
if (channels.includes('email') && !values.email) {
|
||||
this.$message.warning(this.$t('profile.notifications.fillEmail') || '请填写通知邮箱')
|
||||
return
|
||||
}
|
||||
|
||||
this.testingNotification = true
|
||||
try {
|
||||
// First save settings, then test
|
||||
const saveRes = await updateNotificationSettings({
|
||||
default_channels: channels,
|
||||
telegram_bot_token: values.telegram_bot_token || '',
|
||||
telegram_chat_id: values.telegram_chat_id || '',
|
||||
email: values.email || '',
|
||||
phone: values.phone || '',
|
||||
discord_webhook: values.discord_webhook || '',
|
||||
webhook_url: values.webhook_url || '',
|
||||
webhook_token: values.webhook_token || ''
|
||||
})
|
||||
|
||||
if (saveRes.code !== 1) {
|
||||
this.$message.error(saveRes.msg || '保存设置失败')
|
||||
return
|
||||
}
|
||||
|
||||
this.$message.info(this.$t('profile.notifications.testSent') || '测试通知已发送,请检查您的通知渠道')
|
||||
// Note: Actual test notification would require a backend endpoint
|
||||
// For now, we just show a success message after saving
|
||||
} catch (e) {
|
||||
this.$message.error('发送测试通知失败')
|
||||
} finally {
|
||||
this.testingNotification = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -959,6 +1275,30 @@ export default {
|
||||
color: #ff4d4f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// Notification settings form
|
||||
.notification-settings-form {
|
||||
.field-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .ant-checkbox-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/deep/ .ant-checkbox-wrapper {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Credits Card 积分卡片
|
||||
|
||||
@@ -146,20 +146,27 @@ export default {
|
||||
if (!time) return '--'
|
||||
|
||||
try {
|
||||
// 只处理时间戳格式(数字或字符串数字)
|
||||
if (typeof time !== 'number' && (typeof time !== 'string' || !/^\d+$/.test(time))) {
|
||||
let date
|
||||
|
||||
if (typeof time === 'number') {
|
||||
// 数字类型:判断是秒级还是毫秒级时间戳
|
||||
const timestampMs = time < 1e12 ? time * 1000 : time
|
||||
date = new Date(timestampMs)
|
||||
} else if (typeof time === 'string') {
|
||||
// 字符串类型
|
||||
if (/^\d+$/.test(time)) {
|
||||
// 纯数字字符串(时间戳)
|
||||
const timestamp = parseInt(time, 10)
|
||||
const timestampMs = timestamp < 1e12 ? timestamp * 1000 : timestamp
|
||||
date = new Date(timestampMs)
|
||||
} else {
|
||||
// ISO 日期字符串或其他格式,直接解析
|
||||
date = new Date(time)
|
||||
}
|
||||
} else {
|
||||
return '--'
|
||||
}
|
||||
|
||||
// 转换为数字
|
||||
const timestamp = typeof time === 'string' ? parseInt(time, 10) : time
|
||||
|
||||
// 判断是秒级还是毫秒级时间戳
|
||||
// 如果时间戳小于 1e12,认为是秒级,需要乘以 1000
|
||||
// 如果大于等于 1e12,认为是毫秒级
|
||||
const timestampMs = timestamp < 1e12 ? timestamp * 1000 : timestamp
|
||||
const date = new Date(timestampMs)
|
||||
|
||||
// 检查日期是否有效
|
||||
if (isNaN(date.getTime())) {
|
||||
return '--'
|
||||
|
||||
@@ -892,63 +892,28 @@
|
||||
>
|
||||
<a-checkbox value="browser">{{ $t('trading-assistant.notify.browser') }}</a-checkbox>
|
||||
<a-checkbox value="email">{{ $t('trading-assistant.notify.email') }}</a-checkbox>
|
||||
<a-checkbox value="phone">{{ $t('trading-assistant.notify.phone') }}</a-checkbox>
|
||||
<a-checkbox value="telegram">{{ $t('trading-assistant.notify.telegram') }}</a-checkbox>
|
||||
<a-checkbox value="discord">{{ $t('trading-assistant.notify.discord') }}</a-checkbox>
|
||||
<a-checkbox value="webhook">{{ $t('trading-assistant.notify.webhook') }}</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
<div class="form-item-hint">{{ $t('trading-assistant.form.notifyChannelsHint') }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="notifyChannelsUi.includes('email')"
|
||||
:label="$t('trading-assistant.form.notifyEmail')"
|
||||
<!-- Notification settings hint -->
|
||||
<a-alert
|
||||
v-if="notifyChannelsUi.includes('telegram') || notifyChannelsUi.includes('email') || notifyChannelsUi.includes('discord')"
|
||||
type="info"
|
||||
showIcon
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<a-input
|
||||
v-decorator="['notify_email', { rules: [{ type: 'email', message: $t('trading-assistant.validation.emailInvalid') }] }]"
|
||||
:placeholder="$t('trading-assistant.placeholders.inputEmail')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="notifyChannelsUi.includes('phone')"
|
||||
:label="$t('trading-assistant.form.notifyPhone')"
|
||||
>
|
||||
<a-input
|
||||
v-decorator="['notify_phone']"
|
||||
:placeholder="$t('trading-assistant.placeholders.inputPhone')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="notifyChannelsUi.includes('telegram')"
|
||||
:label="$t('trading-assistant.form.notifyTelegram')"
|
||||
>
|
||||
<a-input
|
||||
v-decorator="['notify_telegram']"
|
||||
:placeholder="$t('trading-assistant.placeholders.inputTelegram')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="notifyChannelsUi.includes('discord')"
|
||||
:label="$t('trading-assistant.form.notifyDiscord')"
|
||||
>
|
||||
<a-input
|
||||
v-decorator="['notify_discord']"
|
||||
:placeholder="$t('trading-assistant.placeholders.inputDiscord')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="notifyChannelsUi.includes('webhook')"
|
||||
:label="$t('trading-assistant.form.notifyWebhook')"
|
||||
>
|
||||
<a-input
|
||||
v-decorator="['notify_webhook']"
|
||||
:placeholder="$t('trading-assistant.placeholders.inputWebhook')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<template #message>
|
||||
<span>
|
||||
{{ $t('trading-assistant.form.notificationFromProfile') || '通知将发送到您在个人中心配置的地址' }}
|
||||
<router-link to="/profile" style="margin-left: 8px">
|
||||
<a-icon type="setting" /> {{ $t('trading-assistant.form.goToProfile') || '前往配置' }}
|
||||
</router-link>
|
||||
</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
|
||||
<a-divider v-if="executionModeUi === 'live' && canUseLiveTrading" />
|
||||
|
||||
@@ -1267,6 +1232,7 @@
|
||||
import { getStrategyList, startStrategy, stopStrategy, deleteStrategy, updateStrategy, testExchangeConnection, getStrategyEquityCurve, batchCreateStrategies, batchStartStrategies, batchStopStrategies, batchDeleteStrategies } from '@/api/strategy'
|
||||
import { getWatchlist } from '@/api/market'
|
||||
import { listExchangeCredentials, getExchangeCredential, createExchangeCredential } from '@/api/credentials'
|
||||
import { getNotificationSettings } from '@/api/user'
|
||||
import { baseMixin } from '@/store/app-mixin'
|
||||
import request from '@/utils/request'
|
||||
import TradingRecords from './components/TradingRecords.vue'
|
||||
@@ -1563,6 +1529,17 @@ export default {
|
||||
supportedIPs: [], // 白名单IP列表
|
||||
executionModeUi: 'signal',
|
||||
notifyChannelsUi: ['browser'],
|
||||
// User's notification settings from profile
|
||||
userNotificationSettings: {
|
||||
default_channels: ['browser'],
|
||||
telegram_bot_token: '',
|
||||
telegram_chat_id: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
discord_webhook: '',
|
||||
webhook_url: '',
|
||||
webhook_token: ''
|
||||
},
|
||||
// Exchange credentials vault
|
||||
loadingExchangeCredentials: false,
|
||||
exchangeCredentials: [],
|
||||
@@ -1580,11 +1557,32 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
this.loadStrategies()
|
||||
this.loadUserNotificationSettings()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.stopEquityPolling()
|
||||
},
|
||||
methods: {
|
||||
async loadUserNotificationSettings () {
|
||||
// Load user's default notification settings from profile
|
||||
try {
|
||||
const res = await getNotificationSettings()
|
||||
if (res.code === 1 && res.data) {
|
||||
this.userNotificationSettings = {
|
||||
default_channels: res.data.default_channels || ['browser'],
|
||||
telegram_bot_token: res.data.telegram_bot_token || '',
|
||||
telegram_chat_id: res.data.telegram_chat_id || '',
|
||||
email: res.data.email || '',
|
||||
phone: res.data.phone || '',
|
||||
discord_webhook: res.data.discord_webhook || '',
|
||||
webhook_url: res.data.webhook_url || '',
|
||||
webhook_token: res.data.webhook_token || ''
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail, use default values
|
||||
}
|
||||
},
|
||||
async loadWatchlist () {
|
||||
this.loadingWatchlist = true
|
||||
try {
|
||||
@@ -2965,14 +2963,17 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// Use user's notification settings from profile for targets
|
||||
const notificationConfig = {
|
||||
channels: values.notify_channels || [],
|
||||
targets: {
|
||||
email: values.notify_email || '',
|
||||
phone: values.notify_phone || '',
|
||||
telegram: values.notify_telegram || '',
|
||||
discord: values.notify_discord || '',
|
||||
webhook: values.notify_webhook || ''
|
||||
email: this.userNotificationSettings.email || '',
|
||||
phone: this.userNotificationSettings.phone || '',
|
||||
telegram: this.userNotificationSettings.telegram_chat_id || '',
|
||||
telegram_bot_token: this.userNotificationSettings.telegram_bot_token || '',
|
||||
discord: this.userNotificationSettings.discord_webhook || '',
|
||||
webhook: this.userNotificationSettings.webhook_url || '',
|
||||
webhook_token: this.userNotificationSettings.webhook_token || ''
|
||||
}
|
||||
}
|
||||
if (!notificationConfig.channels || notificationConfig.channels.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user