feat: add Portfolio Management module with AI monitoring
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Portfolio API - Manual positions and monitoring
|
||||
*/
|
||||
import request from '@/utils/request'
|
||||
|
||||
// ==================== Positions ====================
|
||||
|
||||
export function getPositions () {
|
||||
return request({
|
||||
url: '/api/portfolio/positions',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addPosition (data) {
|
||||
return request({
|
||||
url: '/api/portfolio/positions',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updatePosition (id, data) {
|
||||
return request({
|
||||
url: `/api/portfolio/positions/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deletePosition (id) {
|
||||
return request({
|
||||
url: `/api/portfolio/positions/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function getPortfolioSummary () {
|
||||
return request({
|
||||
url: '/api/portfolio/summary',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Monitors ====================
|
||||
|
||||
export function getMonitors () {
|
||||
return request({
|
||||
url: '/api/portfolio/monitors',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addMonitor (data) {
|
||||
return request({
|
||||
url: '/api/portfolio/monitors',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateMonitor (id, data) {
|
||||
return request({
|
||||
url: `/api/portfolio/monitors/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteMonitor (id) {
|
||||
return request({
|
||||
url: `/api/portfolio/monitors/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function runMonitor (id, params = {}) {
|
||||
return request({
|
||||
url: `/api/portfolio/monitors/${id}/run`,
|
||||
method: 'post',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Alerts ====================
|
||||
|
||||
export function getAlerts () {
|
||||
return request({
|
||||
url: '/api/portfolio/alerts',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addAlert (data) {
|
||||
return request({
|
||||
url: '/api/portfolio/alerts',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateAlert (id, data) {
|
||||
return request({
|
||||
url: `/api/portfolio/alerts/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAlert (id) {
|
||||
return request({
|
||||
url: `/api/portfolio/alerts/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Groups ====================
|
||||
|
||||
export function getGroups () {
|
||||
return request({
|
||||
url: '/api/portfolio/groups',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function renameGroup (data) {
|
||||
return request({
|
||||
url: '/api/portfolio/groups/rename',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Market (reuse from market.js) ====================
|
||||
|
||||
export function searchSymbols (data) {
|
||||
return request({
|
||||
url: '/api/market/symbols/search',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getMarketTypes () {
|
||||
return request({
|
||||
url: '/api/market/types',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div :class="wrpCls">
|
||||
<!-- User avatar/name removed for local OSS build -->
|
||||
<notice-icon :class="prefixCls" />
|
||||
<select-lang :class="prefixCls" />
|
||||
<a-tooltip :title="$t('app.setting.tooltip')">
|
||||
<span :class="prefixCls" @click="handleSettingClick">
|
||||
@@ -12,11 +13,13 @@
|
||||
|
||||
<script>
|
||||
import SelectLang from '@/components/SelectLang'
|
||||
import NoticeIcon from '@/components/NoticeIcon'
|
||||
|
||||
export default {
|
||||
name: 'RightContent',
|
||||
components: {
|
||||
SelectLang
|
||||
SelectLang,
|
||||
NoticeIcon
|
||||
},
|
||||
props: {
|
||||
prefixCls: {
|
||||
|
||||
@@ -1,90 +1,789 @@
|
||||
<template>
|
||||
<a-popover
|
||||
v-model="visible"
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
overlayClassName="header-notice-wrapper"
|
||||
:getPopupContainer="() => $refs.noticeRef.parentElement"
|
||||
:autoAdjustOverflow="true"
|
||||
:arrowPointAtCenter="true"
|
||||
:overlayStyle="{ width: '300px', top: '50px' }"
|
||||
>
|
||||
<template slot="content">
|
||||
<a-spin :spinning="loading">
|
||||
<a-tabs>
|
||||
<a-tab-pane tab="通知" key="1">
|
||||
<a-list>
|
||||
<a-list-item>
|
||||
<a-list-item-meta title="你收到了 14 份新周报" description="一年前">
|
||||
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
|
||||
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
|
||||
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="消息" key="2">
|
||||
123
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="待办" key="3">
|
||||
123
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-spin>
|
||||
</template>
|
||||
<span @click="fetchNotice" class="header-notice" ref="noticeRef" style="padding: 0 18px">
|
||||
<a-badge count="12">
|
||||
<a-icon style="font-size: 16px; padding: 4px" type="bell" />
|
||||
</a-badge>
|
||||
</span>
|
||||
</a-popover>
|
||||
<div class="notice-icon-wrapper">
|
||||
<a-popover
|
||||
v-model="visible"
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
overlayClassName="header-notice-wrapper"
|
||||
:getPopupContainer="() => $refs.noticeRef.parentElement"
|
||||
:autoAdjustOverflow="true"
|
||||
:arrowPointAtCenter="true"
|
||||
:overlayStyle="{ width: '380px', top: '50px' }"
|
||||
>
|
||||
<template slot="content">
|
||||
<div class="notice-header">
|
||||
<span class="notice-title">{{ $t('notice.title') }}</span>
|
||||
<a v-if="notifications.length > 0" @click="markAllRead" class="notice-action">
|
||||
{{ $t('notice.markAllRead') }}
|
||||
</a>
|
||||
</div>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="notice-list" v-if="notifications.length > 0">
|
||||
<div
|
||||
v-for="item in notifications"
|
||||
:key="item.id"
|
||||
class="notice-item"
|
||||
:class="{ unread: !item.is_read }"
|
||||
@click="handleNoticeClick(item)"
|
||||
>
|
||||
<div class="notice-item-icon">
|
||||
<a-icon :type="getNoticeIcon(item.signal_type)" :style="{ color: getNoticeColor(item.signal_type) }" />
|
||||
</div>
|
||||
<div class="notice-item-content">
|
||||
<div class="notice-item-title">{{ item.title }}</div>
|
||||
<div class="notice-item-desc">{{ truncateMessage(item.message) }}</div>
|
||||
<div class="notice-item-time">{{ formatTime(item.created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notice-empty" v-else>
|
||||
<a-empty :description="$t('notice.empty')" />
|
||||
</div>
|
||||
</a-spin>
|
||||
<div class="notice-footer" v-if="notifications.length > 0">
|
||||
<a @click="clearNotifications">{{ $t('notice.clear') }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<span @click="fetchNotice" class="header-notice" ref="noticeRef">
|
||||
<a-badge :count="unreadCount" :overflowCount="99">
|
||||
<a-icon style="font-size: 16px; padding: 4px" type="bell" />
|
||||
</a-badge>
|
||||
</span>
|
||||
</a-popover>
|
||||
|
||||
<!-- 通知详情弹窗 -->
|
||||
<a-modal
|
||||
v-model="detailVisible"
|
||||
:title="detailNotice ? detailNotice.title : ''"
|
||||
:footer="null"
|
||||
:width="isHtmlReport ? 900 : 600"
|
||||
:wrapClassName="isHtmlReport ? 'notice-detail-modal html-report-modal' : 'notice-detail-modal'"
|
||||
centered
|
||||
>
|
||||
<div v-if="detailNotice" class="notice-detail">
|
||||
<div class="notice-detail-meta">
|
||||
<div class="notice-detail-type">
|
||||
<a-icon :type="getNoticeIcon(detailNotice.signal_type)" :style="{ color: getNoticeColor(detailNotice.signal_type) }" />
|
||||
<span class="type-label">{{ getNoticeTypeLabel(detailNotice.signal_type) }}</span>
|
||||
</div>
|
||||
<div class="notice-detail-time">
|
||||
<a-icon type="clock-circle" />
|
||||
<span>{{ formatFullTime(detailNotice.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<!-- 消息内容 - 支持 HTML 报告或 Markdown 格式 -->
|
||||
<div class="notice-detail-content" :class="{ 'html-report': isHtmlReport }">
|
||||
<div v-html="formatMessageHtml(detailNotice.message)" class="message-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- 如果有额外的 payload 信息(非 HTML 报告时显示) -->
|
||||
<template v-if="!isHtmlReport && detailNotice.payload && Object.keys(detailNotice.payload).length > 0">
|
||||
<a-divider />
|
||||
<div class="notice-detail-extra">
|
||||
<div class="extra-title">{{ $t('notice.detailInfo') }}</div>
|
||||
|
||||
<!-- AI分析结果 -->
|
||||
<template v-if="detailNotice.signal_type === 'ai_monitor'">
|
||||
<div v-if="detailNotice.payload.final_decision" class="extra-item decision">
|
||||
<span class="label">{{ $t('notice.aiDecision') }}:</span>
|
||||
<a-tag :color="getDecisionColor(detailNotice.payload.final_decision)">
|
||||
{{ detailNotice.payload.final_decision }}
|
||||
</a-tag>
|
||||
<span v-if="detailNotice.payload.confidence" class="confidence">
|
||||
({{ $t('notice.confidence') }}: {{ detailNotice.payload.confidence }}%)
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="detailNotice.payload.reasoning" class="extra-item">
|
||||
<span class="label">{{ $t('notice.reasoning') }}:</span>
|
||||
<span class="value">{{ detailNotice.payload.reasoning }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 价格提醒 -->
|
||||
<template v-if="detailNotice.signal_type === 'price_alert'">
|
||||
<div v-if="detailNotice.payload.symbol" class="extra-item">
|
||||
<span class="label">{{ $t('notice.symbol') }}:</span>
|
||||
<span class="value">{{ detailNotice.payload.symbol }}</span>
|
||||
</div>
|
||||
<div v-if="detailNotice.payload.price" class="extra-item">
|
||||
<span class="label">{{ $t('notice.currentPrice') }}:</span>
|
||||
<span class="value">${{ detailNotice.payload.price }}</span>
|
||||
</div>
|
||||
<div v-if="detailNotice.payload.trigger_price" class="extra-item">
|
||||
<span class="label">{{ $t('notice.triggerPrice') }}:</span>
|
||||
<span class="value">${{ detailNotice.payload.trigger_price }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 交易信号 -->
|
||||
<template v-if="detailNotice.signal_type === 'signal' || detailNotice.signal_type === 'trade'">
|
||||
<div v-if="detailNotice.payload.symbol" class="extra-item">
|
||||
<span class="label">{{ $t('notice.symbol') }}:</span>
|
||||
<span class="value">{{ detailNotice.payload.symbol }}</span>
|
||||
</div>
|
||||
<div v-if="detailNotice.payload.action" class="extra-item">
|
||||
<span class="label">{{ $t('notice.action') }}:</span>
|
||||
<a-tag :color="detailNotice.payload.action === 'BUY' ? 'green' : 'red'">
|
||||
{{ detailNotice.payload.action }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div v-if="detailNotice.payload.quantity" class="extra-item">
|
||||
<span class="label">{{ $t('notice.quantity') }}:</span>
|
||||
<span class="value">{{ detailNotice.payload.quantity }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="notice-detail-actions">
|
||||
<a-button v-if="detailNotice.payload && detailNotice.payload.monitor_id" type="primary" @click="goToPortfolio">
|
||||
<a-icon type="fund" />
|
||||
{{ $t('notice.viewPortfolio') }}
|
||||
</a-button>
|
||||
<a-button @click="detailVisible = false">
|
||||
{{ $t('notice.close') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getStrategyNotifications } from '@/api/strategy'
|
||||
import request from '@/utils/request'
|
||||
|
||||
export default {
|
||||
name: 'HeaderNotice',
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
visible: false
|
||||
visible: false,
|
||||
detailVisible: false,
|
||||
detailNotice: null,
|
||||
notifications: [],
|
||||
lastFetchId: 0,
|
||||
pollingTimer: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
unreadCount () {
|
||||
return this.notifications.filter(n => !n.is_read).length
|
||||
},
|
||||
isHtmlReport () {
|
||||
if (!this.detailNotice || !this.detailNotice.message) return false
|
||||
return this.detailNotice.message.includes('<div class="qd-report">') ||
|
||||
this.detailNotice.message.includes('<style>')
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchNotifications()
|
||||
this.startPolling()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.stopPolling()
|
||||
},
|
||||
methods: {
|
||||
fetchNotice () {
|
||||
if (!this.visible) {
|
||||
startPolling () {
|
||||
this.stopPolling()
|
||||
// 每30秒轮询一次
|
||||
this.pollingTimer = setInterval(() => {
|
||||
this.fetchNotifications(true)
|
||||
}, 30000)
|
||||
},
|
||||
stopPolling () {
|
||||
if (this.pollingTimer) {
|
||||
clearInterval(this.pollingTimer)
|
||||
this.pollingTimer = null
|
||||
}
|
||||
},
|
||||
async fetchNotifications (silent = false) {
|
||||
if (!silent) {
|
||||
this.loading = true
|
||||
setTimeout(() => {
|
||||
this.loading = false
|
||||
}, 2000)
|
||||
} else {
|
||||
}
|
||||
try {
|
||||
const res = await getStrategyNotifications({ limit: 50 })
|
||||
if (res.code === 1 && res.data?.items) {
|
||||
// 解析 payload_json 如果是字符串
|
||||
this.notifications = res.data.items.map(item => {
|
||||
let payload = item.payload_json
|
||||
if (typeof payload === 'string') {
|
||||
try {
|
||||
payload = JSON.parse(payload)
|
||||
} catch (e) {
|
||||
payload = {}
|
||||
}
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
payload,
|
||||
is_read: item.is_read === 1 || item.is_read === true
|
||||
}
|
||||
})
|
||||
if (this.notifications.length > 0) {
|
||||
this.lastFetchId = Math.max(...this.notifications.map(n => n.id))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch notifications:', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
fetchNotice () {
|
||||
if (!this.visible) {
|
||||
this.fetchNotifications()
|
||||
}
|
||||
this.visible = !this.visible
|
||||
},
|
||||
getNoticeIcon (signalType) {
|
||||
const iconMap = {
|
||||
'ai_monitor': 'robot',
|
||||
'price_alert': 'bell',
|
||||
'signal': 'thunderbolt',
|
||||
'buy': 'rise',
|
||||
'sell': 'fall',
|
||||
'hold': 'pause-circle',
|
||||
'trade': 'swap'
|
||||
}
|
||||
return iconMap[signalType] || 'notification'
|
||||
},
|
||||
getNoticeColor (signalType) {
|
||||
const colorMap = {
|
||||
'ai_monitor': '#722ed1',
|
||||
'price_alert': '#faad14',
|
||||
'signal': '#1890ff',
|
||||
'buy': '#52c41a',
|
||||
'sell': '#f5222d',
|
||||
'hold': '#faad14',
|
||||
'trade': '#13c2c2'
|
||||
}
|
||||
return colorMap[signalType] || '#1890ff'
|
||||
},
|
||||
getNoticeTypeLabel (signalType) {
|
||||
const labelMap = {
|
||||
'ai_monitor': this.$t('notice.type.aiMonitor'),
|
||||
'price_alert': this.$t('notice.type.priceAlert'),
|
||||
'signal': this.$t('notice.type.signal'),
|
||||
'buy': this.$t('notice.type.buy'),
|
||||
'sell': this.$t('notice.type.sell'),
|
||||
'hold': this.$t('notice.type.hold'),
|
||||
'trade': this.$t('notice.type.trade')
|
||||
}
|
||||
return labelMap[signalType] || this.$t('notice.type.notification')
|
||||
},
|
||||
getDecisionColor (decision) {
|
||||
const colorMap = {
|
||||
'BUY': 'green',
|
||||
'SELL': 'red',
|
||||
'HOLD': 'orange'
|
||||
}
|
||||
return colorMap[decision] || 'blue'
|
||||
},
|
||||
truncateMessage (message) {
|
||||
if (!message) return ''
|
||||
return message.length > 80 ? message.substring(0, 80) + '...' : message
|
||||
},
|
||||
formatTime (timestamp) {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(timestamp * 1000)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
const days = Math.floor(diff / 86400000)
|
||||
|
||||
if (minutes < 1) {
|
||||
return this.$t('notice.justNow')
|
||||
} else if (minutes < 60) {
|
||||
return `${minutes} ${this.$t('notice.minutesAgo')}`
|
||||
} else if (hours < 24) {
|
||||
return `${hours} ${this.$t('notice.hoursAgo')}`
|
||||
} else if (days < 7) {
|
||||
return `${days} ${this.$t('notice.daysAgo')}`
|
||||
} else {
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
},
|
||||
formatFullTime (timestamp) {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(timestamp * 1000)
|
||||
return date.toLocaleString()
|
||||
},
|
||||
formatMessageHtml (message) {
|
||||
if (!message) return ''
|
||||
|
||||
// 检查是否已经是 HTML 格式(AI Monitor 的报告)
|
||||
if (message.includes('<div class="qd-report">') || message.includes('<style>')) {
|
||||
// 已经是 HTML,直接返回
|
||||
return message
|
||||
}
|
||||
|
||||
// 简单的 Markdown 转换
|
||||
const html = message
|
||||
// 转义 HTML
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
// 标题
|
||||
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
|
||||
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^# (.+)$/gm, '<h2>$1</h2>')
|
||||
// 粗体
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
// 斜体
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
// 列表项
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
// 换行
|
||||
.replace(/\n/g, '<br>')
|
||||
return html
|
||||
},
|
||||
handleNoticeClick (item) {
|
||||
// 标记为已读
|
||||
this.markAsRead(item.id)
|
||||
// 打开详情弹窗
|
||||
this.detailNotice = item
|
||||
this.detailVisible = true
|
||||
this.visible = false
|
||||
},
|
||||
goToPortfolio () {
|
||||
this.detailVisible = false
|
||||
this.$router.push({ path: '/portfolio' }).catch(() => {})
|
||||
},
|
||||
async markAsRead (id) {
|
||||
const item = this.notifications.find(n => n.id === id)
|
||||
if (item) {
|
||||
item.is_read = true
|
||||
}
|
||||
// 调用后端API标记已读
|
||||
try {
|
||||
await request({
|
||||
url: '/api/strategies/notifications/read',
|
||||
method: 'post',
|
||||
data: { id }
|
||||
})
|
||||
} catch (e) {
|
||||
// 忽略错误,前端已标记
|
||||
}
|
||||
},
|
||||
async markAllRead () {
|
||||
this.notifications.forEach(n => { n.is_read = true })
|
||||
try {
|
||||
await request({
|
||||
url: '/api/strategies/notifications/read-all',
|
||||
method: 'post'
|
||||
})
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
},
|
||||
async clearNotifications () {
|
||||
this.notifications = []
|
||||
try {
|
||||
await request({
|
||||
url: '/api/strategies/notifications/clear',
|
||||
method: 'delete'
|
||||
})
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
this.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.header-notice-wrapper {
|
||||
top: 50px !important;
|
||||
}
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
.header-notice{
|
||||
display: inline-block;
|
||||
transition: all 0.3s;
|
||||
.notice-icon-wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: initial;
|
||||
.header-notice {
|
||||
display: inline-block;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.notice-title {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.notice-action {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
display: flex;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background: #e6f7ff;
|
||||
|
||||
&:hover {
|
||||
background: #bae7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-item-icon {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f0f0f0;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.notice-item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.notice-item-title {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.notice-item-desc {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.notice-item-time {
|
||||
font-size: 11px;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-empty {
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.notice-footer {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
a {
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 详情弹窗内容 */
|
||||
.notice-detail {
|
||||
.notice-detail-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.notice-detail-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.type-label {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
}
|
||||
|
||||
.notice-detail-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
.notice-detail-content {
|
||||
.message-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
|
||||
h2, h3, h4 {
|
||||
margin: 12px 0 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 { font-size: 18px; }
|
||||
h3 { font-size: 16px; }
|
||||
h4 { font-size: 14px; }
|
||||
|
||||
li {
|
||||
margin-left: 20px;
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// HTML 报告样式
|
||||
&.html-report {
|
||||
.message-body {
|
||||
max-height: 70vh;
|
||||
padding: 0;
|
||||
margin: -16px -24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-detail-extra {
|
||||
.extra-title {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.extra-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
|
||||
.label {
|
||||
flex-shrink: 0;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&.decision {
|
||||
align-items: center;
|
||||
|
||||
.confidence {
|
||||
margin-left: 8px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-detail-actions {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.header-notice-wrapper {
|
||||
top: 50px !important;
|
||||
|
||||
.ant-popover-inner-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 详情弹窗样式 */
|
||||
.notice-detail-modal {
|
||||
.ant-modal-header {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
// HTML 报告模式
|
||||
&.html-report-modal {
|
||||
.ant-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗黑主题支持 */
|
||||
body.dark,
|
||||
body.realdark,
|
||||
.ant-layout.dark,
|
||||
.ant-layout.realdark {
|
||||
.header-notice-wrapper {
|
||||
.ant-popover-inner {
|
||||
background: #1f1f1f;
|
||||
}
|
||||
|
||||
.ant-popover-arrow {
|
||||
border-color: #1f1f1f;
|
||||
}
|
||||
|
||||
.notice-header {
|
||||
border-color: #303030;
|
||||
|
||||
.notice-title {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
&:hover {
|
||||
background: #303030;
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background: rgba(24, 144, 255, 0.15);
|
||||
|
||||
&:hover {
|
||||
background: rgba(24, 144, 255, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.notice-item-icon {
|
||||
background: #303030;
|
||||
}
|
||||
|
||||
.notice-item-content {
|
||||
.notice-item-title {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.notice-item-desc {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.notice-item-time {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-footer {
|
||||
border-color: #303030;
|
||||
}
|
||||
|
||||
.ant-empty-description {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
/* 详情弹窗暗黑主题 */
|
||||
.notice-detail-modal {
|
||||
.ant-modal-content {
|
||||
background: #1f1f1f;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
background: #1f1f1f;
|
||||
border-color: #303030;
|
||||
|
||||
.ant-modal-title {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-close-x {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.ant-divider {
|
||||
border-color: #303030;
|
||||
}
|
||||
|
||||
.notice-detail {
|
||||
.notice-detail-meta {
|
||||
.notice-detail-type .type-label {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.notice-detail-time {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
.notice-detail-content .message-body {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.notice-detail-extra {
|
||||
.extra-title {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.extra-item {
|
||||
.label {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.confidence {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,6 +37,13 @@ export const asyncRouterMap = [
|
||||
component: () => import('@/views/trading-assistant'),
|
||||
meta: { title: 'menu.dashboard.tradingAssistant', keepAlive: true, icon: 'robot', permission: ['dashboard'] }
|
||||
},
|
||||
// 资产监测
|
||||
{
|
||||
path: '/portfolio',
|
||||
name: 'Portfolio',
|
||||
component: () => import('@/views/portfolio'),
|
||||
meta: { title: 'menu.dashboard.portfolio', keepAlive: true, icon: 'fund', permission: ['dashboard'] }
|
||||
},
|
||||
// 指标社区(keepAlive disabled intentionally for iframe page)
|
||||
{
|
||||
path: '/indicator-community',
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
Space,
|
||||
Empty,
|
||||
Rate,
|
||||
AutoComplete,
|
||||
message,
|
||||
notification
|
||||
} from 'ant-design-vue'
|
||||
@@ -107,6 +108,7 @@ Vue.use(Descriptions)
|
||||
Vue.use(Space)
|
||||
Vue.use(Empty)
|
||||
Vue.use(Rate)
|
||||
Vue.use(AutoComplete)
|
||||
// Textarea 是 Input 组件的一部分,通过 Vue.use(Input) 已自动注册
|
||||
|
||||
Vue.prototype.$confirm = Modal.confirm
|
||||
|
||||
@@ -8,6 +8,15 @@ const components = {
|
||||
}
|
||||
|
||||
const locale = {
|
||||
// Common
|
||||
'common.confirm': 'Confirm',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.save': 'Save',
|
||||
'common.delete': 'Delete',
|
||||
'common.edit': 'Edit',
|
||||
'common.add': 'Add',
|
||||
'common.close': 'Close',
|
||||
'common.ok': 'OK',
|
||||
'submit': 'Submit',
|
||||
'save': 'Save',
|
||||
'submit.ok': 'Submit successfully',
|
||||
@@ -19,6 +28,7 @@ const locale = {
|
||||
'menu.dashboard.indicator': 'Indicator Analysis',
|
||||
'menu.dashboard.community': 'Indicator Community',
|
||||
'menu.dashboard.tradingAssistant': 'Trading Assistant',
|
||||
'menu.dashboard.portfolio': 'Portfolio',
|
||||
'menu.settings': 'Settings',
|
||||
'menu.dashboard.aiTradingAssistant': 'AI Trading Assistant',
|
||||
'menu.dashboard.signalRobot': 'Signal Robot',
|
||||
@@ -97,6 +107,36 @@ const locale = {
|
||||
'app.setting.themecolor.geekblue': 'Geek Blue',
|
||||
'app.setting.themecolor.purple': 'Golden Purple',
|
||||
'app.setting.tooltip': 'Page Settings',
|
||||
|
||||
// Notification Center
|
||||
'notice.title': 'Notifications',
|
||||
'notice.empty': 'No notifications',
|
||||
'notice.markAllRead': 'Mark all as read',
|
||||
'notice.clear': 'Clear all',
|
||||
'notice.close': 'Close',
|
||||
'notice.justNow': 'Just now',
|
||||
'notice.minutesAgo': 'minutes ago',
|
||||
'notice.hoursAgo': 'hours ago',
|
||||
'notice.daysAgo': 'days ago',
|
||||
'notice.detailInfo': 'Details',
|
||||
'notice.aiDecision': 'AI Decision',
|
||||
'notice.confidence': 'Confidence',
|
||||
'notice.reasoning': 'Reasoning',
|
||||
'notice.symbol': 'Symbol',
|
||||
'notice.currentPrice': 'Current Price',
|
||||
'notice.triggerPrice': 'Trigger Price',
|
||||
'notice.action': 'Action',
|
||||
'notice.quantity': 'Quantity',
|
||||
'notice.viewPortfolio': 'View Portfolio',
|
||||
'notice.type.aiMonitor': 'AI Monitor',
|
||||
'notice.type.priceAlert': 'Price Alert',
|
||||
'notice.type.signal': 'Trade Signal',
|
||||
'notice.type.buy': 'Buy Signal',
|
||||
'notice.type.sell': 'Sell Signal',
|
||||
'notice.type.hold': 'Hold Suggestion',
|
||||
'notice.type.trade': 'Trade Execution',
|
||||
'notice.type.notification': 'Notification',
|
||||
|
||||
'user.login.userName': 'userName',
|
||||
'user.login.password': 'password',
|
||||
'user.login.username.placeholder': 'Account: admin',
|
||||
@@ -2049,7 +2089,140 @@ const locale = {
|
||||
'settings.desc.MAKER_WAIT_SEC': 'Wait time for limit order fill before switching to market order',
|
||||
'settings.desc.MAKER_OFFSET_BPS': 'Price offset in basis points. Buy: price*(1-offset), Sell: price*(1+offset)',
|
||||
'settings.desc.TIINGO_API_KEY': 'Tiingo API key for Forex/Metals data (free tier does not support 1-minute data)',
|
||||
'settings.desc.TIINGO_TIMEOUT': 'Tiingo API request timeout'
|
||||
'settings.desc.TIINGO_TIMEOUT': 'Tiingo API request timeout',
|
||||
|
||||
// Portfolio
|
||||
'portfolio.summary.totalValue': 'Total Value',
|
||||
'portfolio.summary.totalCost': 'Total Cost',
|
||||
'portfolio.summary.totalPnl': 'Total P&L',
|
||||
'portfolio.summary.positionCount': 'Positions',
|
||||
'portfolio.summary.profitLossRatio': 'Profit/Loss',
|
||||
'portfolio.summary.today': 'Today',
|
||||
'portfolio.summary.todayPnl': 'Today P&L',
|
||||
'portfolio.summary.bestPerformer': 'Best Performer',
|
||||
'portfolio.summary.worstPerformer': 'Worst Performer',
|
||||
'portfolio.summary.priceSync': 'Price Sync',
|
||||
'portfolio.summary.syncInterval': 'Refresh',
|
||||
'portfolio.summary.justNow': 'Just now',
|
||||
'portfolio.summary.ago': ' ago',
|
||||
'portfolio.positions.title': 'My Positions',
|
||||
'portfolio.positions.add': 'Add Position',
|
||||
'portfolio.positions.addFirst': 'Add Your First Position',
|
||||
'portfolio.positions.empty': 'No positions yet',
|
||||
'portfolio.positions.deleteConfirm': 'Are you sure to delete this position?',
|
||||
'portfolio.positions.currentPrice': 'Current',
|
||||
'portfolio.positions.entryPrice': 'Entry',
|
||||
'portfolio.positions.quantity': 'Quantity',
|
||||
'portfolio.positions.side': 'Side',
|
||||
'portfolio.positions.long': 'Long',
|
||||
'portfolio.positions.short': 'Short',
|
||||
'portfolio.positions.marketValue': 'Value',
|
||||
'portfolio.positions.pnl': 'P&L',
|
||||
'portfolio.positions.items': 'positions',
|
||||
'portfolio.monitors.title': 'AI Monitors',
|
||||
'portfolio.monitors.add': 'Add Monitor',
|
||||
'portfolio.monitors.addFirst': 'Add AI Monitor',
|
||||
'portfolio.monitors.empty': 'No monitors yet',
|
||||
'portfolio.monitors.deleteConfirm': 'Are you sure to delete this monitor?',
|
||||
'portfolio.monitors.interval': 'Interval',
|
||||
'portfolio.monitors.lastRun': 'Last Run',
|
||||
'portfolio.monitors.nextRun': 'Next Run',
|
||||
'portfolio.monitors.channels': 'Channels',
|
||||
'portfolio.monitors.runNow': 'Run Now',
|
||||
'portfolio.monitors.analysisResult': 'AI Analysis Result',
|
||||
'portfolio.modal.addPosition': 'Add Position',
|
||||
'portfolio.modal.editPosition': 'Edit Position',
|
||||
'portfolio.modal.addMonitor': 'Add Monitor',
|
||||
'portfolio.modal.editMonitor': 'Edit Monitor',
|
||||
'portfolio.form.market': 'Market',
|
||||
'portfolio.form.marketRequired': 'Please select a market',
|
||||
'portfolio.form.selectMarket': 'Select Market',
|
||||
'portfolio.form.symbol': 'Symbol',
|
||||
'portfolio.form.symbolRequired': 'Please enter symbol',
|
||||
'portfolio.form.searchSymbol': 'Search or enter symbol',
|
||||
'portfolio.form.useAsSymbol': 'Use',
|
||||
'portfolio.form.asSymbolCode': 'as symbol code',
|
||||
'portfolio.form.symbolHint': 'Search symbols or enter any code directly',
|
||||
'portfolio.form.side': 'Side',
|
||||
'portfolio.form.quantity': 'Quantity',
|
||||
'portfolio.form.quantityRequired': 'Please enter quantity',
|
||||
'portfolio.form.enterQuantity': 'Enter quantity',
|
||||
'portfolio.form.entryPrice': 'Entry Price',
|
||||
'portfolio.form.entryPriceRequired': 'Please enter entry price',
|
||||
'portfolio.form.enterEntryPrice': 'Enter entry price',
|
||||
'portfolio.form.notes': 'Notes',
|
||||
'portfolio.form.enterNotes': 'Optional: Add notes',
|
||||
'portfolio.form.monitorName': 'Monitor Name',
|
||||
'portfolio.form.monitorNameRequired': 'Please enter monitor name',
|
||||
'portfolio.form.enterMonitorName': 'e.g. Daily Portfolio Analysis',
|
||||
'portfolio.form.interval': 'Interval',
|
||||
'portfolio.form.minutes': 'minutes',
|
||||
'portfolio.form.hour': 'hour',
|
||||
'portfolio.form.hours': 'hours',
|
||||
'portfolio.form.notifyChannels': 'Notify Channels',
|
||||
'portfolio.form.browser': 'Browser',
|
||||
'portfolio.form.email': 'Email',
|
||||
'portfolio.form.telegramChatId': 'Telegram Chat ID',
|
||||
'portfolio.form.enterTelegramChatId': 'Enter Telegram Chat ID',
|
||||
'portfolio.form.telegramRequired': 'Please enter Telegram Chat ID',
|
||||
'portfolio.form.emailAddress': 'Email Address',
|
||||
'portfolio.form.enterEmail': 'Enter email address',
|
||||
'portfolio.form.emailRequired': 'Please enter email address',
|
||||
'portfolio.form.emailInvalid': 'Please enter a valid email address',
|
||||
'portfolio.form.customPrompt': 'Custom Prompt',
|
||||
'portfolio.form.customPromptPlaceholder': 'Optional: Add focus areas, e.g. "Focus on tech stock risks"',
|
||||
'portfolio.form.monitorScope': 'Monitor Scope',
|
||||
'portfolio.form.allPositions': 'All Positions',
|
||||
'portfolio.form.selectedPositions': 'Selected Positions',
|
||||
'portfolio.form.selectPositions': 'Select Positions',
|
||||
'portfolio.form.selectAll': 'Select All',
|
||||
'portfolio.form.deselectAll': 'Deselect All',
|
||||
'portfolio.form.selectedCount': '{count} of {total} selected',
|
||||
'portfolio.form.pleaseSelectPositions': 'Please select at least one position to monitor',
|
||||
'portfolio.message.loadFailed': 'Failed to load data',
|
||||
'portfolio.message.saveSuccess': 'Saved successfully',
|
||||
'portfolio.message.saveFailed': 'Failed to save',
|
||||
'portfolio.message.deleteSuccess': 'Deleted successfully',
|
||||
'portfolio.message.deleteFailed': 'Failed to delete',
|
||||
'portfolio.message.updateFailed': 'Failed to update',
|
||||
'portfolio.message.monitorEnabled': 'Monitor enabled',
|
||||
'portfolio.message.monitorDisabled': 'Monitor paused',
|
||||
'portfolio.message.monitorRunSuccess': 'Analysis completed',
|
||||
'portfolio.message.monitorRunFailed': 'Analysis failed',
|
||||
// Portfolio - Groups
|
||||
'portfolio.groups.all': 'All Positions',
|
||||
'portfolio.groups.ungrouped': 'Ungrouped',
|
||||
'portfolio.form.group': 'Group',
|
||||
'portfolio.form.enterGroup': 'Enter or select group',
|
||||
// Portfolio - Alerts
|
||||
'portfolio.alerts.title': 'Price/PnL Alerts',
|
||||
'portfolio.alerts.addAlert': 'Add Alert',
|
||||
'portfolio.alerts.editAlert': 'Edit Alert',
|
||||
'portfolio.alerts.alertType': 'Alert Type',
|
||||
'portfolio.alerts.priceAbove': 'Price Above',
|
||||
'portfolio.alerts.priceBelow': 'Price Below',
|
||||
'portfolio.alerts.pnlAbove': 'Profit Above (%)',
|
||||
'portfolio.alerts.pnlBelow': 'Loss Below (%)',
|
||||
'portfolio.alerts.threshold': 'Threshold',
|
||||
'portfolio.alerts.thresholdRequired': 'Please enter threshold',
|
||||
'portfolio.alerts.enterPrice': 'Enter price',
|
||||
'portfolio.alerts.enterPercent': 'Enter percentage',
|
||||
'portfolio.alerts.currentPrice': 'Current Price',
|
||||
'portfolio.alerts.currentPriceHint': 'Current price',
|
||||
'portfolio.alerts.repeatInterval': 'Repeat Alert',
|
||||
'portfolio.alerts.noRepeat': 'No repeat (trigger once)',
|
||||
'portfolio.alerts.every5min': 'Every 5 minutes',
|
||||
'portfolio.alerts.every15min': 'Every 15 minutes',
|
||||
'portfolio.alerts.every30min': 'Every 30 minutes',
|
||||
'portfolio.alerts.every1hour': 'Every 1 hour',
|
||||
'portfolio.alerts.every4hours': 'Every 4 hours',
|
||||
'portfolio.alerts.onceDaily': 'Once daily',
|
||||
'portfolio.alerts.enabled': 'Enable Alert',
|
||||
'portfolio.alerts.enabledDesc': 'Auto-monitor and trigger notifications',
|
||||
'portfolio.alerts.delete': 'Delete',
|
||||
'portfolio.alerts.deleteConfirm': 'Are you sure you want to delete this alert?',
|
||||
'portfolio.modal.addAlert': 'Add Alert',
|
||||
'portfolio.modal.editAlert': 'Edit Alert'
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -8,6 +8,15 @@ momentLocale: momentCN
|
||||
}
|
||||
|
||||
const locale = {
|
||||
// 通用
|
||||
'common.confirm': '确定',
|
||||
'common.cancel': '取消',
|
||||
'common.save': '保存',
|
||||
'common.delete': '删除',
|
||||
'common.edit': '编辑',
|
||||
'common.add': '添加',
|
||||
'common.close': '关闭',
|
||||
'common.ok': '确定',
|
||||
'submit': '提交',
|
||||
'save': '保存',
|
||||
'submit.ok': '提交成功',
|
||||
@@ -19,6 +28,7 @@ const locale = {
|
||||
'menu.dashboard.community': '指标社区',
|
||||
'menu.dashboard.analysis': 'AI 分析',
|
||||
'menu.dashboard.tradingAssistant': '交易助手',
|
||||
'menu.dashboard.portfolio': '资产监测',
|
||||
'menu.settings': '系统设置',
|
||||
'menu.dashboard.aiTradingAssistant': 'AI交易助手',
|
||||
'menu.dashboard.signalRobot': '信号机器人',
|
||||
@@ -97,6 +107,36 @@ const locale = {
|
||||
'app.setting.themecolor.geekblue': '极客蓝',
|
||||
'app.setting.themecolor.purple': '酱紫',
|
||||
'app.setting.tooltip': '页面设置',
|
||||
|
||||
// 通知中心
|
||||
'notice.title': '通知中心',
|
||||
'notice.empty': '暂无通知',
|
||||
'notice.markAllRead': '全部已读',
|
||||
'notice.clear': '清空通知',
|
||||
'notice.close': '关闭',
|
||||
'notice.justNow': '刚刚',
|
||||
'notice.minutesAgo': '分钟前',
|
||||
'notice.hoursAgo': '小时前',
|
||||
'notice.daysAgo': '天前',
|
||||
'notice.detailInfo': '详细信息',
|
||||
'notice.aiDecision': 'AI决策',
|
||||
'notice.confidence': '置信度',
|
||||
'notice.reasoning': '分析理由',
|
||||
'notice.symbol': '标的代码',
|
||||
'notice.currentPrice': '当前价格',
|
||||
'notice.triggerPrice': '触发价格',
|
||||
'notice.action': '操作',
|
||||
'notice.quantity': '数量',
|
||||
'notice.viewPortfolio': '查看持仓',
|
||||
'notice.type.aiMonitor': 'AI监控',
|
||||
'notice.type.priceAlert': '价格提醒',
|
||||
'notice.type.signal': '交易信号',
|
||||
'notice.type.buy': '买入信号',
|
||||
'notice.type.sell': '卖出信号',
|
||||
'notice.type.hold': '持有建议',
|
||||
'notice.type.trade': '交易执行',
|
||||
'notice.type.notification': '系统通知',
|
||||
|
||||
'user.login.userName': '用户名',
|
||||
'user.login.password': '密码',
|
||||
'user.login.username.placeholder': '账户: admin',
|
||||
@@ -1858,7 +1898,140 @@ const locale = {
|
||||
'settings.desc.RATE_LIMIT': '每IP每分钟的API请求限制',
|
||||
'settings.desc.ENABLE_CACHE': '启用响应缓存以提高性能',
|
||||
'settings.desc.ENABLE_REQUEST_LOG': '记录所有API请求日志,用于调试',
|
||||
'settings.desc.ENABLE_AI_ANALYSIS': '启用AI驱动的市场分析功能'
|
||||
'settings.desc.ENABLE_AI_ANALYSIS': '启用AI驱动的市场分析功能',
|
||||
|
||||
// Portfolio - 资产监测
|
||||
'portfolio.summary.totalValue': '总市值',
|
||||
'portfolio.summary.totalCost': '总成本',
|
||||
'portfolio.summary.totalPnl': '总盈亏',
|
||||
'portfolio.summary.positionCount': '持仓数量',
|
||||
'portfolio.summary.profitLossRatio': '盈利/亏损',
|
||||
'portfolio.summary.today': '今日',
|
||||
'portfolio.summary.todayPnl': '今日盈亏',
|
||||
'portfolio.summary.bestPerformer': '最佳表现',
|
||||
'portfolio.summary.worstPerformer': '最差表现',
|
||||
'portfolio.summary.priceSync': '价格同步',
|
||||
'portfolio.summary.syncInterval': '刷新间隔',
|
||||
'portfolio.summary.justNow': '刚刚',
|
||||
'portfolio.summary.ago': '前',
|
||||
'portfolio.positions.title': '我的持仓',
|
||||
'portfolio.positions.add': '添加持仓',
|
||||
'portfolio.positions.addFirst': '添加第一笔持仓',
|
||||
'portfolio.positions.empty': '暂无持仓记录',
|
||||
'portfolio.positions.deleteConfirm': '确定删除这笔持仓吗?',
|
||||
'portfolio.positions.currentPrice': '现价',
|
||||
'portfolio.positions.entryPrice': '买入价',
|
||||
'portfolio.positions.quantity': '数量',
|
||||
'portfolio.positions.side': '方向',
|
||||
'portfolio.positions.long': '做多',
|
||||
'portfolio.positions.short': '做空',
|
||||
'portfolio.positions.marketValue': '市值',
|
||||
'portfolio.positions.pnl': '盈亏',
|
||||
'portfolio.positions.items': '个持仓',
|
||||
'portfolio.monitors.title': 'AI 监控',
|
||||
'portfolio.monitors.add': '添加监控',
|
||||
'portfolio.monitors.addFirst': '添加 AI 监控',
|
||||
'portfolio.monitors.empty': '暂无监控任务',
|
||||
'portfolio.monitors.deleteConfirm': '确定删除这个监控任务吗?',
|
||||
'portfolio.monitors.interval': '执行间隔',
|
||||
'portfolio.monitors.lastRun': '上次执行',
|
||||
'portfolio.monitors.nextRun': '下次执行',
|
||||
'portfolio.monitors.channels': '通知渠道',
|
||||
'portfolio.monitors.runNow': '立即执行',
|
||||
'portfolio.monitors.analysisResult': 'AI 分析结果',
|
||||
'portfolio.modal.addPosition': '添加持仓',
|
||||
'portfolio.modal.editPosition': '编辑持仓',
|
||||
'portfolio.modal.addMonitor': '添加监控',
|
||||
'portfolio.modal.editMonitor': '编辑监控',
|
||||
'portfolio.form.market': '市场',
|
||||
'portfolio.form.marketRequired': '请选择市场',
|
||||
'portfolio.form.selectMarket': '选择市场',
|
||||
'portfolio.form.symbol': '标的代码',
|
||||
'portfolio.form.symbolRequired': '请输入标的代码',
|
||||
'portfolio.form.searchSymbol': '搜索或输入标的代码',
|
||||
'portfolio.form.useAsSymbol': '使用',
|
||||
'portfolio.form.asSymbolCode': '作为标的代码',
|
||||
'portfolio.form.symbolHint': '可搜索标的库,或直接输入任意代码',
|
||||
'portfolio.form.side': '方向',
|
||||
'portfolio.form.quantity': '数量',
|
||||
'portfolio.form.quantityRequired': '请输入数量',
|
||||
'portfolio.form.enterQuantity': '输入持仓数量',
|
||||
'portfolio.form.entryPrice': '买入价',
|
||||
'portfolio.form.entryPriceRequired': '请输入买入价',
|
||||
'portfolio.form.enterEntryPrice': '输入买入价格',
|
||||
'portfolio.form.notes': '备注',
|
||||
'portfolio.form.enterNotes': '可选:添加备注',
|
||||
'portfolio.form.monitorName': '监控名称',
|
||||
'portfolio.form.monitorNameRequired': '请输入监控名称',
|
||||
'portfolio.form.enterMonitorName': '例如:每日组合分析',
|
||||
'portfolio.form.interval': '执行间隔',
|
||||
'portfolio.form.minutes': '分钟',
|
||||
'portfolio.form.hour': '小时',
|
||||
'portfolio.form.hours': '小时',
|
||||
'portfolio.form.notifyChannels': '通知渠道',
|
||||
'portfolio.form.browser': '浏览器通知',
|
||||
'portfolio.form.email': '邮件',
|
||||
'portfolio.form.telegramChatId': 'Telegram Chat ID',
|
||||
'portfolio.form.enterTelegramChatId': '输入 Telegram Chat ID',
|
||||
'portfolio.form.telegramRequired': '请输入 Telegram Chat ID',
|
||||
'portfolio.form.emailAddress': '邮箱地址',
|
||||
'portfolio.form.enterEmail': '输入邮箱地址',
|
||||
'portfolio.form.emailRequired': '请输入邮箱地址',
|
||||
'portfolio.form.emailInvalid': '请输入有效的邮箱地址',
|
||||
'portfolio.form.customPrompt': '自定义提示',
|
||||
'portfolio.form.customPromptPlaceholder': '可选:添加特别关注点,例如"重点关注科技股风险"',
|
||||
'portfolio.form.monitorScope': '监控范围',
|
||||
'portfolio.form.allPositions': '全部持仓',
|
||||
'portfolio.form.selectedPositions': '指定持仓',
|
||||
'portfolio.form.selectPositions': '选择持仓',
|
||||
'portfolio.form.selectAll': '全选',
|
||||
'portfolio.form.deselectAll': '全不选',
|
||||
'portfolio.form.selectedCount': '已选 {count}/{total}',
|
||||
'portfolio.form.pleaseSelectPositions': '请至少选择一个持仓进行监控',
|
||||
'portfolio.message.loadFailed': '加载数据失败',
|
||||
'portfolio.message.saveSuccess': '保存成功',
|
||||
'portfolio.message.saveFailed': '保存失败',
|
||||
'portfolio.message.deleteSuccess': '删除成功',
|
||||
'portfolio.message.deleteFailed': '删除失败',
|
||||
'portfolio.message.updateFailed': '更新失败',
|
||||
'portfolio.message.monitorEnabled': '监控已启用',
|
||||
'portfolio.message.monitorDisabled': '监控已暂停',
|
||||
'portfolio.message.monitorRunSuccess': '分析完成',
|
||||
'portfolio.message.monitorRunFailed': '分析失败',
|
||||
// Portfolio - 分组
|
||||
'portfolio.groups.all': '全部持仓',
|
||||
'portfolio.groups.ungrouped': '未分组',
|
||||
'portfolio.form.group': '分组',
|
||||
'portfolio.form.enterGroup': '输入或选择分组',
|
||||
// Portfolio - 预警
|
||||
'portfolio.alerts.title': '价格/盈亏预警',
|
||||
'portfolio.alerts.addAlert': '添加预警',
|
||||
'portfolio.alerts.editAlert': '编辑预警',
|
||||
'portfolio.alerts.alertType': '预警类型',
|
||||
'portfolio.alerts.priceAbove': '价格高于',
|
||||
'portfolio.alerts.priceBelow': '价格低于',
|
||||
'portfolio.alerts.pnlAbove': '盈利高于 (%)',
|
||||
'portfolio.alerts.pnlBelow': '亏损低于 (%)',
|
||||
'portfolio.alerts.threshold': '阈值',
|
||||
'portfolio.alerts.thresholdRequired': '请输入阈值',
|
||||
'portfolio.alerts.enterPrice': '输入价格',
|
||||
'portfolio.alerts.enterPercent': '输入百分比',
|
||||
'portfolio.alerts.currentPrice': '当前价格',
|
||||
'portfolio.alerts.currentPriceHint': '当前价格',
|
||||
'portfolio.alerts.repeatInterval': '重复提醒',
|
||||
'portfolio.alerts.noRepeat': '不重复 (触发一次)',
|
||||
'portfolio.alerts.every5min': '每 5 分钟',
|
||||
'portfolio.alerts.every15min': '每 15 分钟',
|
||||
'portfolio.alerts.every30min': '每 30 分钟',
|
||||
'portfolio.alerts.every1hour': '每 1 小时',
|
||||
'portfolio.alerts.every4hours': '每 4 小时',
|
||||
'portfolio.alerts.onceDaily': '每天一次',
|
||||
'portfolio.alerts.enabled': '启用预警',
|
||||
'portfolio.alerts.enabledDesc': '开启后将自动监测并触发通知',
|
||||
'portfolio.alerts.delete': '删除',
|
||||
'portfolio.alerts.deleteConfirm': '确定要删除此预警吗?',
|
||||
'portfolio.modal.addAlert': '添加预警',
|
||||
'portfolio.modal.editAlert': '编辑预警'
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { isIE } from '@/utils/util'
|
||||
|
||||
// 本地开发已有完整后端 API,禁用 mock 以避免请求被拦截
|
||||
// 如需启用 mock,将下面的 false 改为 true
|
||||
const ENABLE_MOCK = false
|
||||
|
||||
// 判断环境不是 prod 或者 preview 是 true 时,加载 mock 服务
|
||||
if (process.env.NODE_ENV !== 'production' || process.env.VUE_APP_PREVIEW === 'true') {
|
||||
if (ENABLE_MOCK && (process.env.NODE_ENV !== 'production' || process.env.VUE_APP_PREVIEW === 'true')) {
|
||||
if (isIE()) {
|
||||
}
|
||||
// 使用同步加载依赖
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user