2310 lines
168 KiB
HTML
2310 lines
168 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||
<title>股票投资</title>
|
||
|
||
<!-- PWA 全屏显示配置 -->
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="apple-mobile-web-app-title" content="股票投资">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="theme-color" content="#0a0a0a">
|
||
<link rel="manifest" href="/static/manifest.json">
|
||
|
||
<!-- 添加到主屏幕图标 -->
|
||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||
|
||
<style>[v-cloak]{display:none!important}</style>
|
||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📈</text></svg>">
|
||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||
<link rel="stylesheet" href="/static/css/base.css?v=20260719v2">
|
||
<link rel="stylesheet" href="/static/css/components.css?v=20260718v1">
|
||
<link rel="stylesheet" href="/static/css/auth.css?v=20260718v1">
|
||
<link rel="stylesheet" href="/static/css/pages.css?v=20260719v2">
|
||
<link rel="stylesheet" href="/static/css/scan.css?v=20260722v1">
|
||
<link rel="stylesheet" href="/static/css/responsive.css?v=20260719v1">
|
||
</head>
|
||
<body>
|
||
{% raw %}
|
||
<div id="app" v-cloak>
|
||
<!-- 全局 Toast 提示 -->
|
||
<transition name="toast-fade">
|
||
<div v-if="toastVisible" :class="['toast-container', toastType]" role="alert" aria-live="assertive">
|
||
<span class="toast-icon">
|
||
{{ toastType === 'success' ? '✓' : toastType === 'error' ? '✕' : toastType === 'warning' ? '⚠' : 'ℹ' }}
|
||
</span>
|
||
<span class="toast-message">{{ toastMessage }}</span>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- 确认对话框 -->
|
||
<transition name="modal-fade">
|
||
<div v-if="confirmVisible" class="confirm-overlay" @click.self="handleConfirmCancel" role="dialog" aria-modal="true" aria-label="确认操作">
|
||
<div class="confirm-dialog">
|
||
<div class="confirm-message">{{ confirmMessage }}</div>
|
||
<div class="confirm-buttons">
|
||
<button type="button" class="confirm-btn cancel" @click="handleConfirmCancel">取消</button>
|
||
<button type="button" class="confirm-btn ok" @click="handleConfirmOk">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- 智能执行结果模态框 -->
|
||
<transition name="modal-fade">
|
||
<div v-if="smartResultVisible" class="modal-overlay" @click.self="smartResultVisible = false">
|
||
<div class="smart-result-modal">
|
||
<div class="smart-result-header">
|
||
<div class="smart-result-title">
|
||
<span class="smart-result-icon" :class="smartResultData?.type === 'traded' ? 'success' : 'warning'">
|
||
{{ smartResultData?.type === 'traded' ? '✅' : '📋' }}
|
||
</span>
|
||
{{ smartResultData?.title || '执行结果' }}
|
||
</div>
|
||
<button type="button" class="modal-close-btn" @click="smartResultVisible = false">×</button>
|
||
</div>
|
||
<div class="smart-result-body" v-if="smartResultData">
|
||
<!-- 算法标识 -->
|
||
<div class="smart-result-algo">
|
||
<span class="algo-badge">{{ smartResultData.algo }}</span>
|
||
<span class="result-summary">{{ smartResultData.summary }}</span>
|
||
</div>
|
||
|
||
<!-- 交易结果列表 -->
|
||
<div v-if="smartResultData.results && smartResultData.results.length > 0" class="smart-result-trades">
|
||
<div class="result-section-title">📈 交易执行</div>
|
||
<div v-for="(r, i) in smartResultData.results" :key="i" class="result-trade-item" :class="r.type">
|
||
<span class="trade-type-tag" :class="r.type">{{ r.type === 'buy' ? '买入' : '卖出' }}</span>
|
||
<span class="trade-stock">{{ r.stock_code }} {{ r.stock_name }}</span>
|
||
<span class="trade-detail">¥{{ r.price?.toFixed(2) }} × {{ r.shares }}股</span>
|
||
<span v-if="r.fee > 0" class="trade-fee">手续费¥{{ r.fee?.toFixed(2) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 详细原因 -->
|
||
<div v-if="smartResultData.reasons && smartResultData.reasons.length > 0" class="smart-result-reasons">
|
||
<div class="result-section-title">{{ smartResultData.type === 'no_trade' ? '📊 分析详情' : '💡 附加信息' }}</div>
|
||
<div v-for="(reason, i) in smartResultData.reasons" :key="i" class="result-reason-item" :class="getReasonClass(reason)">
|
||
<span class="reason-icon">{{ getReasonIcon(reason) }}</span>
|
||
<span class="reason-text">{{ reason }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 手续费摘要 -->
|
||
<div v-if="smartResultData.fees > 0" class="smart-result-fees">
|
||
<span>💰 总手续费: ¥{{ smartResultData.fees.toFixed(2) }}</span>
|
||
</div>
|
||
|
||
<!-- 可用资金 -->
|
||
<div v-if="smartResultData.availableCash > 0" class="smart-result-cash">
|
||
<span>💵 当前可用资金: ¥{{ smartResultData.availableCash.toLocaleString('zh-CN', {minimumFractionDigits: 2}) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="smart-result-footer">
|
||
<button type="button" class="confirm-btn ok" @click="smartResultVisible = false">我知道了</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- 登录弹窗 -->
|
||
<transition name="modal-fade">
|
||
<div v-if="showLoginModal" class="modal-overlay" @click.self="showLoginModal = false" role="dialog" aria-modal="true" aria-label="登录">
|
||
<div class="login-modal">
|
||
<div class="login-tabs">
|
||
<button type="button" :class="['login-tab', { active: !isRegister }]" @click="isRegister = false; loginError = ''">登录</button>
|
||
<button type="button" :class="['login-tab', { active: isRegister }]" @click="isRegister = true; loginError = ''">注册</button>
|
||
<button type="button" class="modal-close-btn" @click="showLoginModal = false">×</button>
|
||
</div>
|
||
<div class="login-body">
|
||
<div class="login-field">
|
||
<label>邮箱</label>
|
||
<input v-model="loginForm.email" type="email" placeholder="请输入邮箱地址" @keyup.enter="handleLogin">
|
||
</div>
|
||
<div class="login-field">
|
||
<label>密码</label>
|
||
<input v-model="loginForm.password" type="password" :placeholder="isRegister ? '设置密码(至少6位)' : '请输入密码'" @keyup.enter="handleLogin">
|
||
</div>
|
||
<div v-if="loginError" class="login-error">{{ loginError }}</div>
|
||
<button type="button" class="login-submit" @click="handleLogin" :disabled="loginLoading">
|
||
{{ loginLoading ? '处理中...' : (isRegister ? '注册' : '登录') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
|
||
<div class="container">
|
||
<!-- 未登录状态:只显示登录界面 -->
|
||
<div v-if="!currentUser && !checkingAuth" class="login-page">
|
||
<div class="login-page-header">
|
||
<svg class="login-icon" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M8 36L16 26L22 32L32 18L40 28" stroke="url(#grad1)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||
<path d="M34 18H40V24" stroke="url(#grad1)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||
<circle cx="16" cy="26" r="3" fill="#00ff88"/>
|
||
<circle cx="22" cy="32" r="3" fill="#00ff88"/>
|
||
<circle cx="32" cy="18" r="3" fill="#00ff88"/>
|
||
<defs>
|
||
<linearGradient id="grad1" x1="8" y1="18" x2="40" y2="36" gradientUnits="userSpaceOnUse">
|
||
<stop offset="0%" stop-color="#00ff88"/>
|
||
<stop offset="100%" stop-color="#00ccff"/>
|
||
</linearGradient>
|
||
</defs>
|
||
</svg>
|
||
<h1>股票投资</h1>
|
||
<p>请登录以访问您的投资数据</p>
|
||
</div>
|
||
<div class="login-modal login-modal-inline">
|
||
<div class="login-tabs">
|
||
<button type="button" :class="['login-tab', { active: !isRegister }]" @click="isRegister = false; loginError = ''">登录</button>
|
||
<button type="button" :class="['login-tab', { active: isRegister }]" @click="isRegister = true; loginError = ''">注册</button>
|
||
</div>
|
||
<div class="login-body">
|
||
<div class="login-field">
|
||
<label>邮箱</label>
|
||
<input v-model="loginForm.email" type="email" placeholder="请输入邮箱地址" @keyup.enter="handleLogin">
|
||
</div>
|
||
<div class="login-field">
|
||
<label>密码</label>
|
||
<input v-model="loginForm.password" type="password" :placeholder="isRegister ? '设置密码(至少6位)' : '请输入密码'" @keyup.enter="handleLogin">
|
||
</div>
|
||
<div v-if="loginError" class="login-error">{{ loginError }}</div>
|
||
<button type="button" class="login-submit" @click="handleLogin" :disabled="loginLoading">
|
||
{{ loginLoading ? '处理中...' : (isRegister ? '注册' : '登录') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 检查登录状态中 -->
|
||
<div v-else-if="checkingAuth" class="loading-auth">
|
||
<p>正在加载...</p>
|
||
</div>
|
||
|
||
<!-- 已登录状态:显示主内容 -->
|
||
<template v-else>
|
||
<div class="title-bar">
|
||
<div class="title-left">
|
||
<h1>股票投资 <span class="main-title-date">{{ todayDate }}<span v-if="dataLatestDate && dataLatestDate !== todayDate">({{ dataLatestDate }})</span></span> <span :class="['market-status', marketStatus.isOpen ? 'open' : 'closed']">{{ marketStatus.text }}</span></h1>
|
||
<div class="current-model">策略: 交易信号实战体系</div>
|
||
</div>
|
||
<div class="user-info">
|
||
<span class="username" @click="showPasswordModal = true" class="cursor-pointer">{{ currentUser?.username }}</span>
|
||
<button type="button" class="logout-btn" @click="handleLogout">退出</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 顶部导航 -->
|
||
<div class="nav-tabs nav-tabs-compact" role="tablist">
|
||
<button type="button" :class="['nav-tab', { active: activeTab === 'alerts' }]" role="tab" :aria-selected="activeTab === 'alerts'" @click="activeTab = 'alerts'">提醒</button>
|
||
<button type="button" :class="['nav-tab', { active: activeTab === 'trades' }]" role="tab" :aria-selected="activeTab === 'trades'" @click="activeTab = 'trades'; loadTrades()">交易</button>
|
||
<button type="button" :class="['nav-tab', { active: activeTab === 'analysis' }]" role="tab" :aria-selected="activeTab === 'analysis'" @click="activeTab = 'analysis'; if (analysisSubTab === 'scan') fetchFullScanResults(1)">分析</button>
|
||
<button type="button" :class="['nav-tab', { active: activeTab === 'model' }]" role="tab" :aria-selected="activeTab === 'model'" @click="activeTab = 'model'">模型</button>
|
||
</div>
|
||
|
||
<!-- 提醒页面 -->
|
||
<div v-show="activeTab === 'alerts'">
|
||
<!-- 信号汇总(始终显示在顶部) -->
|
||
<div class="alerts-summary top-summary">
|
||
<div class="summary-row">
|
||
<span class="summary-label">持有</span>
|
||
<span class="summary-value">{{ holdingStocks.length }}</span>
|
||
</div>
|
||
<div class="summary-row">
|
||
<span class="summary-label">关注</span>
|
||
<span class="summary-value">{{ watchingStocks.length }}</span>
|
||
</div>
|
||
<div class="summary-row buy">
|
||
<span class="summary-label">建议买入</span>
|
||
<span class="summary-value">{{ buyAlerts.length + holdingAlerts.filter(a => a.signalType === 'buy').length }}</span>
|
||
</div>
|
||
<div class="summary-row sell">
|
||
<span class="summary-label">建议卖出</span>
|
||
<span class="summary-value">{{ sellAlerts.length + holdingAlerts.filter(a => a.signalType === 'sell').length }}</span>
|
||
</div>
|
||
<button type="button" class="refresh-btn-inline" @click="refreshAlerts(true)" :disabled="alertsLoading">
|
||
{{ alertsLoading ? (alertsProgress.total > 0 ? `${alertsProgress.current}/${alertsProgress.total}` : '加载中...') : '刷新' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 子导航 -->
|
||
<div class="sub-tabs">
|
||
<button type="button" :class="['sub-tab', { active: alertSubTab === 'holding' }]" @click="alertSubTab = 'holding'">
|
||
持有 <span v-if="holdingStocks.length" class="sub-badge">{{ holdingStocks.length }}</span>
|
||
</button>
|
||
<button type="button" :class="['sub-tab', { active: alertSubTab === 'watching' }]" @click="alertSubTab = 'watching'">
|
||
关注 <span v-if="watchingStocks.length" class="sub-badge">{{ watchingStocks.length }}</span>
|
||
</button>
|
||
<button type="button" class="add-watch-btn" @click="showAddWatch = !showAddWatch" title="添加关注">+</button>
|
||
</div>
|
||
|
||
<!-- 添加关注输入框 -->
|
||
<div v-if="showAddWatch" class="add-watch-input">
|
||
<input v-model="newWatchCode" type="text" placeholder="输入股票代码" maxlength="6" @keyup.enter="addToWatch">
|
||
<button type="button" @click="addToWatch" :disabled="!newWatchCode || newWatchCode.length < 6">添加</button>
|
||
</div>
|
||
|
||
<div v-if="alertsLoading" class="loading">
|
||
<p>正在分析股票...</p>
|
||
</div>
|
||
|
||
<!-- 持有列表 -->
|
||
<div v-else-if="alertSubTab === 'holding'">
|
||
<div v-if="holdingStocks.length === 0" class="no-signal-tip">
|
||
暂无持有股票<br>在交易中记录买入后自动添加到持有列表
|
||
</div>
|
||
|
||
<div v-else>
|
||
<!-- 卖出信号(持有的股票出现卖出信号需要优先关注) -->
|
||
<div v-if="holdingAlerts.filter(a => a.signalType === 'sell').length > 0" class="alert-section">
|
||
<h4 class="section-title sell">卖出信号</h4>
|
||
<div v-for="alert in holdingAlerts.filter(a => a.signalType === 'sell')" :key="alert.code" class="alert-card sell" role="button" tabindex="0" @click="loadFundamental(alert.code, alert.name)" @keyup.enter="loadFundamental(alert.code, alert.name)">
|
||
<div class="alert-header">
|
||
<span class="alert-code">{{ alert.code }}</span>
|
||
<span class="alert-name">{{ alert.name }}</span>
|
||
</div>
|
||
<div class="alert-prices-dual">
|
||
<div class="price-col">
|
||
<span class="price-label">实时</span>
|
||
<span class="alert-price" v-if="alert.price">¥{{ alert.price.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.changePct >= 0 ? 'up' : 'down'" v-if="alert.changePct != null">{{ alert.changePct >= 0 ? '+' : '' }}{{ alert.changePct.toFixed(2) }}%</span>
|
||
</div>
|
||
<div class="price-col scan">
|
||
<span class="price-label">扫描</span>
|
||
<span class="alert-price" v-if="alert.scanPrice">¥{{ alert.scanPrice.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.scanChangePct >= 0 ? 'up' : 'down'" v-if="alert.scanChangePct != null">{{ alert.scanChangePct >= 0 ? '+' : '' }}{{ alert.scanChangePct.toFixed(2) }}%</span>
|
||
</div>
|
||
</div>
|
||
<div class="alert-signals" v-if="alert.triggeredSignals && alert.triggeredSignals.length">
|
||
<span v-for="sig in alert.triggeredSignals" :key="sig" class="alert-sig-tag">{{ sig }}</span>
|
||
</div>
|
||
<div class="alert-bottom">
|
||
<div class="alert-desc">{{ alert.reason }}</div>
|
||
<span class="recommend-rate sell">{{ alert.recommendRate }}%</span>
|
||
<button type="button" class="alert-action-btn sell" @click.stop="quickTradeFromAlert(alert, 'sell')">卖出</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 持有正向信号(加仓/持有) -->
|
||
<div v-if="holdingAlerts.filter(a => a.signalType === 'buy').length > 0" class="alert-section">
|
||
<h4 class="section-title buy">正向信号</h4>
|
||
<div v-for="alert in holdingAlerts.filter(a => a.signalType === 'buy')" :key="alert.code"
|
||
class="alert-card" :class="alert.recommendText === '加仓' ? 'buy' : 'hold'" role="button" tabindex="0" @click="loadFundamental(alert.code, alert.name)" @keyup.enter="loadFundamental(alert.code, alert.name)">
|
||
<div class="alert-header">
|
||
<span class="alert-code">{{ alert.code }}</span>
|
||
<span class="alert-name">{{ alert.name }}</span>
|
||
<span class="alert-rec-tag" :class="alert.recommendText === '加仓' ? 'tag-add' : 'tag-hold'">{{ alert.recommendText || '持有' }}</span>
|
||
</div>
|
||
<div class="alert-prices-dual">
|
||
<div class="price-col">
|
||
<span class="price-label">实时</span>
|
||
<span class="alert-price" v-if="alert.price">¥{{ alert.price.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.changePct >= 0 ? 'up' : 'down'" v-if="alert.changePct != null">{{ alert.changePct >= 0 ? '+' : '' }}{{ alert.changePct.toFixed(2) }}%</span>
|
||
</div>
|
||
<div class="price-col scan">
|
||
<span class="price-label">扫描</span>
|
||
<span class="alert-price" v-if="alert.scanPrice">¥{{ alert.scanPrice.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.scanChangePct >= 0 ? 'up' : 'down'" v-if="alert.scanChangePct != null">{{ alert.scanChangePct >= 0 ? '+' : '' }}{{ alert.scanChangePct.toFixed(2) }}%</span>
|
||
</div>
|
||
</div>
|
||
<div class="alert-signals" v-if="alert.triggeredSignals && alert.triggeredSignals.length">
|
||
<span v-for="sig in alert.triggeredSignals" :key="sig" class="alert-sig-tag">{{ sig }}</span>
|
||
</div>
|
||
<div class="alert-bottom">
|
||
<div class="alert-desc">{{ alert.reason }}</div>
|
||
<span class="recommend-rate" :class="alert.recommendText === '加仓' ? 'buy' : 'hold'">{{ alert.recommendRate }}%</span>
|
||
<button type="button"v-if="alert.recommendText === '加仓'" class="alert-action-btn buy" @click.stop="quickTradeFromAlert(alert, 'buy')">加仓</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 观望(持有的股票无明确信号) -->
|
||
<div v-if="holdingAlerts.filter(a => a.signalType === 'watch').length > 0" class="alert-section">
|
||
<h4 class="section-title watch">持有观望</h4>
|
||
<div v-for="alert in holdingAlerts.filter(a => a.signalType === 'watch')" :key="alert.code" class="alert-card watch" role="button" tabindex="0" @click="loadFundamental(alert.code, alert.name)" @keyup.enter="loadFundamental(alert.code, alert.name)">
|
||
<div class="alert-header">
|
||
<span class="alert-code">{{ alert.code }}</span>
|
||
<span class="alert-name">{{ alert.name }}</span>
|
||
</div>
|
||
<div class="alert-prices-dual">
|
||
<div class="price-col">
|
||
<span class="price-label">实时</span>
|
||
<span class="alert-price" v-if="alert.price">¥{{ alert.price.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.changePct >= 0 ? 'up' : 'down'" v-if="alert.changePct != null">{{ alert.changePct >= 0 ? '+' : '' }}{{ alert.changePct.toFixed(2) }}%</span>
|
||
</div>
|
||
<div class="price-col scan">
|
||
<span class="price-label">扫描</span>
|
||
<span class="alert-price" v-if="alert.scanPrice">¥{{ alert.scanPrice.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.scanChangePct >= 0 ? 'up' : 'down'" v-if="alert.scanChangePct != null">{{ alert.scanChangePct >= 0 ? '+' : '' }}{{ alert.scanChangePct.toFixed(2) }}%</span>
|
||
</div>
|
||
</div>
|
||
<div class="alert-signals" v-if="alert.triggeredSignals && alert.triggeredSignals.length">
|
||
<span v-for="sig in alert.triggeredSignals" :key="sig" class="alert-sig-tag">{{ sig }}</span>
|
||
</div>
|
||
<div class="alert-desc" v-if="alert.reason">{{ alert.reason }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 关注列表 -->
|
||
<div v-else-if="alertSubTab === 'watching'">
|
||
<div v-if="watchingStocks.length === 0" class="no-signal-tip">
|
||
暂无关注股票<br>请先在"分析"页面查询股票后自动添加到关注列表
|
||
</div>
|
||
|
||
<div v-else>
|
||
<!-- 买入提醒列表 -->
|
||
<div v-if="buyAlerts.length > 0" class="alert-section">
|
||
<h4 class="section-title buy">买入信号</h4>
|
||
<div v-for="alert in buyAlerts" :key="alert.code" class="alert-card buy" role="button" tabindex="0" @click="loadFundamental(alert.code, alert.name)" @keyup.enter="loadFundamental(alert.code, alert.name)">
|
||
<div class="alert-header">
|
||
<span class="alert-code">{{ alert.code }}</span>
|
||
<span class="alert-name">{{ alert.name }}</span>
|
||
<span class="alert-remove" @click.stop="removeFromWatch(alert.code)">×</span>
|
||
</div>
|
||
<div class="alert-prices-dual">
|
||
<div class="price-col">
|
||
<span class="price-label">实时</span>
|
||
<span class="alert-price" v-if="alert.price">¥{{ alert.price.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.changePct >= 0 ? 'up' : 'down'" v-if="alert.changePct != null">{{ alert.changePct >= 0 ? '+' : '' }}{{ alert.changePct.toFixed(2) }}%</span>
|
||
</div>
|
||
<div class="price-col scan">
|
||
<span class="price-label">扫描</span>
|
||
<span class="alert-price" v-if="alert.scanPrice">¥{{ alert.scanPrice.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.scanChangePct >= 0 ? 'up' : 'down'" v-if="alert.scanChangePct != null">{{ alert.scanChangePct >= 0 ? '+' : '' }}{{ alert.scanChangePct.toFixed(2) }}%</span>
|
||
</div>
|
||
</div>
|
||
<div class="alert-signals" v-if="alert.triggeredSignals && alert.triggeredSignals.length">
|
||
<span v-for="sig in alert.triggeredSignals" :key="sig" class="alert-sig-tag">{{ sig }}</span>
|
||
</div>
|
||
<div class="alert-bottom">
|
||
<div class="alert-desc">{{ alert.reason }}</div>
|
||
<div class="alert-desc holding-note" v-if="alert.holdingNote">{{ alert.holdingNote }}</div>
|
||
<span class="recommend-rate buy">{{ alert.recommendRate }}%</span>
|
||
<button type="button" class="alert-action-btn buy" @click.stop="quickTradeFromAlert(alert, 'buy')">买入</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 卖出提醒列表 -->
|
||
<div v-if="sellAlerts.length > 0" class="alert-section">
|
||
<h4 class="section-title sell">卖出信号</h4>
|
||
<div v-for="alert in sellAlerts" :key="alert.code" class="alert-card sell" role="button" tabindex="0" @click="loadFundamental(alert.code, alert.name)" @keyup.enter="loadFundamental(alert.code, alert.name)">
|
||
<div class="alert-header">
|
||
<span class="alert-code">{{ alert.code }}</span>
|
||
<span class="alert-name">{{ alert.name }}</span>
|
||
<span class="alert-remove" @click.stop="removeFromWatch(alert.code)">×</span>
|
||
</div>
|
||
<div class="alert-prices-dual">
|
||
<div class="price-col">
|
||
<span class="price-label">实时</span>
|
||
<span class="alert-price" v-if="alert.price">¥{{ alert.price.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.changePct >= 0 ? 'up' : 'down'" v-if="alert.changePct != null">{{ alert.changePct >= 0 ? '+' : '' }}{{ alert.changePct.toFixed(2) }}%</span>
|
||
</div>
|
||
<div class="price-col scan">
|
||
<span class="price-label">扫描</span>
|
||
<span class="alert-price" v-if="alert.scanPrice">¥{{ alert.scanPrice.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.scanChangePct >= 0 ? 'up' : 'down'" v-if="alert.scanChangePct != null">{{ alert.scanChangePct >= 0 ? '+' : '' }}{{ alert.scanChangePct.toFixed(2) }}%</span>
|
||
</div>
|
||
</div>
|
||
<div class="alert-signals" v-if="alert.triggeredSignals && alert.triggeredSignals.length">
|
||
<span v-for="sig in alert.triggeredSignals" :key="sig" class="alert-sig-tag">{{ sig }}</span>
|
||
</div>
|
||
<div class="alert-bottom">
|
||
<div class="alert-desc">{{ alert.reason }}</div>
|
||
<span class="recommend-rate sell">{{ alert.recommendRate }}%</span>
|
||
<button type="button" class="alert-action-btn sell" @click.stop="quickTradeFromAlert(alert, 'sell')">卖出</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 观望列表 -->
|
||
<div v-if="watchAlerts.length > 0" class="alert-section">
|
||
<h4 class="section-title watch">观望</h4>
|
||
<div v-for="alert in watchAlerts" :key="alert.code" class="alert-card watch" role="button" tabindex="0" @click="loadFundamental(alert.code, alert.name)" @keyup.enter="loadFundamental(alert.code, alert.name)">
|
||
<div class="alert-header">
|
||
<span class="alert-code">{{ alert.code }}</span>
|
||
<span class="alert-name">{{ alert.name }}</span>
|
||
<span class="alert-remove" @click.stop="removeFromWatch(alert.code)">×</span>
|
||
</div>
|
||
<div class="alert-prices-dual">
|
||
<div class="price-col">
|
||
<span class="price-label">实时</span>
|
||
<span class="alert-price" v-if="alert.price">¥{{ alert.price.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.changePct >= 0 ? 'up' : 'down'" v-if="alert.changePct != null">{{ alert.changePct >= 0 ? '+' : '' }}{{ alert.changePct.toFixed(2) }}%</span>
|
||
</div>
|
||
<div class="price-col scan">
|
||
<span class="price-label">扫描</span>
|
||
<span class="alert-price" v-if="alert.scanPrice">¥{{ alert.scanPrice.toFixed(2) }}</span>
|
||
<span class="alert-change" :class="alert.scanChangePct >= 0 ? 'up' : 'down'" v-if="alert.scanChangePct != null">{{ alert.scanChangePct >= 0 ? '+' : '' }}{{ alert.scanChangePct.toFixed(2) }}%</span>
|
||
</div>
|
||
</div>
|
||
<div class="alert-signals" v-if="alert.triggeredSignals && alert.triggeredSignals.length">
|
||
<span v-for="sig in alert.triggeredSignals" :key="sig" class="alert-sig-tag">{{ sig }}</span>
|
||
</div>
|
||
<div class="alert-desc" v-if="alert.reason && alert.reason !== '无核心信号触发'">{{ alert.reason }}</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- v-else 关注内容结束 -->
|
||
</div><!-- 关注页面结束 -->
|
||
</div><!-- 提醒页面结束 -->
|
||
|
||
<!-- 基本面信息弹窗 -->
|
||
<div v-if="showFundamentalModal" class="modal-overlay" @click.self="showFundamentalModal = false">
|
||
<div class="modal-content fundamental-modal">
|
||
<div class="fundamental-header">
|
||
<h3>{{ fundamentalData?.stock_name || '股票' }} 基本面</h3>
|
||
<span class="fundamental-code">{{ fundamentalData?.stock_code }}</span>
|
||
<button type="button" class="modal-close" @click="showFundamentalModal = false">×</button>
|
||
</div>
|
||
<div v-if="fundamentalData" class="fundamental-actions">
|
||
<button type="button" v-if="!searchHistory.some(h => h.code === fundamentalData.stock_code)"
|
||
class="watch-btn add"
|
||
@click="addToWatchFromFundamental">
|
||
★ 添加关注
|
||
</button>
|
||
<button type="button" v-else
|
||
class="watch-btn remove"
|
||
@click="removeFromWatchFromFundamental">
|
||
☆ 取消关注
|
||
</button>
|
||
<button type="button" class="ai-analyze-btn"
|
||
@click="aiAnalyze(fundamentalData.stock_code, fundamentalData.stock_name)"
|
||
:disabled="aiAnalyzing">
|
||
{{ aiAnalyzing ? '分析中...' : 'AI分析' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- AI分析结果(流式) -->
|
||
<div v-if="aiAnalysisResult || aiAnalyzing" class="ai-analysis-section">
|
||
<h4 class="section-title">AI分析</h4>
|
||
|
||
<!-- 思考过程(可折叠) -->
|
||
<div v-if="aiReasoningResult" class="ai-reasoning-section">
|
||
<div class="ai-reasoning-header" @click="aiReasoningExpanded = !aiReasoningExpanded">
|
||
<span>💭 思考过程</span>
|
||
<span class="expand-icon">{{ aiReasoningExpanded ? '▼' : '▶' }}</span>
|
||
</div>
|
||
<div v-show="aiReasoningExpanded" class="ai-reasoning-content">
|
||
{{ aiReasoningResult }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ai-analysis-content" v-html="formatAiAnalysis(aiAnalysisResult)"></div>
|
||
|
||
<!-- 加载指示器 -->
|
||
<div v-if="aiAnalyzing" class="ai-loading-indicator">
|
||
<span class="loading-dot"></span> AI正在分析中...
|
||
</div>
|
||
|
||
<button type="button" v-if="!aiAnalyzing" class="ai-close-btn" @click="aiAnalysisResult = ''; aiReasoningResult = ''">关闭</button>
|
||
</div>
|
||
|
||
<div v-if="fundamentalLoading" class="fundamental-loading">
|
||
加载中...
|
||
</div>
|
||
|
||
<div v-else-if="fundamentalData" class="fundamental-body">
|
||
<!-- 交易信号状态 -->
|
||
<div class="fundamental-section signal-section" v-if="fundamentalData.signal_data">
|
||
<h4>🎯 交易信号 <span class="signal-count">触发 {{ fundamentalData.signal_data.triggered_count }}/7</span></h4>
|
||
<div class="signal-grid">
|
||
<div v-for="sig in fundamentalData.signal_data.signal_status" :key="sig.name"
|
||
class="signal-chip" :class="sig.triggered ? 'triggered' : 'inactive'">
|
||
<span class="signal-dot" :class="sig.triggered ? 'on' : 'off'"></span>
|
||
{{ sig.name }}
|
||
</div>
|
||
</div>
|
||
<div class="signal-summary" v-if="fundamentalData.signal_data.triggered_count >= 3">
|
||
<span class="signal-strong">⚡ 多信号共振({{ fundamentalData.signal_data.triggered_count }}个信号触发),关注度较高</span>
|
||
</div>
|
||
<div class="signal-summary" v-else-if="fundamentalData.signal_data.triggered_count >= 1">
|
||
<span class="signal-normal">📊 有信号触发,可结合基本面综合判断</span>
|
||
</div>
|
||
<div class="signal-summary" v-else>
|
||
<span class="signal-none">⏸ 暂无信号触发</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 估值指标 - 只有有数据时才显示整个区块 -->
|
||
<div class="fundamental-section" v-if="fundamentalData.pe_ttm || fundamentalData.pb || fundamentalData.roe">
|
||
<h4>📊 估值指标</h4>
|
||
<div class="fundamental-grid">
|
||
<div class="fundamental-item" v-if="fundamentalData.pe_ttm">
|
||
<span class="label">市盈率(动)</span>
|
||
<span class="value">{{ typeof fundamentalData.pe_ttm === 'number' ? fundamentalData.pe_ttm.toFixed(2) : fundamentalData.pe_ttm }}</span>
|
||
</div>
|
||
<div class="fundamental-item" v-if="fundamentalData.pb">
|
||
<span class="label">市净率</span>
|
||
<span class="value">{{ typeof fundamentalData.pb === 'number' ? fundamentalData.pb.toFixed(2) : fundamentalData.pb }}</span>
|
||
</div>
|
||
<div class="fundamental-item" v-if="fundamentalData.roe">
|
||
<span class="label">ROE</span>
|
||
<span class="value">{{ typeof fundamentalData.roe === 'number' ? fundamentalData.roe.toFixed(2) + '%' : fundamentalData.roe }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 市值规模 - 只有有数据时才显示整个区块 -->
|
||
<div class="fundamental-section" v-if="fundamentalData.total_market_cap || fundamentalData.circulating_market_cap || fundamentalData.industry">
|
||
<h4>💰 市值规模</h4>
|
||
<div class="fundamental-grid">
|
||
<div class="fundamental-item" v-if="fundamentalData.total_market_cap">
|
||
<span class="label">总市值</span>
|
||
<span class="value">{{ formatMarketCap(fundamentalData.total_market_cap) }}</span>
|
||
</div>
|
||
<div class="fundamental-item" v-if="fundamentalData.circulating_market_cap">
|
||
<span class="label">流通市值</span>
|
||
<span class="value">{{ formatMarketCap(fundamentalData.circulating_market_cap) }}</span>
|
||
</div>
|
||
<div class="fundamental-item" v-if="fundamentalData.industry">
|
||
<span class="label">行业</span>
|
||
<span class="value">{{ fundamentalData.industry }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 成长性 - 只有有数据时才显示整个区块 -->
|
||
<div class="fundamental-section" v-if="fundamentalData.revenue_yoy || fundamentalData.profit_yoy || fundamentalData.gross_margin">
|
||
<h4>📈 成长性</h4>
|
||
<div class="fundamental-grid">
|
||
<div class="fundamental-item" v-if="fundamentalData.revenue_yoy">
|
||
<span class="label">营收同比</span>
|
||
<span class="value" :class="fundamentalData.revenue_yoy >= 0 ? 'up' : 'down'">
|
||
{{ typeof fundamentalData.revenue_yoy === 'number' ? (fundamentalData.revenue_yoy >= 0 ? '+' : '') + fundamentalData.revenue_yoy.toFixed(2) + '%' : fundamentalData.revenue_yoy }}
|
||
</span>
|
||
</div>
|
||
<div class="fundamental-item" v-if="fundamentalData.profit_yoy">
|
||
<span class="label">净利润同比</span>
|
||
<span class="value" :class="fundamentalData.profit_yoy >= 0 ? 'up' : 'down'">
|
||
{{ typeof fundamentalData.profit_yoy === 'number' ? (fundamentalData.profit_yoy >= 0 ? '+' : '') + fundamentalData.profit_yoy.toFixed(2) + '%' : fundamentalData.profit_yoy }}
|
||
</span>
|
||
</div>
|
||
<div class="fundamental-item" v-if="fundamentalData.gross_margin">
|
||
<span class="label">毛利率</span>
|
||
<span class="value">{{ typeof fundamentalData.gross_margin === 'number' ? fundamentalData.gross_margin.toFixed(2) + '%' : fundamentalData.gross_margin }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 每股指标 - 只有有数据时才显示整个区块 -->
|
||
<div class="fundamental-section" v-if="fundamentalData.eps || fundamentalData.bps || fundamentalData.net_margin">
|
||
<h4>📋 每股指标</h4>
|
||
<div class="fundamental-grid">
|
||
<div class="fundamental-item" v-if="fundamentalData.eps">
|
||
<span class="label">每股收益</span>
|
||
<span class="value">{{ typeof fundamentalData.eps === 'number' ? fundamentalData.eps.toFixed(4) + '元' : fundamentalData.eps }}</span>
|
||
</div>
|
||
<div class="fundamental-item" v-if="fundamentalData.bps">
|
||
<span class="label">每股净资产</span>
|
||
<span class="value">{{ typeof fundamentalData.bps === 'number' ? fundamentalData.bps.toFixed(2) + '元' : fundamentalData.bps }}</span>
|
||
</div>
|
||
<div class="fundamental-item" v-if="fundamentalData.net_margin">
|
||
<span class="label">净利率</span>
|
||
<span class="value">{{ typeof fundamentalData.net_margin === 'number' ? fundamentalData.net_margin.toFixed(2) + '%' : fundamentalData.net_margin }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fundamental-section" v-if="fundamentalData.fund_flow_3days && fundamentalData.fund_flow_3days.length > 0">
|
||
<h4>💰 近3日资金流向</h4>
|
||
<div class="fundflow-table">
|
||
<div class="fundflow-header">
|
||
<span class="ff-col">日期</span>
|
||
<span class="ff-col">超大单</span>
|
||
<span class="ff-col">主力</span>
|
||
<span class="ff-col">涨跌</span>
|
||
</div>
|
||
<div v-for="flow in fundamentalData.fund_flow_3days" :key="flow.date" class="fundflow-row">
|
||
<span class="ff-col date">{{ flow.date }}</span>
|
||
<span class="ff-col" :class="flow.super_pct >= 0 ? 'up' : 'down'">
|
||
{{ flow.super_pct >= 0 ? '+' : '' }}{{ flow.super_pct.toFixed(1) }}%
|
||
</span>
|
||
<span class="ff-col" :class="flow.main_pct >= 0 ? 'up' : 'down'">
|
||
{{ flow.main_pct >= 0 ? '+' : '' }}{{ flow.main_pct.toFixed(1) }}%
|
||
</span>
|
||
<span class="ff-col" :class="flow.change_pct >= 0 ? 'up' : 'down'">
|
||
{{ flow.change_pct >= 0 ? '+' : '' }}{{ flow.change_pct.toFixed(2) }}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- K线图 -->
|
||
<div class="fundamental-section">
|
||
<div class="kline-header">
|
||
<h4>📈 股价走势</h4>
|
||
<div class="kline-period-tabs">
|
||
<button type="button" :class="['kline-tab', { active: klinePeriod === 'weekly' }]" @click="changeKlinePeriod('weekly')">周</button>
|
||
<button type="button" :class="['kline-tab', { active: klinePeriod === 'monthly' }]" @click="changeKlinePeriod('monthly')">月</button>
|
||
<button type="button" :class="['kline-tab', { active: klinePeriod === 'quarterly' }]" @click="changeKlinePeriod('quarterly')">季</button>
|
||
<button type="button" :class="['kline-tab', { active: klinePeriod === 'yearly' }]" @click="changeKlinePeriod('yearly')">年</button>
|
||
</div>
|
||
</div>
|
||
<div class="kline-chart-container" :class="{ loading: klineLoading }">
|
||
<canvas ref="klineCanvas"></canvas>
|
||
<div v-if="klineLoading" class="kline-loading">加载中...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="fundamental-error">
|
||
无法获取基本面数据
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分析页面 -->
|
||
<div v-show="activeTab === 'analysis'">
|
||
|
||
<!-- 分析页子导航 -->
|
||
<div class="sub-tabs">
|
||
<button type="button" :class="['sub-tab', { active: analysisSubTab === 'scan' }]" @click="analysisSubTab = 'scan'; fetchFullScanResults(1)">
|
||
全景扫描
|
||
</button>
|
||
<button type="button" :class="['sub-tab', { active: analysisSubTab === 'bull' }]" @click="analysisSubTab = 'bull'">
|
||
个股深析
|
||
</button>
|
||
<button type="button" :class="['sub-tab', { active: analysisSubTab === 'buyanalysis' }]" @click="analysisSubTab = 'buyanalysis'; fetchBuyAnalysis()">
|
||
买入分析
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 市场扫描子页面 -->
|
||
<div v-show="analysisSubTab === 'scan'">
|
||
|
||
<!-- 单股分析结果(保留原有功能) -->
|
||
<div v-if="result && !loading">
|
||
|
||
<!-- 交易建议卡片 -->
|
||
<div :class="['recommendation-card', tradingSignal.type]">
|
||
<div class="signal-icon">{{ tradingSignal.icon }}</div>
|
||
<div class="signal-text">{{ tradingSignal.title }}</div>
|
||
<div class="signal-desc">{{ tradingSignal.description }}</div>
|
||
|
||
<div class="signal-details">
|
||
<div class="signal-detail-item">
|
||
<h4>当前价格</h4>
|
||
<div class="value">{{ latestData.price?.toFixed(4) || '-' }} 元</div>
|
||
</div>
|
||
<div class="signal-detail-item">
|
||
<h4>价格位置</h4>
|
||
<div class="value">{{ latestData.pricePosition?.toFixed(1) || '-' }}%</div>
|
||
</div>
|
||
<div class="signal-detail-item">
|
||
<h4>超大单净流入</h4>
|
||
<div class="value">{{ latestData.superRatio?.toFixed(2) || '-' }}%</div>
|
||
</div>
|
||
<div class="signal-detail-item">
|
||
<h4>主力净流入</h4>
|
||
<div class="value">{{ latestData.mainRatio?.toFixed(2) || '-' }}%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图表分析区域(可折叠) -->
|
||
<div class="collapsible-section">
|
||
<div class="collapsible-header" @click="toggleSection('charts')">
|
||
<h3>图表分析</h3>
|
||
<span :class="['toggle-icon', { expanded: expandedSections.charts }]">▼</span>
|
||
</div>
|
||
<div :class="['collapsible-content', { expanded: expandedSections.charts }]">
|
||
<div class="analysis-grid" style="margin-top: 15px;">
|
||
<div class="card">
|
||
<div class="card-title">超大单流入/流出 对未来表现影响</div>
|
||
<div class="chart-container">
|
||
<canvas ref="superChart"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-title">主力流入/流出 对未来表现影响</div>
|
||
<div class="chart-container">
|
||
<canvas ref="mainChart"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 详细数据表格(可折叠) -->
|
||
<div class="collapsible-section">
|
||
<div class="collapsible-header" @click="toggleSection('table')">
|
||
<h3>详细数据</h3>
|
||
<span :class="['toggle-icon', { expanded: expandedSections.table }]">▼</span>
|
||
</div>
|
||
<div :class="['collapsible-content', { expanded: expandedSections.table }]">
|
||
<div class="card">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>时间段</th>
|
||
<th>超大单流入</th>
|
||
<th>超大单流出</th>
|
||
<th>差异</th>
|
||
<th>主力流入</th>
|
||
<th>主力流出</th>
|
||
<th>差异</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="period in periods" :key="period">
|
||
<td><strong>{{ period }}</strong></td>
|
||
<td :class="getClass(getComparisonValue('超大单', period, '大额流入表现'))">
|
||
{{ formatPercent(getComparisonValue('超大单', period, '大额流入表现')) }}
|
||
</td>
|
||
<td :class="getClass(getComparisonValue('超大单', period, '大额流出表现'))">
|
||
{{ formatPercent(getComparisonValue('超大单', period, '大额流出表现')) }}
|
||
</td>
|
||
<td :class="getClass(getComparisonValue('超大单', period, '差异'))">
|
||
{{ formatPercent(getComparisonValue('超大单', period, '差异')) }}
|
||
</td>
|
||
<td :class="getClass(getComparisonValue('主力', period, '大额流入表现'))">
|
||
{{ formatPercent(getComparisonValue('主力', period, '大额流入表现')) }}
|
||
</td>
|
||
<td :class="getClass(getComparisonValue('主力', period, '大额流出表现'))">
|
||
{{ formatPercent(getComparisonValue('主力', period, '大额流出表现')) }}
|
||
</td>
|
||
<td :class="getClass(getComparisonValue('主力', period, '差异'))">
|
||
{{ formatPercent(getComparisonValue('主力', period, '差异')) }}
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 汇总统计(可折叠) -->
|
||
<div class="collapsible-section">
|
||
<div class="collapsible-header" @click="toggleSection('summary')">
|
||
<h3>数据汇总</h3>
|
||
<span :class="['toggle-icon', { expanded: expandedSections.summary }]">▼</span>
|
||
</div>
|
||
<div :class="['collapsible-content', { expanded: expandedSections.summary }]">
|
||
<div class="card">
|
||
<div class="summary-grid">
|
||
<div class="summary-item">
|
||
<h4>分析周期</h4>
|
||
<div class="value">{{ result.data['数据概览']['总交易日数'] }} 天</div>
|
||
<div class="sub">{{ result.data['数据概览']['日期范围']['开始'] }} 至 {{ result.data['数据概览']['日期范围']['结束'] }}</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<h4>超大单大额流入日</h4>
|
||
<div class="value">{{ result.data['数据概览']['超大单']['大额流入日数'] }} 天</div>
|
||
<div class="sub">占比 {{ (result.data['数据概览']['超大单']['大额流入日数'] / result.data['数据概览']['总交易日数'] * 100).toFixed(1) }}%</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<h4>超大单大额流出日</h4>
|
||
<div class="value">{{ result.data['数据概览']['超大单']['大额流出日数'] }} 天</div>
|
||
<div class="sub">占比 {{ (result.data['数据概览']['超大单']['大额流出日数'] / result.data['数据概览']['总交易日数'] * 100).toFixed(1) }}%</div>
|
||
</div>
|
||
<div class="summary-item">
|
||
<h4>主力大额流入日</h4>
|
||
<div class="value">{{ result.data['数据概览']['主力']['大额流入日数'] }} 天</div>
|
||
<div class="sub">占比 {{ (result.data['数据概览']['主力']['大额流入日数'] / result.data['数据概览']['总交易日数'] * 100).toFixed(1) }}%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 单只股票检测 -->
|
||
<div class="tech-signal-header" style="margin-bottom:8px;">
|
||
<div class="tech-input-row">
|
||
<input v-model="techSignalCode" placeholder="输入股票代码" class="tech-input" @keyup.enter="fetchTechSignals()">
|
||
<button type="button" class="tech-btn" @click="fetchTechSignals()" :disabled="techLoading">
|
||
{{ techLoading ? '检测中...' : '检测信号' }}
|
||
</button>
|
||
<button type="button" class="tech-btn batch" @click="batchTechSignals" :disabled="techLoading">
|
||
批量扫描关注
|
||
</button>
|
||
</div>
|
||
<div class="tech-legend">
|
||
<span class="legend-item"><span class="dot s85"></span>主升浪85%</span>
|
||
<span class="legend-item"><span class="dot s80"></span>底背离80%</span>
|
||
<span class="legend-item"><span class="dot s75"></span>龙抬头75%</span>
|
||
<span class="legend-item"><span class="dot s70"></span>真龙70%</span>
|
||
<span class="legend-item"><span class="dot s65"></span>短底背离65%</span>
|
||
<span class="legend-item"><span class="dot s60"></span>老鼠仓60%</span>
|
||
<span class="legend-item"><span class="dot s55"></span>反弹55%</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 全量信号扫描区域 -->
|
||
<div class="full-scan-trigger-section">
|
||
<!-- 加载状态提示 -->
|
||
<div v-if="fullScanLoading && !fullScanStatus" class="scan-loading-hint">
|
||
<span class="loading-spinner"></span> 正在加载全景扫描数据...
|
||
</div>
|
||
|
||
<!-- 全量扫描结果 -->
|
||
<template v-if="fullScanStatus && (fullScanResults.length > 0 || fullScanFilterRecommend !== 'all')">
|
||
<div class="scan-summary-section">
|
||
<div class="scan-summary-header" role="button" tabindex="0" :aria-expanded="!scanSummaryCollapsed" @click="scanSummaryCollapsed = !scanSummaryCollapsed" @keyup.enter="scanSummaryCollapsed = !scanSummaryCollapsed">
|
||
<span class="scan-summary-text">扫描 <b>{{ fullScanStatus.scanned }}</b><small v-if="fullScanStatus.total && fullScanStatus.scanned < fullScanStatus.total">/{{ fullScanStatus.total }}</small> 只,有信号 <b class="highlight">{{ fullScanStatus.triggered }}</b> 只</span>
|
||
<span class="scan-summary-actions">
|
||
<button type="button" class="section-close inline" @click.stop="fullScanStatus = null; fullScanResults = []" title="关闭">✕</button>
|
||
<span class="scan-summary-toggle">{{ scanSummaryCollapsed ? '▶' : '▼' }}</span>
|
||
</span>
|
||
</div>
|
||
<div v-show="!scanSummaryCollapsed" class="scan-summary-body">
|
||
<div class="scan-time-info" v-if="fullScanStatus.scan_end">
|
||
扫描时间: {{ fullScanStatus.scan_end?.substring(0, 19) }}
|
||
</div>
|
||
<div v-if="fullScanSignalDist.length > 0" class="signal-dist-bar">
|
||
<span v-for="d in fullScanSignalDist" :key="d.type" class="dist-tag"
|
||
:class="{ active: fullScanFilterTypes.includes(d.type) }" @click="toggleSignalFilter(d.type)">
|
||
{{ d.name }} <b>{{ d.count }}</b>
|
||
</span>
|
||
</div>
|
||
<div class="scan-filters">
|
||
<div class="scan-filter-row">
|
||
<span class="scan-filter-label">范围</span>
|
||
<span :class="['filter-tag', fullScanFilter === 'all' ? 'active' : '']" @click="changeFullScanFilter('all')">全部 <b v-if="fullScanStatus && fullScanStatus.scanned">{{ fullScanStatus.scanned }}</b></span>
|
||
<span :class="['filter-tag', fullScanFilter === 'triggered' ? 'active' : '']" @click="changeFullScanFilter('triggered')">有信号 <b v-if="fullScanStatus && fullScanStatus.triggered">{{ fullScanStatus.triggered }}</b></span>
|
||
</div>
|
||
<div class="scan-filter-row">
|
||
<span class="scan-filter-label">推荐</span>
|
||
<span :class="['filter-tag', 'rec-all', fullScanFilterRecommend === 'all' ? 'active' : '']" @click="changeFullScanFilterRecommend('all')">全部 {{ fullScanStatus ? fullScanStatus.scanned : '' }}</span>
|
||
<span v-for="opt in fullScanRecommendOptions" :key="opt.text"
|
||
:class="['filter-tag', getRecommendFilterClass(opt.text), fullScanFilterRecommend === opt.text ? 'active' : '']"
|
||
:title="opt.hint"
|
||
@click="changeFullScanFilterRecommend(opt.text)">
|
||
{{ opt.text }} <b v-if="opt.count > 0">{{ opt.count }}</b>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="full-scan-list-section">
|
||
<div class="scan-list-header" role="button" tabindex="0" :aria-expanded="!scanResultsCollapsed" @click="scanResultsCollapsed = !scanResultsCollapsed" @keyup.enter="scanResultsCollapsed = !scanResultsCollapsed">
|
||
<span class="scan-list-title">推荐列表 <small v-if="fullScanTotalPages > 1">{{ fullScanPage }}/{{ fullScanTotalPages }}页</small></span>
|
||
<span class="scan-summary-toggle">{{ scanResultsCollapsed ? '▶' : '▼' }}</span>
|
||
</div>
|
||
<div v-show="!scanResultsCollapsed" class="full-scan-list">
|
||
<div v-if="fullScanResultsFiltered.length === 0" class="scan-empty-hint">暂无符合条件的股票</div>
|
||
<div v-for="item in fullScanResultsFiltered" :key="item.code" class="scan-result-card"
|
||
:class="{ 'has-signal': item.triggered_count > 0, 'is-watched': searchHistory.some(h => h.code === item.code) }">
|
||
<div class="scan-card-header" @click="techSignalCode = item.code; analysisSubTab = 'scan'; fetchTechSignals(item)">
|
||
<span class="scan-watched-icon" v-if="searchHistory.some(h => h.code === item.code)">★</span>
|
||
<span class="scan-code" @dblclick.stop="toggleWatchFromScan(item.code, item.name)" title="双击添加/取消关注">{{ item.code }}</span>
|
||
<span class="scan-name">{{ item.name }}</span>
|
||
<span :class="['scan-recommend', getScanRecommend(item).cls]">{{ getScanRecommend(item).text }}</span>
|
||
</div>
|
||
<div class="scan-card-info" @click="techSignalCode = item.code; analysisSubTab = 'scan'; fetchTechSignals(item)">
|
||
<span class="scan-info-left">
|
||
<span class="scan-price" v-if="item.price">¥{{ item.price.toFixed(2) }}</span>
|
||
<span class="scan-change" v-if="item.change_pct != null"
|
||
:class="item.change_pct >= 0 ? 'up' : 'down'">{{ item.change_pct >= 0 ? '+' : '' }}{{ item.change_pct.toFixed(2) }}%</span>
|
||
</span>
|
||
<span class="scan-score-pill" v-if="item.final_score != null"
|
||
:class="'score-' + (item.final_score >= 65 ? 'high' : item.final_score >= 50 ? 'mid' : 'low')"
|
||
:title="'技术' + item.technical_score + ' / 外部' + (item.external_score >= 0 ? '+' : '') + item.external_score">
|
||
{{ item.final_score }}分
|
||
</span>
|
||
<span class="scan-triggered" v-if="item.triggered_count > 0">🔔 {{ item.triggered_count }}个信号</span>
|
||
<span class="scan-no-signal" v-else>无信号</span>
|
||
</div>
|
||
<div class="scan-signals" v-if="item.signal_status && item.signal_status.length > 0">
|
||
<span v-for="ss in item.signal_status" :key="ss.type"
|
||
:class="['scan-signal-tag', ss.triggered ? 'active' : 'inactive']" :title="ss.description">
|
||
{{ ss.triggered ? '✅' : '○' }} {{ ss.name }}
|
||
</span>
|
||
</div>
|
||
<div class="scan-holding-note" v-if="item.holding_note">{{ item.holding_note }}</div>
|
||
</div>
|
||
<div v-if="fullScanTotalPages > 1" class="scan-pagination">
|
||
<button type="button" @click.stop="fetchFullScanResults(fullScanPage - 1)" :disabled="fullScanPage <= 1">上一页</button>
|
||
<span>{{ fullScanPage }} / {{ fullScanTotalPages }}</span>
|
||
<button type="button" @click.stop="fetchFullScanResults(fullScanPage + 1)" :disabled="fullScanPage >= fullScanTotalPages">下一页</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 单只股票检测结果 -->
|
||
<div v-if="techSignalResult" class="tech-result-card">
|
||
<div class="tech-result-header">
|
||
<h4>{{ techSignalResult.stock_code }} {{ techSignalResult.stock_name }}</h4>
|
||
<span class="signal-count" v-if="(techSignalResult.signal_status || []).filter(s => s.triggered).length > 0">
|
||
🔔 {{ (techSignalResult.signal_status || []).filter(s => s.triggered).length }} 个信号
|
||
</span>
|
||
<span class="signal-count none" v-else-if="!techSignalResult.has_scan_data">无信号</span>
|
||
<button type="button" class="section-close" @click="techSignalResult = null" title="关闭">✕</button>
|
||
</div>
|
||
<div class="tech-result-actions">
|
||
<button type="button" v-if="!searchHistory.some(h => h.code === techSignalResult.stock_code)"
|
||
class="watch-btn add" @click="addToWatchFromSignal">
|
||
★ 添加关注
|
||
</button>
|
||
<button type="button" v-else class="watch-btn remove" @click="removeFromWatchFromSignal">
|
||
☆ 取消关注
|
||
</button>
|
||
</div>
|
||
<!-- 方案C:来自扫描列表时,同时显示扫描结果和实时检测 -->
|
||
<template v-if="techSignalResult.has_scan_data">
|
||
<div class="signal-source-section">
|
||
<h5 class="signal-source-title">📋 扫描结果(与列表一致)</h5>
|
||
<span class="signal-count" v-if="techSignalResult.scan_triggered_count > 0">🔔 {{ techSignalResult.scan_triggered_count }} 个信号</span>
|
||
<span class="signal-count none" v-else>无信号</span>
|
||
</div>
|
||
<div v-if="techSignalResult.recommend_text" class="tech-recommend-bar" :class="'rec-' + (techSignalResult.recommend_type || 'watch')">
|
||
<span class="tech-recommend-label">综合推荐</span>
|
||
<span class="tech-recommend-text">{{ techSignalResult.recommend_text }}</span>
|
||
<span class="tech-recommend-rate" v-if="techSignalResult.recommend_rate">{{ techSignalResult.recommend_rate }}%</span>
|
||
<div class="tech-recommend-reason">{{ techSignalResult.recommend_reason }}</div>
|
||
</div>
|
||
<!-- 综合评分明细(外部因素) -->
|
||
<div v-if="techSignalResult.final_score != null" class="scan-score-breakdown">
|
||
<div class="score-breakdown-item">
|
||
<span class="breakdown-label">技术得分</span>
|
||
<span class="breakdown-value">{{ techSignalResult.technical_score }}</span>
|
||
</div>
|
||
<div class="score-breakdown-item">
|
||
<span class="breakdown-label">外部得分</span>
|
||
<span class="breakdown-value" :class="techSignalResult.external_score >= 0 ? 'pos' : 'neg'">
|
||
{{ techSignalResult.external_score >= 0 ? '+' : '' }}{{ techSignalResult.external_score }}
|
||
</span>
|
||
</div>
|
||
<div class="score-breakdown-item highlight">
|
||
<span class="breakdown-label">综合得分</span>
|
||
<span class="breakdown-value">{{ techSignalResult.final_score }}</span>
|
||
</div>
|
||
<div class="score-breakdown-item">
|
||
<span class="breakdown-label">评级</span>
|
||
<span class="breakdown-value">{{ techSignalResult.verdict }}</span>
|
||
</div>
|
||
</div>
|
||
<!-- 外部因素详情 -->
|
||
<div v-if="techSignalResult.score_detail_loading" class="scan-external-factors loading">
|
||
<span class="loading-spinner"></span> 正在加载外部因素...
|
||
</div>
|
||
<div v-else-if="techSignalResult.score_factors" class="scan-external-factors">
|
||
<div v-if="techSignalResult.score_factors.fund_flow" class="ext-factor-item">
|
||
<span class="ext-factor-label">💰 资金面</span>
|
||
<span class="ext-factor-score" :class="techSignalResult.score_factors.fund_flow.score >= 0 ? 'pos' : 'neg'">
|
||
{{ techSignalResult.score_factors.fund_flow.score >= 0 ? '+' : '' }}{{ techSignalResult.score_factors.fund_flow.score }}
|
||
</span>
|
||
<span class="ext-factor-summary">{{ techSignalResult.score_factors.fund_flow.summary }}</span>
|
||
</div>
|
||
<div v-if="techSignalResult.score_factors.market_sentiment" class="ext-factor-item">
|
||
<span class="ext-factor-label">📊 市场情绪</span>
|
||
<span class="ext-factor-score" :class="techSignalResult.score_factors.market_sentiment.score >= 0 ? 'pos' : 'neg'">
|
||
{{ techSignalResult.score_factors.market_sentiment.score >= 0 ? '+' : '' }}{{ techSignalResult.score_factors.market_sentiment.score }}
|
||
</span>
|
||
<span class="ext-factor-summary">{{ techSignalResult.score_factors.market_sentiment.sentiment || techSignalResult.score_factors.market_sentiment.summary }}</span>
|
||
</div>
|
||
<div v-if="techSignalResult.score_factors.external" class="ext-factor-item">
|
||
<span class="ext-factor-label">🌍 外部环境</span>
|
||
<span class="ext-factor-score" :class="techSignalResult.score_factors.external.total_score >= 0 ? 'pos' : 'neg'">
|
||
{{ techSignalResult.score_factors.external.total_score >= 0 ? '+' : '' }}{{ techSignalResult.score_factors.external.total_score }}
|
||
</span>
|
||
<span class="ext-factor-summary">{{ techSignalResult.score_factors.external.summary }}</span>
|
||
</div>
|
||
<div v-if="techSignalResult.score_factors.policy" class="ext-factor-item">
|
||
<span class="ext-factor-label">📜 政策面</span>
|
||
<span class="ext-factor-score" :class="techSignalResult.score_factors.policy.score >= 0 ? 'pos' : 'neg'">
|
||
{{ techSignalResult.score_factors.policy.score >= 0 ? '+' : '' }}{{ techSignalResult.score_factors.policy.score }}
|
||
</span>
|
||
<span class="ext-factor-summary">{{ techSignalResult.score_factors.policy.summary }}</span>
|
||
</div>
|
||
<div v-if="techSignalResult.score_factors.news" class="ext-factor-item">
|
||
<span class="ext-factor-label">📰 公告/异动</span>
|
||
<span class="ext-factor-score">{{ techSignalResult.score_factors.news.total_score }}</span>
|
||
<span class="ext-factor-summary">{{ techSignalResult.score_factors.news.summary }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="techSignalResult.signal_status && techSignalResult.signal_status.length > 0" class="signal-status-list">
|
||
<div v-for="ss in techSignalResult.signal_status" :key="'scan-'+ss.type"
|
||
:class="['signal-status-item', ss.triggered ? 'triggered' : 'inactive']">
|
||
<div class="ss-header">
|
||
<span :class="['ss-dot', 's' + (ss.strength||70)]"></span>
|
||
<span class="ss-name">{{ ss.name }}</span>
|
||
<span class="ss-strength" v-if="ss.strength">{{ ss.strength }}%</span>
|
||
<span :class="['ss-badge', ss.triggered ? 'active' : 'wait']">{{ ss.triggered ? '已触发' : '未触发' }}</span>
|
||
</div>
|
||
<div class="ss-desc" v-if="ss.description">{{ ss.description }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="signal-source-section">
|
||
<h5 class="signal-source-title">⚡ 当前实时检测</h5>
|
||
<span v-if="techSignalResult.realtime_loading" class="realtime-loading">加载中...</span>
|
||
<span v-else-if="techSignalResult.realtime_error" class="realtime-error">{{ techSignalResult.realtime_error }}</span>
|
||
<template v-else-if="techSignalResult.realtime_signal_status">
|
||
<span class="signal-count" v-if="techSignalResult.realtime_triggered_count > 0">🔔 {{ techSignalResult.realtime_triggered_count }} 个信号</span>
|
||
<span class="signal-count none" v-else>无信号</span>
|
||
</template>
|
||
</div>
|
||
<div v-if="!techSignalResult.realtime_loading && techSignalResult.realtime_recommend_text" class="tech-recommend-bar realtime-bar" :class="'rec-' + (techSignalResult.realtime_recommend_type || 'watch')">
|
||
<span class="tech-recommend-label">综合推荐</span>
|
||
<span class="tech-recommend-text">{{ techSignalResult.realtime_recommend_text }}</span>
|
||
<span class="tech-recommend-rate" v-if="techSignalResult.realtime_recommend_rate">{{ techSignalResult.realtime_recommend_rate }}%</span>
|
||
<div class="tech-recommend-reason">{{ techSignalResult.realtime_recommend_reason }}</div>
|
||
<div v-if="techSignalResult.realtime_holding_note" class="tech-holding-note">⚠️ {{ techSignalResult.realtime_holding_note }}</div>
|
||
</div>
|
||
<div v-if="!techSignalResult.realtime_loading && techSignalResult.realtime_signal_status && techSignalResult.realtime_signal_status.length > 0" class="signal-status-list">
|
||
<div v-for="ss in techSignalResult.realtime_signal_status" :key="'rt-'+ss.type"
|
||
:class="['signal-status-item', ss.triggered ? 'triggered' : 'inactive']">
|
||
<div class="ss-header">
|
||
<span :class="['ss-dot', 's' + (ss.strength||70)]"></span>
|
||
<span class="ss-name">{{ ss.name }}</span>
|
||
<span class="ss-strength" v-if="ss.strength">{{ ss.strength }}%</span>
|
||
<span :class="['ss-badge', ss.triggered ? 'active' : 'wait']">{{ ss.triggered ? '已触发' : '未触发' }}</span>
|
||
</div>
|
||
<div class="ss-desc" v-if="ss.description">{{ ss.description }}</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="techSignalResult.realtime_indicators" class="tech-indicators">
|
||
<div class="indicator-group">
|
||
<span class="ind-label">MACD</span>
|
||
<span :class="['ind-val', (techSignalResult.realtime_indicators.macd?.dif||0) > 0 ? 'pos' : 'neg']">DIF {{ techSignalResult.realtime_indicators.macd?.dif }}</span>
|
||
<span :class="['ind-val', (techSignalResult.realtime_indicators.macd?.dea||0) > 0 ? 'pos' : 'neg']">DEA {{ techSignalResult.realtime_indicators.macd?.dea }}</span>
|
||
</div>
|
||
<div class="indicator-group">
|
||
<span class="ind-label">SKDJ</span>
|
||
<span :class="['ind-val', techSignalResult.realtime_indicators.skdj?.k < 20 ? 'oversold' : (techSignalResult.realtime_indicators.skdj?.k > 80 ? 'overbought' : '')]">K {{ techSignalResult.realtime_indicators.skdj?.k }}</span>
|
||
<span class="ind-val">D {{ techSignalResult.realtime_indicators.skdj?.d }}</span>
|
||
</div>
|
||
<div class="indicator-group">
|
||
<span class="ind-label">EMA</span>
|
||
<span class="ind-val">EMA3 {{ techSignalResult.realtime_indicators.ema?.ema3 }}</span>
|
||
<span class="ind-val">EMA21 {{ techSignalResult.realtime_indicators.ema?.ema21 }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<!-- 手动输入检测:仅显示实时结果 -->
|
||
<template v-else>
|
||
<div v-if="techSignalResult.recommend_text" class="tech-recommend-bar" :class="'rec-' + (techSignalResult.recommend_type || 'watch')">
|
||
<span class="tech-recommend-label">综合推荐</span>
|
||
<span class="tech-recommend-text">{{ techSignalResult.recommend_text }}</span>
|
||
<span class="tech-recommend-rate" v-if="techSignalResult.recommend_rate">{{ techSignalResult.recommend_rate }}%</span>
|
||
<div class="tech-recommend-reason">{{ techSignalResult.recommend_reason }}</div>
|
||
<div v-if="techSignalResult.holding_note" class="tech-holding-note">⚠️ {{ techSignalResult.holding_note }}</div>
|
||
</div>
|
||
<div class="tech-indicators" v-if="techSignalResult.indicators">
|
||
<div class="indicator-group">
|
||
<span class="ind-label">MACD</span>
|
||
<span :class="['ind-val', techSignalResult.indicators.macd?.dif > 0 ? 'pos' : 'neg']">DIF {{ techSignalResult.indicators.macd?.dif }}</span>
|
||
<span :class="['ind-val', techSignalResult.indicators.macd?.dea > 0 ? 'pos' : 'neg']">DEA {{ techSignalResult.indicators.macd?.dea }}</span>
|
||
</div>
|
||
<div class="indicator-group">
|
||
<span class="ind-label">SKDJ</span>
|
||
<span :class="['ind-val', techSignalResult.indicators.skdj?.k < 20 ? 'oversold' : (techSignalResult.indicators.skdj?.k > 80 ? 'overbought' : '')]">K {{ techSignalResult.indicators.skdj?.k }}</span>
|
||
<span class="ind-val">D {{ techSignalResult.indicators.skdj?.d }}</span>
|
||
</div>
|
||
<div class="indicator-group">
|
||
<span class="ind-label">EMA</span>
|
||
<span class="ind-val">EMA3 {{ techSignalResult.indicators.ema?.ema3 }}</span>
|
||
<span class="ind-val">EMA21 {{ techSignalResult.indicators.ema?.ema21 }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="techSignalResult.signal_status && techSignalResult.signal_status.length > 0" class="signal-status-list">
|
||
<div v-for="ss in techSignalResult.signal_status" :key="ss.type"
|
||
:class="['signal-status-item', ss.triggered ? 'triggered' : 'inactive']">
|
||
<div class="ss-header">
|
||
<span :class="['ss-dot', 's' + ss.strength]"></span>
|
||
<span class="ss-name">{{ ss.name }}</span>
|
||
<span class="ss-strength">{{ ss.strength }}%</span>
|
||
<span :class="['ss-badge', ss.triggered ? 'active' : 'wait']">{{ ss.triggered ? '已触发' : '未触发' }}</span>
|
||
</div>
|
||
<div class="ss-desc">{{ ss.description }}</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div v-if="(techSignalResult.signals || []).length > 0" class="tech-signal-list">
|
||
<h5 style="color:var(--text-secondary);margin:12px 0 8px;font-size:13px;">📋 近期触发记录</h5>
|
||
<div v-for="sig in techSignalResult.signals" :key="sig.date + sig.type"
|
||
:class="['tech-signal-item', 'strength-' + sig.strength]">
|
||
<div class="sig-header">
|
||
<span class="sig-name">{{ sig.name }}</span>
|
||
<span class="sig-strength">胜率 {{ sig.strength }}%</span>
|
||
<span class="sig-date">{{ sig.date }}</span>
|
||
</div>
|
||
<div class="sig-desc">{{ sig.description }}</div>
|
||
<div class="sig-price">价格: ¥{{ sig.price?.toFixed(2) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 批量扫描结果(复用全景扫描卡片样式) -->
|
||
<div v-if="techBatchResults.length > 0" class="tech-batch-results">
|
||
<div class="batch-results-header">
|
||
<h4 class="section-title">关注股票信号扫描 <span class="badge">{{ techBatchResults.length }}</span></h4>
|
||
<button type="button" class="section-close" @click="techBatchResults = []" title="关闭">✕</button>
|
||
</div>
|
||
<div class="full-scan-list">
|
||
<div v-for="item in techBatchResults" :key="item.code" class="scan-result-card"
|
||
:class="{ 'has-signal': item.triggered_count > 0, 'is-watched': true }">
|
||
<div class="scan-card-header" @click="techSignalCode = item.code; analysisSubTab = 'scan'; fetchTechSignals(item)">
|
||
<span class="scan-watched-icon">★</span>
|
||
<span class="scan-code" @dblclick.stop="toggleWatchFromScan(item.code, item.name)" title="双击取消关注">{{ item.code }}</span>
|
||
<span class="scan-name">{{ item.name }}</span>
|
||
<span :class="['scan-recommend', getScanRecommend(item).cls]">{{ getScanRecommend(item).text }}</span>
|
||
</div>
|
||
<div class="scan-card-info" @click="techSignalCode = item.code; analysisSubTab = 'scan'; fetchTechSignals(item)">
|
||
<span class="scan-info-left">
|
||
<span class="scan-price" v-if="item.price">¥{{ item.price.toFixed(2) }}</span>
|
||
<span class="scan-change" v-if="item.change_pct != null"
|
||
:class="item.change_pct >= 0 ? 'up' : 'down'">{{ item.change_pct >= 0 ? '+' : '' }}{{ item.change_pct.toFixed(2) }}%</span>
|
||
</span>
|
||
<span class="scan-triggered" v-if="item.triggered_count > 0">🔔 {{ item.triggered_count }}个信号</span>
|
||
<span class="scan-no-signal" v-else>无信号</span>
|
||
</div>
|
||
<div class="scan-signals" v-if="item.signal_status && item.signal_status.length > 0">
|
||
<span v-for="ss in item.signal_status" :key="ss.type"
|
||
:class="['scan-signal-tag', ss.triggered ? 'active' : 'inactive']" :title="ss.description">
|
||
{{ ss.triggered ? '✅' : '○' }} {{ ss.name }}
|
||
</span>
|
||
</div>
|
||
<div class="scan-holding-note" v-if="item.holding_note">{{ item.holding_note }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- 全景扫描子页面结束 -->
|
||
|
||
<!-- 个股深析子页面 -->
|
||
<div v-show="analysisSubTab === 'bull'" class="deep-analysis-page">
|
||
|
||
<!-- 输入区 -->
|
||
<div class="deep-input-card">
|
||
<div class="deep-input-row">
|
||
<input v-model="deepCode" placeholder="输入股票代码,如 601609"
|
||
class="deep-code-input" @keyup.enter="fetchDeepAnalysis()" maxlength="6">
|
||
<button type="button" class="deep-analyze-btn" @click="fetchDeepAnalysis()" :disabled="deepLoading">
|
||
{{ deepLoading ? '分析中...' : '深度分析' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div v-if="deepLoading" class="skeleton-card">
|
||
<div class="skeleton-line medium"></div>
|
||
<div class="skeleton-line long"></div>
|
||
<div class="skeleton-grid">
|
||
<div class="skeleton-line short"></div>
|
||
<div class="skeleton-line short"></div>
|
||
</div>
|
||
<div class="skeleton-line long"></div>
|
||
<div class="skeleton-line medium"></div>
|
||
<div class="skeleton-line long"></div>
|
||
</div>
|
||
|
||
<!-- 分析报告 -->
|
||
<div v-if="deepReport && !deepLoading" class="deep-report">
|
||
|
||
<!-- 头部:股票信息+综合评分 -->
|
||
<div class="deep-header-card">
|
||
<div class="deep-header-left">
|
||
<div class="deep-stock-name">{{ deepReport.stock_name }}</div>
|
||
<div class="deep-stock-code">{{ deepReport.stock_code }}</div>
|
||
</div>
|
||
<div class="deep-header-center" v-if="deepReport.realtime">
|
||
<div class="deep-price">¥{{ deepReport.realtime.price.toFixed(2) }}</div>
|
||
<div class="deep-change" :class="deepReport.realtime.change_pct >= 0 ? 'up' : 'down'">
|
||
{{ deepReport.realtime.change_pct >= 0 ? '+' : '' }}{{ deepReport.realtime.change_pct.toFixed(2) }}%
|
||
</div>
|
||
</div>
|
||
<div class="deep-header-right">
|
||
<div class="deep-score-circle" :class="'score-' + (deepReport.deep_score >= 65 ? 'high' : deepReport.deep_score >= 50 ? 'mid' : 'low')">
|
||
<span class="score-num">{{ deepReport.deep_score }}</span>
|
||
</div>
|
||
<div class="deep-verdict">{{ deepReport.verdict }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 三项得分明细 -->
|
||
<div v-if="deepReport.comprehensive" class="deep-score-breakdown">
|
||
<div class="score-breakdown-item">
|
||
<span class="breakdown-label">技术得分</span>
|
||
<span class="breakdown-value">{{ deepReport.comprehensive.technical_score }}</span>
|
||
</div>
|
||
<div class="score-breakdown-item">
|
||
<span class="breakdown-label">外部得分</span>
|
||
<span class="breakdown-value" :class="deepReport.comprehensive.external_score >= 0 ? 'pos' : 'neg'">
|
||
{{ deepReport.comprehensive.external_score >= 0 ? '+' : '' }}{{ deepReport.comprehensive.external_score }}
|
||
</span>
|
||
</div>
|
||
<div class="score-breakdown-item highlight">
|
||
<span class="breakdown-label">综合得分</span>
|
||
<span class="breakdown-value">{{ deepReport.comprehensive.final_score }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI通俗解说 -->
|
||
<div v-if="deepReport.ai_summary" class="deep-ai-summary">
|
||
<div class="deep-ai-title">💡 AI 解读</div>
|
||
<div class="deep-ai-text">
|
||
<p>{{ deepReport.ai_summary.text }}</p>
|
||
</div>
|
||
<div class="ai-action-tip">
|
||
<span class="ai-action-label">操作建议:</span>{{ deepReport.ai_summary.action_tip }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 推荐操作 -->
|
||
<div class="deep-section">
|
||
<div class="deep-section-title">系统推荐</div>
|
||
<div class="deep-recommend-bar" :class="'rec-' + deepReport.recommend.signal_type">
|
||
<span class="rec-display">{{ deepReport.recommend.display }}</span>
|
||
<span class="rec-rate">{{ deepReport.recommend.rate }}分</span>
|
||
<span class="rec-reason-text">{{ deepReport.recommend.reason }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 信号列表 -->
|
||
<div class="deep-section" v-if="deepReport.signals && deepReport.signals.length">
|
||
<div class="deep-section-title">触发信号 ({{ deepReport.signals.length }}个)</div>
|
||
<div class="deep-signal-list">
|
||
<div v-for="sig in deepReport.signals" :key="sig.type" class="deep-signal-item">
|
||
<span class="deep-sig-name">{{ sig.name }}</span>
|
||
<span class="deep-sig-strength">{{ sig.strength }}%</span>
|
||
<span class="deep-sig-desc">{{ sig.description }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 价格位置 -->
|
||
<div class="deep-section">
|
||
<div class="deep-section-title">价格位置</div>
|
||
<div class="deep-position-grid">
|
||
<div v-for="(pos, key) in deepReport.position" :key="key" class="deep-pos-item">
|
||
<div class="pos-label">{{ key.replace('d','') }}日</div>
|
||
<div class="pos-range">{{ pos.low }} ~ {{ pos.high }}</div>
|
||
<div class="pos-bar-wrap">
|
||
<div class="pos-bar-bg">
|
||
<div class="pos-bar-fill" :style="{ width: pos.range_pct + '%' }"
|
||
:class="pos.range_pct > 80 ? 'high' : pos.range_pct < 30 ? 'low' : 'mid'"></div>
|
||
</div>
|
||
<span class="pos-pct">{{ pos.range_pct }}%</span>
|
||
</div>
|
||
<div class="pos-space">
|
||
<span class="space-up">↑{{ pos.up_space }}%</span>
|
||
<span class="space-down">↓{{ pos.down_risk }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 支撑压力 -->
|
||
<div class="deep-section">
|
||
<div class="deep-section-title">支撑与压力</div>
|
||
<div class="deep-sr-grid">
|
||
<div class="sr-col">
|
||
<div class="sr-col-title support-title">支撑位</div>
|
||
<div v-for="s in deepReport.supports" :key="s.name" class="sr-item support">
|
||
<span class="sr-name">{{ s.name }}</span>
|
||
<span class="sr-level">{{ s.level }}</span>
|
||
</div>
|
||
<div v-if="!deepReport.supports.length" class="sr-empty">无</div>
|
||
</div>
|
||
<div class="sr-col">
|
||
<div class="sr-col-title resist-title">压力位</div>
|
||
<div v-for="r in deepReport.resistances" :key="r.name" class="sr-item resist">
|
||
<span class="sr-name">{{ r.name }}</span>
|
||
<span class="sr-level">{{ r.level }}</span>
|
||
</div>
|
||
<div v-if="!deepReport.resistances.length" class="sr-empty">无</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 量价与形态 -->
|
||
<div class="deep-section">
|
||
<div class="deep-section-title">量价分析</div>
|
||
<div class="deep-vol-info">
|
||
<span class="vol-tag" :class="deepReport.volume.ratio >= 1.3 ? 'vol-up' : deepReport.volume.ratio < 0.7 ? 'vol-dn' : ''">
|
||
{{ deepReport.volume.trend }}
|
||
</span>
|
||
<span class="vol-detail">量比 {{ deepReport.volume.ratio }}x · 5日均量 {{ (deepReport.volume.avg_5/10000).toFixed(0) }}万</span>
|
||
</div>
|
||
<div v-if="deepReport.patterns.length" class="deep-patterns">
|
||
<span v-for="p in deepReport.patterns" :key="p.name"
|
||
class="pattern-tag" :class="p.bullish === true ? 'bullish' : p.bullish === false ? 'bearish' : ''">
|
||
{{ p.name }} <small>{{ p.desc }}</small>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 均线与基本面 -->
|
||
<div class="deep-section">
|
||
<div class="deep-section-title">技术指标</div>
|
||
<div class="deep-ma-info">
|
||
<span class="ma-trend-tag" :class="deepReport.ma_trend">{{ deepReport.ma_trend_label }}</span>
|
||
<span v-for="(val, key) in deepReport.ma" :key="key" class="ma-val">
|
||
{{ key.toUpperCase() }}={{ val }}
|
||
</span>
|
||
</div>
|
||
<div v-if="deepReport.realtime" class="deep-fundamental">
|
||
<span v-if="deepReport.realtime.pe > 0">PE {{ deepReport.realtime.pe.toFixed(1) }}</span>
|
||
<span v-if="deepReport.realtime.pb > 0">PB {{ deepReport.realtime.pb.toFixed(1) }}</span>
|
||
<span v-if="deepReport.realtime.total_market_cap > 0">
|
||
市值 {{ (deepReport.realtime.total_market_cap/100000000).toFixed(0) }}亿
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 评分明细 -->
|
||
<div class="deep-section">
|
||
<div class="deep-section-title">评分明细</div>
|
||
<div class="deep-score-details">
|
||
<span v-for="r in deepReport.score_reasons" :key="r" class="score-reason-tag"
|
||
:class="r.includes('+') ? 'positive' : 'negative'">{{ r }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="!deepReport && !deepLoading" class="empty-state">
|
||
<div class="empty-state-icon">📊</div>
|
||
<div class="empty-state-title">个股深析</div>
|
||
<div class="empty-state-desc">输入股票代码,获取深度技术分析报告<br>包含价格位置、压力支撑、量价分析、空间估算、综合评分</div>
|
||
</div>
|
||
</div><!-- 个股深析子页面结束 -->
|
||
|
||
<!-- 买入分析子页面 -->
|
||
<div v-show="analysisSubTab === 'buyanalysis'" class="buy-analysis-page">
|
||
|
||
<div class="buy-analysis-header">
|
||
<div class="buy-analysis-title">买入股深度分析</div>
|
||
<div class="buy-analysis-desc">对全景扫描中推荐买入的股票批量执行深度分析</div>
|
||
<button type="button" class="deep-analyze-btn" @click="fetchBuyAnalysis()" :disabled="buyAnalysisLoading" style="margin-top:8px;">
|
||
{{ buyAnalysisLoading ? '分析中...' : '刷新分析' }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="buyAnalysisLoading" class="bull-loading">
|
||
<span class="loading-dot"></span> 正在批量分析买入推荐股...({{ buyAnalysisProgress }}/{{ buyAnalysisTotal }})
|
||
</div>
|
||
|
||
<div v-if="!buyAnalysisLoading && buyAnalysisList.length === 0" class="empty-state">
|
||
<div class="empty-state-icon">📋</div>
|
||
<div class="empty-state-title">暂无买入推荐股票</div>
|
||
<div class="empty-state-desc">当全景扫描中有买入推荐时,可在此批量执行深度分析</div>
|
||
</div>
|
||
|
||
<div v-if="buyAnalysisList.length > 0" class="buy-analysis-list">
|
||
<div v-for="(item, idx) in buyAnalysisList" :key="item.stock_code" class="buy-analysis-card">
|
||
<div class="buy-card-header" @click="item._expanded = !item._expanded">
|
||
<div class="buy-card-left">
|
||
<span class="buy-card-name">{{ item.stock_name }}</span>
|
||
<span class="buy-card-code">{{ item.stock_code }}</span>
|
||
</div>
|
||
<div class="buy-card-center">
|
||
<span class="buy-card-price" v-if="item.realtime">¥{{ item.realtime.price.toFixed(2) }}</span>
|
||
<span class="buy-card-change" v-if="item.realtime" :class="item.realtime.change_pct >= 0 ? 'up' : 'down'">
|
||
{{ item.realtime.change_pct >= 0 ? '+' : '' }}{{ item.realtime.change_pct.toFixed(2) }}%
|
||
</span>
|
||
</div>
|
||
<div class="buy-card-right">
|
||
<span class="buy-card-score" :class="'score-' + (item.deep_score >= 65 ? 'high' : item.deep_score >= 50 ? 'mid' : 'low')">
|
||
{{ item.deep_score }}分
|
||
</span>
|
||
<span class="buy-card-verdict">{{ item.verdict }}</span>
|
||
</div>
|
||
<span class="buy-card-arrow" :class="{ expanded: item._expanded }">▸</span>
|
||
</div>
|
||
<div v-show="item._expanded" class="buy-card-detail">
|
||
<div v-if="item.comprehensive" class="deep-score-breakdown" style="margin-bottom:8px;">
|
||
<div class="score-breakdown-item">
|
||
<span class="breakdown-label">技术得分</span>
|
||
<span class="breakdown-value">{{ item.comprehensive.technical_score }}</span>
|
||
</div>
|
||
<div class="score-breakdown-item">
|
||
<span class="breakdown-label">外部得分</span>
|
||
<span class="breakdown-value" :class="item.comprehensive.external_score >= 0 ? 'pos' : 'neg'">
|
||
{{ item.comprehensive.external_score >= 0 ? '+' : '' }}{{ item.comprehensive.external_score }}
|
||
</span>
|
||
</div>
|
||
<div class="score-breakdown-item highlight">
|
||
<span class="breakdown-label">综合得分</span>
|
||
<span class="breakdown-value">{{ item.comprehensive.final_score }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="item.ai_summary" class="deep-ai-summary" style="margin-bottom:8px;">
|
||
<div class="deep-ai-title">💡 AI 解读</div>
|
||
<div class="deep-ai-text"><p>{{ item.ai_summary.text }}</p></div>
|
||
<div class="ai-action-tip">
|
||
<span class="ai-action-label">操作建议:</span>{{ item.ai_summary.action_tip }}
|
||
</div>
|
||
</div>
|
||
<div class="buy-detail-grid">
|
||
<div class="buy-detail-item">
|
||
<span class="buy-detail-label">系统推荐</span>
|
||
<span class="buy-detail-value" style="color:var(--up);font-weight:600;">{{ item.recommend.display }} {{ item.recommend.rate }}分</span>
|
||
</div>
|
||
<div class="buy-detail-item">
|
||
<span class="buy-detail-label">推荐理由</span>
|
||
<span class="buy-detail-value">{{ item.recommend.reason }}</span>
|
||
</div>
|
||
<div class="buy-detail-item" v-if="item.signals && item.signals.length">
|
||
<span class="buy-detail-label">触发信号</span>
|
||
<span class="buy-detail-value">
|
||
<span v-for="sig in item.signals" :key="sig.type" class="deep-sig-name" style="margin-right:6px;">{{ sig.name }}</span>
|
||
</span>
|
||
</div>
|
||
<div class="buy-detail-item" v-if="item.supports && item.supports.length">
|
||
<span class="buy-detail-label">支撑位</span>
|
||
<span class="buy-detail-value">{{ item.supports.map(s => s.level.toFixed(2)).join(' / ') }}</span>
|
||
</div>
|
||
<div class="buy-detail-item" v-if="item.resistances && item.resistances.length">
|
||
<span class="buy-detail-label">压力位</span>
|
||
<span class="buy-detail-value">{{ item.resistances.map(r => r.level.toFixed(2)).join(' / ') }}</span>
|
||
</div>
|
||
<div class="buy-detail-item">
|
||
<span class="buy-detail-label">量能</span>
|
||
<span class="buy-detail-value">{{ item.volume ? item.volume.trend + ' (量比' + item.volume.ratio + 'x)' : '-' }}</span>
|
||
</div>
|
||
<div class="buy-detail-item">
|
||
<span class="buy-detail-label">均线</span>
|
||
<span class="buy-detail-value">{{ item.ma_trend_label }}</span>
|
||
</div>
|
||
</div>
|
||
<button type="button" class="deep-analyze-btn" style="margin-top:8px;font-size:12px;padding:6px 12px;"
|
||
@click="deepCode = item.stock_code; analysisSubTab = 'bull'; fetchDeepAnalysis();">
|
||
查看完整报告 →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- 买入分析子页面结束 -->
|
||
|
||
<!-- (模拟交易已移至模型tab) -->
|
||
<div class="hidden">
|
||
<!-- 模拟交易统计卡片 -->
|
||
<div class="sim-stats-card">
|
||
<div class="sim-stats-header">
|
||
<h4>智能交易 <span v-if="smartAlgoConfig" style="font-size:12px;color:var(--text-muted);">{{ smartAlgoConfig.algo_name }}</span></h4>
|
||
<div class="sim-actions">
|
||
<button type="button" class="sim-btn auto" @click="executeAutoTrade" :disabled="simAutoTrading">
|
||
{{ simAutoTrading ? '执行中...' : '执行' }}
|
||
</button>
|
||
<button type="button" class="sim-btn reset" @click="confirmResetSim">重置</button>
|
||
</div>
|
||
</div>
|
||
<div class="sim-stats-grid">
|
||
<div class="sim-stat-item">
|
||
<div class="sim-stat-value">{{ formatMoney(smartAlgoConfig?.total_capital || simStats.initial_capital || 200000) }}</div>
|
||
<div class="sim-stat-label">总本金</div>
|
||
</div>
|
||
<div class="sim-stat-item">
|
||
<div class="sim-stat-value">{{ formatMoney(simStats.total_market_value || 0) }}</div>
|
||
<div class="sim-stat-label">持仓市值</div>
|
||
</div>
|
||
<div class="sim-stat-item">
|
||
<div class="sim-stat-value">{{ formatMoney(simStats.cash || (smartAlgoConfig?.total_capital || 200000)) }}</div>
|
||
<div class="sim-stat-label">可用资金</div>
|
||
</div>
|
||
<div class="sim-stat-item" :class="(simStats.total_profit || 0) >= 0 ? 'profit' : 'loss'">
|
||
<div class="sim-stat-value">{{ (simStats.total_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.total_profit || 0) }}</div>
|
||
<div class="sim-stat-label">总盈亏</div>
|
||
</div>
|
||
<div class="sim-stat-item" :class="(simStats.profit_rate || 0) >= 0 ? 'profit' : 'loss'">
|
||
<div class="sim-stat-value">{{ (simStats.profit_rate || 0) >= 0 ? '+' : '' }}{{ (simStats.profit_rate || 0).toFixed(2) }}%</div>
|
||
<div class="sim-stat-label">收益率</div>
|
||
</div>
|
||
<div class="sim-stat-item">
|
||
<div class="sim-stat-value">{{ simStats.total_trades || 0 }}</div>
|
||
<div class="sim-stat-label">交易次数</div>
|
||
</div>
|
||
</div>
|
||
<div class="sim-stats-detail">
|
||
<span :class="(simStats.unrealized_profit || 0) >= 0 ? 'profit' : 'loss'">
|
||
浮动 {{ (simStats.unrealized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.unrealized_profit || 0) }}
|
||
</span>
|
||
<span :class="(simStats.realized_profit || 0) >= 0 ? 'profit' : 'loss'">
|
||
已实现 {{ (simStats.realized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.realized_profit || 0) }}
|
||
</span>
|
||
<button type="button" class="sim-btn refresh" @click="updateSimPrices" :disabled="simPositions.length === 0 || simPriceRefreshing" v-if="simPositions.length > 0" style="margin-left:auto;font-size:11px;padding:2px 8px;">
|
||
{{ simPriceRefreshing ? '刷新中...' : '刷新现价' }}
|
||
</button>
|
||
</div>
|
||
<!-- 模拟 vs 实盘 对比看板 -->
|
||
<div class="sim-compare-panel" v-if="simStats.total_fees > 0 || simStats.total_trades > 0">
|
||
<div class="sim-compare-header" @click="compareExpanded = !compareExpanded" class="cursor-pointer">
|
||
<span style="font-size:12px;font-weight:600;color:var(--text-secondary);">
|
||
<svg class="icon-xs" viewBox="0 0 24 24" fill="none" stroke="var(--warning)" stroke-width="2"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>
|
||
模拟 vs 实盘对比
|
||
</span>
|
||
<span :class="['toggle-icon', { expanded: compareExpanded }]" style="color:var(--text-muted);font-size:10px;">▼</span>
|
||
</div>
|
||
<div v-show="compareExpanded" class="sim-compare-body">
|
||
<div class="sim-compare-row">
|
||
<div class="sim-compare-col">
|
||
<div class="sim-compare-label">毛收益 (不含费用)</div>
|
||
<div class="sim-compare-value" :class="(simStats.gross_profit || 0) >= 0 ? 'profit' : 'loss'">
|
||
{{ (simStats.gross_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.gross_profit || 0) }}
|
||
<small>({{ (simStats.gross_rate || 0) >= 0 ? '+' : '' }}{{ (simStats.gross_rate || 0).toFixed(2) }}%)</small>
|
||
</div>
|
||
</div>
|
||
<div class="sim-compare-arrow">→</div>
|
||
<div class="sim-compare-col">
|
||
<div class="sim-compare-label">净收益 (扣除费用)</div>
|
||
<div class="sim-compare-value" :class="(simStats.net_profit || 0) >= 0 ? 'profit' : 'loss'">
|
||
{{ (simStats.net_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.net_profit || 0) }}
|
||
<small>({{ (simStats.net_rate || 0) >= 0 ? '+' : '' }}{{ (simStats.net_rate || 0).toFixed(2) }}%)</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sim-compare-fees">
|
||
<span>总手续费: <b style="color:var(--up);">-{{ formatMoney(simStats.total_fees || 0) }}</b></span>
|
||
<span style="color:var(--text-dim);font-size:11px;">
|
||
佣金 ¥{{ (simStats.total_commission || 0).toFixed(2) }} +
|
||
印花税 ¥{{ (simStats.total_stamp_tax || 0).toFixed(2) }}
|
||
</span>
|
||
</div>
|
||
<div class="sim-compare-tip" v-if="simStats.total_fees > 0">
|
||
<span style="color:var(--text-muted);font-size:11px;">
|
||
💡 手续费占毛利 {{ simStats.gross_profit > 0 ? ((simStats.total_fees / simStats.gross_profit) * 100).toFixed(1) : '0.0' }}%
|
||
| 每笔均费 ¥{{ simStats.total_trades > 0 ? (simStats.total_fees / simStats.total_trades).toFixed(2) : '0.00' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 算法配置卡片 -->
|
||
<div class="sim-rule-card collapsible">
|
||
<div class="sim-rule-header" @click="smartConfigExpanded = !smartConfigExpanded">
|
||
<h4>⚙️ 算法配置 <span v-if="smartAlgoIsDefault" style="color:var(--hold);font-size:12px;">(默认)</span></h4>
|
||
<span :class="['toggle-icon', { expanded: smartConfigExpanded }]">▼</span>
|
||
</div>
|
||
<div v-show="smartConfigExpanded" class="padding-y-sm">
|
||
|
||
<!-- 模板选择 -->
|
||
<div v-if="smartTemplates.length > 0" style="margin-bottom:12px;">
|
||
<div style="font-size:13px;font-weight:600;margin-bottom:6px;color:var(--text-secondary);">快速选择算法模板:</div>
|
||
<div class="flex-wrap-gap">
|
||
<button type="button" v-for="tpl in smartTemplates" :key="tpl.name"
|
||
class="sim-btn"
|
||
:class="{'auto': smartAlgoConfig?.algo_name === tpl.display_name}"
|
||
@click="applyAlgoTemplate(tpl.name)"
|
||
style="font-size:12px;padding:4px 10px;">
|
||
{{ tpl.display_name }}
|
||
<span v-if="tpl.backtest_annual_return" style="opacity:0.7;font-size:11px;"> (年化{{ tpl.backtest_annual_return }}%)</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 当前配置详情 -->
|
||
<div v-if="smartAlgoConfig" style="font-size:12px;color:var(--text-secondary);line-height:1.8;">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px 16px;">
|
||
<span>止盈: <strong style="color:var(--teal);">{{ smartAlgoConfig.take_profit_pct }}%</strong></span>
|
||
<span>止损: <strong style="color:var(--up);">{{ smartAlgoConfig.stop_loss_pct }}%</strong></span>
|
||
<span>仓位: <strong>{{ smartAlgoConfig.position_pct }}%</strong> <span v-if="smartAlgoConfig.signal_weight">+信号加权</span></span>
|
||
<span>最长持仓: <strong>{{ smartAlgoConfig.max_hold_days }}</strong>天</span>
|
||
<span>部分止盈: <strong>{{ smartAlgoConfig.partial_exit_pct }}%</strong></span>
|
||
<span>跟踪回撤: <strong>{{ smartAlgoConfig.momentum_trail_gap }}%</strong></span>
|
||
<span>保本触发: <strong>{{ smartAlgoConfig.breakeven_at > 0 ? smartAlgoConfig.breakeven_at + '%' : '关闭' }}</strong></span>
|
||
<span>卖出确认: <strong>{{ smartAlgoConfig.sell_confirm_days }}</strong>天</span>
|
||
<span>买入时点: <strong style="color:var(--accent-cyan);">{{ smartAlgoConfig.buy_time }}</strong></span>
|
||
<span>卖出时点: <strong style="color:var(--accent-cyan);">{{ smartAlgoConfig.sell_time }}</strong></span>
|
||
<span>忽略卖信号: <strong>{{ smartAlgoConfig.ignore_sell_signal ? '是' : '否' }}</strong></span>
|
||
<span>连涨不平仓: <strong>{{ smartAlgoConfig.no_timeout_if_rising ? '是' : '否' }}</strong></span>
|
||
</div>
|
||
|
||
<!-- 编辑模式 -->
|
||
<div v-if="smartConfigEditing" class="border-top-divider">
|
||
<div class="grid-2col">
|
||
<label class="flex-center">
|
||
总本金: <input type="number" v-model.number="smartAlgoConfig.total_capital"
|
||
class="input-mini-wide">
|
||
</label>
|
||
<label class="flex-center">
|
||
止盈%: <input type="number" v-model.number="smartAlgoConfig.take_profit_pct" step="1"
|
||
class="input-mini">
|
||
</label>
|
||
<label class="flex-center">
|
||
止损%: <input type="number" v-model.number="smartAlgoConfig.stop_loss_pct" step="1"
|
||
class="input-mini">
|
||
</label>
|
||
<label class="flex-center">
|
||
仓位%: <input type="number" v-model.number="smartAlgoConfig.position_pct" step="1"
|
||
class="input-mini">
|
||
</label>
|
||
<label class="flex-center">
|
||
最长天数: <input type="number" v-model.number="smartAlgoConfig.max_hold_days"
|
||
class="input-mini">
|
||
</label>
|
||
<label class="flex-center">
|
||
部分止盈%: <input type="number" v-model.number="smartAlgoConfig.partial_exit_pct"
|
||
class="input-mini">
|
||
</label>
|
||
<label class="flex-center">
|
||
保本触发%: <input type="number" v-model.number="smartAlgoConfig.breakeven_at"
|
||
class="input-mini">
|
||
</label>
|
||
<label class="flex-center">
|
||
确认天数: <input type="number" v-model.number="smartAlgoConfig.sell_confirm_days"
|
||
class="input-mini">
|
||
</label>
|
||
</div>
|
||
<div style="margin-top:8px;display:flex;gap:8px;">
|
||
<button type="button" class="sim-btn auto" @click="saveSmartConfig" style="font-size:12px;">保存配置</button>
|
||
<button type="button" class="sim-btn" @click="smartConfigEditing = false; loadSmartAlgoConfig();" style="font-size:12px;">取消</button>
|
||
</div>
|
||
</div>
|
||
<div v-else style="margin-top:6px;">
|
||
<button type="button" class="sim-btn" @click="smartConfigEditing = true" style="font-size:11px;padding:2px 8px;">✏️ 自定义参数</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 自动执行时间说明 -->
|
||
<div style="margin-top:8px;font-size:11px;color:var(--text-dim);border-top:1px solid var(--border-glass);padding-top:6px;">
|
||
⏰ 每个交易日 <strong style="color:var(--accent-cyan);">{{ smartAlgoConfig?.buy_time || '09:35' }}</strong> 买入 /
|
||
<strong style="color:var(--accent-cyan);">{{ smartAlgoConfig?.sell_time || '13:40' }}</strong> 卖出 /
|
||
<strong>15:05</strong> 收盘更新 — 基于v7回测最优时点
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 今日交易 -->
|
||
<div class="sim-section collapsible-section">
|
||
<div class="sim-section-header" @click="todayTradesExpanded = !todayTradesExpanded" class="cursor-pointer">
|
||
<h4 class="section-title"><svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="var(--accent-cyan)" stroke-width="2"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg>今日交易 <span class="badge">{{ todayTrades.length }}</span></h4>
|
||
<span :class="['toggle-icon', { expanded: todayTradesExpanded }]" class="text-muted-2">▼</span>
|
||
</div>
|
||
<div v-show="todayTradesExpanded && todayTrades.length === 0" class="empty-tip">今日暂无交易</div>
|
||
<div v-show="todayTradesExpanded" class="sim-trades-list">
|
||
<div v-for="trade in todayTrades" :key="trade.id" :class="['sim-trade-item', trade.trade_type]" @click="loadFundamental(trade.stock_code, trade.stock_name)" class="cursor-pointer">
|
||
<div class="sim-trade-type" :class="trade.trade_type">
|
||
{{ trade.trade_type === 'buy' ? '买' : '卖' }}
|
||
</div>
|
||
<div class="sim-trade-content">
|
||
<div class="sim-trade-header">
|
||
<span class="sim-trade-stock">{{ trade.stock_code }} {{ trade.stock_name }}</span>
|
||
<span class="sim-trade-rate" v-if="trade.recommend_rate" title="信号强度评分">信号 {{ trade.recommend_rate }}%</span>
|
||
</div>
|
||
<div class="sim-trade-detail">
|
||
<span>¥{{ trade.price?.toFixed(2) }} × {{ trade.quantity }}股</span>
|
||
<span class="sim-trade-amount">{{ formatMoney(trade.price * trade.quantity) }}</span>
|
||
</div>
|
||
<div class="sim-trade-fee" v-if="trade.total_fee > 0" style="font-size:11px;color:var(--text-muted);">
|
||
手续费 ¥{{ trade.total_fee?.toFixed(2) }}
|
||
</div>
|
||
<div class="sim-trade-reason" v-if="trade.signal_reason">{{ trade.signal_reason }}</div>
|
||
<div class="sim-trade-date">{{ trade.trade_time?.substring(0,5) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模拟持仓列表(增强版:显示算法状态) -->
|
||
<div class="sim-section collapsible-section">
|
||
<div class="sim-section-header" @click="simPositionsExpanded = !simPositionsExpanded" class="cursor-pointer">
|
||
<h4 class="section-title"><svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="var(--teal)" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>当前持仓 <span class="badge">{{ simPositions.length }}</span></h4>
|
||
<span :class="['toggle-icon', { expanded: simPositionsExpanded }]" class="text-muted-2">▼</span>
|
||
</div>
|
||
<div v-show="simPositionsExpanded && simPositions.length === 0" class="empty-tip">暂无持仓</div>
|
||
<div v-show="simPositionsExpanded" class="sim-positions-list">
|
||
<div v-for="pos in simPositions" :key="pos.stock_code" class="sim-position-item" @click="loadFundamental(pos.stock_code, pos.stock_name)">
|
||
<div class="sim-pos-header">
|
||
<span class="sim-pos-code">{{ pos.stock_code }}</span>
|
||
<span class="sim-pos-name">{{ pos.stock_name }}</span>
|
||
<span class="sim-pos-qty">{{ pos.quantity }}股</span>
|
||
</div>
|
||
<div class="sim-pos-detail">
|
||
<span>成本 ¥{{ pos.avg_cost?.toFixed(2) }}</span>
|
||
<span>现价 ¥{{ pos.current_price?.toFixed(2) }}</span>
|
||
<span :class="(pos.current_price - pos.avg_cost) >= 0 ? 'profit' : 'loss'">
|
||
{{ (pos.current_price - pos.avg_cost) >= 0 ? '+' : '' }}{{ ((pos.current_price - pos.avg_cost) / pos.avg_cost * 100).toFixed(1) }}%
|
||
({{ (pos.current_price - pos.avg_cost) >= 0 ? '+' : '' }}¥{{ ((pos.current_price - pos.avg_cost) * pos.quantity).toFixed(0) }})
|
||
</span>
|
||
</div>
|
||
<!-- 算法状态标签 -->
|
||
<div v-if="getActiveRules(pos.stock_code).length > 0 || getPositionMeta(pos.stock_code)"
|
||
class="sim-pos-algo-status" style="font-size:11px;color:var(--text-muted);margin-top:2px;display:flex;gap:6px;flex-wrap:wrap;">
|
||
<span v-for="rule in getActiveRules(pos.stock_code)" :key="rule"
|
||
style="background:var(--grad-strategy-text);padding:1px 6px;border-radius:8px;font-size:10px;">{{ rule }}</span>
|
||
<span v-if="getPositionMeta(pos.stock_code)?.days_held" style="color:var(--text-dim);">
|
||
持{{ getPositionMeta(pos.stock_code).days_held }}天
|
||
</span>
|
||
<span v-if="getPositionMeta(pos.stock_code)?.consecutive_up_days > 0" style="color:var(--teal);">
|
||
连涨{{ getPositionMeta(pos.stock_code).consecutive_up_days }}天
|
||
</span>
|
||
<span v-if="getPositionMeta(pos.stock_code)?.consecutive_sell_signals > 0" style="color:var(--hold);">
|
||
卖信号{{ getPositionMeta(pos.stock_code).consecutive_sell_signals }}天
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 信号日志(可折叠) -->
|
||
<div class="sim-section collapsible-section">
|
||
<div class="sim-section-header" @click="smartSignalExpanded = !smartSignalExpanded" class="cursor-pointer">
|
||
<h4 class="section-title"><svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="var(--hold)" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>信号日志 <span class="badge">{{ smartSignals.length }}</span></h4>
|
||
<span :class="['toggle-icon', { expanded: smartSignalExpanded }]" class="text-muted-2">▼</span>
|
||
</div>
|
||
<div v-show="smartSignalExpanded">
|
||
<div v-if="smartSignals.length === 0" class="empty-tip">暂无信号记录</div>
|
||
<div v-else class="max-height-scroll">
|
||
<div v-for="sig in smartSignals" :key="sig.id"
|
||
style="padding:6px 0;border-bottom:1px solid var(--grad-strategy-text);font-size:12px;display:flex;gap:8px;align-items:flex-start;">
|
||
<span :style="{
|
||
color: sig.action === 'buy' ? 'var(--up)' :
|
||
sig.action === 'sell' ? 'var(--teal)' :
|
||
sig.action === 'partial_sell' ? 'var(--hold)' : 'var(--text-muted)',
|
||
fontWeight: 600, minWidth: '48px'
|
||
}">
|
||
{{ sig.action === 'buy' ? '买入' : sig.action === 'sell' ? '卖出' :
|
||
sig.action === 'partial_sell' ? '减仓' : sig.action === 'hold' ? '持有' : sig.action }}
|
||
</span>
|
||
<span style="color:var(--text-secondary);flex:1;">
|
||
{{ sig.stock_code }}
|
||
<span v-if="sig.algo_rule" style="color:var(--accent-cyan);font-size:11px;">[{{ sig.algo_rule }}]</span>
|
||
{{ sig.reason }}
|
||
<span v-if="sig.profit_pct" :style="{color: sig.profit_pct >= 0 ? 'var(--teal)' : 'var(--up)'}">
|
||
({{ sig.profit_pct >= 0 ? '+' : '' }}{{ sig.profit_pct?.toFixed(1) }}%)
|
||
</span>
|
||
</span>
|
||
<span style="color:var(--text-dim);font-size:11px;white-space:nowrap;">{{ sig.signal_date }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模拟交易记录 -->
|
||
<div class="sim-section collapsible-section">
|
||
<div class="sim-section-header" @click="simTradesExpanded = !simTradesExpanded" class="cursor-pointer">
|
||
<h4 class="section-title"><svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="var(--purple-light)" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>交易记录 <span class="badge">{{ simTrades.length }}</span></h4>
|
||
<span :class="['toggle-icon', { expanded: simTradesExpanded }]" class="text-muted-2">▼</span>
|
||
</div>
|
||
<div v-show="simTradesExpanded && simTrades.length === 0" class="empty-tip">暂无交易记录</div>
|
||
<div v-show="simTradesExpanded" class="sim-trades-list">
|
||
<div v-for="trade in simTrades" :key="trade.id" :class="['sim-trade-item', trade.trade_type]" @click="loadFundamental(trade.stock_code, trade.stock_name)" class="cursor-pointer">
|
||
<div class="sim-trade-type" :class="trade.trade_type">
|
||
{{ trade.trade_type === 'buy' ? '买' : '卖' }}
|
||
</div>
|
||
<div class="sim-trade-content">
|
||
<div class="sim-trade-header">
|
||
<span class="sim-trade-stock">{{ trade.stock_code }} {{ trade.stock_name }}</span>
|
||
<span class="sim-trade-rate" v-if="trade.recommend_rate" title="信号强度评分">信号 {{ trade.recommend_rate }}%</span>
|
||
</div>
|
||
<div class="sim-trade-detail">
|
||
<span>¥{{ trade.price?.toFixed(2) }} × {{ trade.quantity }}股</span>
|
||
<span class="sim-trade-amount">{{ formatMoney(trade.price * trade.quantity) }}</span>
|
||
</div>
|
||
<div class="sim-trade-fee" v-if="trade.total_fee > 0" style="font-size:11px;color:var(--text-muted);">
|
||
手续费 ¥{{ trade.total_fee?.toFixed(2) }}
|
||
<span style="color:var(--text-dim);">(佣金{{ trade.commission?.toFixed(2) }}{{ trade.stamp_tax > 0 ? ' + 印花税' + trade.stamp_tax?.toFixed(2) : '' }})</span>
|
||
</div>
|
||
<div class="sim-trade-reason" v-if="trade.signal_reason">{{ trade.signal_reason }}</div>
|
||
<div class="sim-trade-date">{{ trade.trade_date }} {{ trade.trade_time?.substring(0,5) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- 旧模拟交易占位结束 -->
|
||
|
||
</div><!-- 分析页面结束 -->
|
||
|
||
<!-- 模型页面 -->
|
||
<div v-show="activeTab === 'model'" class="model-page">
|
||
<!-- 模型页子导航 -->
|
||
<div class="sub-tabs">
|
||
<button type="button" :class="['sub-tab', { active: modelSubTab === 'system' }]" @click="modelSubTab = 'system'">
|
||
信号体系
|
||
</button>
|
||
<button type="button" :class="['sub-tab', { active: modelSubTab === 'sim' }]" @click="modelSubTab = 'sim'; loadSimData()">
|
||
模拟交易
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 信号体系子页面 -->
|
||
<div v-show="modelSubTab === 'system'">
|
||
<!-- 信号胜率排行 -->
|
||
<div class="signal-system-section">
|
||
<h4 class="signal-system-title">交易信号胜率排行</h4>
|
||
<div class="signal-rank-list">
|
||
<div class="signal-rank-item rank-1">
|
||
<span class="rank-badge">1</span>
|
||
<span class="rank-name">★主升浪</span>
|
||
<span class="rank-rate">85%</span>
|
||
<span class="rank-desc">趋势走好,进入加速拉升阶段</span>
|
||
<span class="rank-param">MACD零上金叉</span>
|
||
</div>
|
||
<div class="signal-rank-item rank-2">
|
||
<span class="rank-badge">2</span>
|
||
<span class="rank-name">日线底背离</span>
|
||
<span class="rank-rate">80%</span>
|
||
<span class="rank-desc">真正跌透,迎来大级别反转</span>
|
||
<span class="rank-param">20日新低版</span>
|
||
</div>
|
||
<div class="signal-rank-item rank-3">
|
||
<span class="rank-badge">3</span>
|
||
<span class="rank-name">龙抬头</span>
|
||
<span class="rank-rate">75%</span>
|
||
<span class="rank-desc">短线起爆点,反弹稳定性强</span>
|
||
<span class="rank-param">SKDJ超跌+不漂移</span>
|
||
</div>
|
||
<div class="signal-rank-item rank-4">
|
||
<span class="rank-badge">4</span>
|
||
<span class="rank-name">真龙</span>
|
||
<span class="rank-rate">70%</span>
|
||
<span class="rank-desc">中期趋势刚刚启动</span>
|
||
<span class="rank-param">趋势启动</span>
|
||
</div>
|
||
<div class="signal-rank-item rank-5">
|
||
<span class="rank-badge">5</span>
|
||
<span class="rank-name">短底背离</span>
|
||
<span class="rank-rate">65%</span>
|
||
<span class="rank-desc">小级别反弹,灵敏度高但力度偏弱</span>
|
||
<span class="rank-param">10/30版</span>
|
||
</div>
|
||
<div class="signal-rank-item rank-6">
|
||
<span class="rank-badge">6</span>
|
||
<span class="rank-name">老鼠仓</span>
|
||
<span class="rank-rate">60%</span>
|
||
<span class="rank-desc">主力偷偷吸筹,上涨不具备即时性</span>
|
||
<span class="rank-param">异常量价</span>
|
||
</div>
|
||
<div class="signal-rank-item rank-7">
|
||
<span class="rank-badge">7</span>
|
||
<span class="rank-name">反弹</span>
|
||
<span class="rank-rate">55%</span>
|
||
<span class="rank-desc">普通均线金叉,震荡市适用</span>
|
||
<span class="rank-param">EMA3上穿EMA21</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 标准牛股启动信号顺序 -->
|
||
<div class="signal-system-section bull-flow-section">
|
||
<h4 class="signal-system-title">标准牛股启动顺序</h4>
|
||
<div class="bull-flow">
|
||
<div class="bull-step"><span class="bull-num">1</span><span class="bull-label">底背离/短底背离</span><span class="bull-hint">跌到底部,停止下跌</span></div>
|
||
<div class="bull-arrow">→</div>
|
||
<div class="bull-step"><span class="bull-num">2</span><span class="bull-label">龙抬头</span><span class="bull-hint">资金进场,短线起爆</span></div>
|
||
<div class="bull-arrow">→</div>
|
||
<div class="bull-step"><span class="bull-num">3</span><span class="bull-label">真龙</span><span class="bull-hint">趋势正式确立</span></div>
|
||
<div class="bull-arrow">→</div>
|
||
<div class="bull-step"><span class="bull-num">4</span><span class="bull-label">★主升浪</span><span class="bull-hint">加速段,利润最快</span></div>
|
||
<div class="bull-arrow">→</div>
|
||
<div class="bull-step"><span class="bull-num">5</span><span class="bull-label">反弹</span><span class="bull-hint">回调后补涨信号</span></div>
|
||
</div>
|
||
<div class="bull-note">老鼠仓可在底部任意位置提前出现,属于提前埋伏类信号</div>
|
||
</div>
|
||
|
||
<!-- 体系最强战法 -->
|
||
<div class="signal-system-section highlight-section">
|
||
<h4 class="signal-system-title">体系最强战法</h4>
|
||
<div class="strategy-steps">
|
||
<div class="strategy-step step-watch">
|
||
<span class="step-icon">👀</span>
|
||
<span class="step-action">关注</span>
|
||
<span class="step-rule">日线底背离出现 → 纳入关注范围</span>
|
||
</div>
|
||
<div class="strategy-step step-buy">
|
||
<span class="step-icon">💰</span>
|
||
<span class="step-action">买入</span>
|
||
<span class="step-rule">龙抬头出现 → 执行买入操作</span>
|
||
</div>
|
||
<div class="strategy-step step-add">
|
||
<span class="step-icon">📈</span>
|
||
<span class="step-action">加仓</span>
|
||
<span class="step-rule">真龙 / ★主升浪出现 → 持有并加仓</span>
|
||
</div>
|
||
<div class="strategy-step step-hold">
|
||
<span class="step-icon">🔒</span>
|
||
<span class="step-action">持有</span>
|
||
<span class="step-rule">不见主升浪 → 坚决不出场</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 核心信号一句话总结 -->
|
||
<div class="signal-system-section">
|
||
<h4 class="signal-system-title">核心信号一句话总结</h4>
|
||
<div class="signal-summary-list">
|
||
<div class="signal-summary-item"><span class="summary-tag core">★主升浪</span>最稳、最猛(利润核心阶段)</div>
|
||
<div class="signal-summary-item"><span class="summary-tag core">日线底背离</span>最安全抄底(反转前置信号)</div>
|
||
<div class="signal-summary-item"><span class="summary-tag core">龙抬头</span>最佳入场点(实操核心买点)</div>
|
||
<div class="signal-summary-item"><span class="summary-tag normal">真龙</span>趋势确认(中期行情定局信号)</div>
|
||
<div class="signal-summary-item"><span class="summary-tag aux">短底背离 / 反弹</span>仅作辅助参考,不单独作为核心决策依据</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- 信号体系子页面结束 -->
|
||
|
||
<!-- 模拟交易子页面 -->
|
||
<div v-show="modelSubTab === 'sim'" class="sim-trade-page">
|
||
|
||
<!-- 模拟交易统计卡片 -->
|
||
<div class="sim-stats-card">
|
||
<div class="sim-stats-header">
|
||
<h4>智能交易 <span v-if="smartAlgoConfig" style="font-size:12px;color:var(--text-muted);">{{ smartAlgoConfig.algo_name }}</span></h4>
|
||
<div class="sim-actions">
|
||
<button type="button" class="sim-btn auto" @click="executeAutoTrade" :disabled="simAutoTrading">
|
||
{{ simAutoTrading ? '执行中...' : '执行' }}
|
||
</button>
|
||
<button type="button" class="sim-btn reset" @click="confirmResetSim">重置</button>
|
||
</div>
|
||
</div>
|
||
<div class="sim-stats-grid">
|
||
<div class="sim-stat-item">
|
||
<div class="sim-stat-value">{{ formatMoney(smartAlgoConfig?.total_capital || simStats.initial_capital || 200000) }}</div>
|
||
<div class="sim-stat-label">总本金</div>
|
||
</div>
|
||
<div class="sim-stat-item">
|
||
<div class="sim-stat-value">{{ formatMoney(simStats.total_market_value || 0) }}</div>
|
||
<div class="sim-stat-label">持仓市值</div>
|
||
</div>
|
||
<div class="sim-stat-item">
|
||
<div class="sim-stat-value">{{ formatMoney(simStats.cash || (smartAlgoConfig?.total_capital || 200000)) }}</div>
|
||
<div class="sim-stat-label">可用资金</div>
|
||
</div>
|
||
<div class="sim-stat-item" :class="(simStats.total_profit || 0) >= 0 ? 'profit' : 'loss'">
|
||
<div class="sim-stat-value">{{ (simStats.total_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.total_profit || 0) }}</div>
|
||
<div class="sim-stat-label">总盈亏</div>
|
||
</div>
|
||
<div class="sim-stat-item" :class="(simStats.profit_rate || 0) >= 0 ? 'profit' : 'loss'">
|
||
<div class="sim-stat-value">{{ (simStats.profit_rate || 0) >= 0 ? '+' : '' }}{{ (simStats.profit_rate || 0).toFixed(2) }}%</div>
|
||
<div class="sim-stat-label">收益率</div>
|
||
</div>
|
||
<div class="sim-stat-item">
|
||
<div class="sim-stat-value">{{ simStats.total_trades || 0 }}</div>
|
||
<div class="sim-stat-label">交易次数</div>
|
||
</div>
|
||
</div>
|
||
<div class="sim-stats-detail">
|
||
<span :class="(simStats.unrealized_profit || 0) >= 0 ? 'profit' : 'loss'">
|
||
浮动 {{ (simStats.unrealized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.unrealized_profit || 0) }}
|
||
</span>
|
||
<span :class="(simStats.realized_profit || 0) >= 0 ? 'profit' : 'loss'">
|
||
已实现 {{ (simStats.realized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.realized_profit || 0) }}
|
||
</span>
|
||
<button type="button" class="sim-btn refresh" @click="updateSimPrices" :disabled="simPositions.length === 0 || simPriceRefreshing" v-if="simPositions.length > 0" style="margin-left:auto;font-size:11px;padding:2px 8px;">
|
||
{{ simPriceRefreshing ? '刷新中...' : '刷新现价' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模拟持仓和交易历史直接复用现有逻辑 -->
|
||
<div class="sim-section collapsible-section" v-if="simPositions.length > 0">
|
||
<div class="sim-section-header" class="cursor-pointer" @click="simPositionsExpanded = !simPositionsExpanded">
|
||
<h4 class="section-title">持仓 <span class="badge">{{ simPositions.length }}</span></h4>
|
||
<span class="expand-arrow" :class="{ expanded: simPositionsExpanded }">▸</span>
|
||
</div>
|
||
<div v-show="simPositionsExpanded" class="sim-positions-list">
|
||
<div v-for="pos in simPositions" :key="pos.stock_code" class="sim-position-item" @click="loadFundamental(pos.stock_code, pos.stock_name)" class="cursor-pointer">
|
||
<div class="sim-pos-header">
|
||
<span class="sim-pos-stock">{{ pos.stock_code }} {{ pos.stock_name }}</span>
|
||
<span class="sim-pos-profit" :class="(pos.unrealized_profit || 0) >= 0 ? 'profit' : 'loss'">
|
||
{{ (pos.unrealized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(pos.unrealized_profit || 0) }}
|
||
({{ (pos.profit_rate || 0) >= 0 ? '+' : '' }}{{ (pos.profit_rate || 0).toFixed(2) }}%)
|
||
</span>
|
||
</div>
|
||
<div class="sim-pos-detail">
|
||
<span>{{ pos.quantity }}股 @ ¥{{ (pos.avg_cost || 0).toFixed(2) }}</span>
|
||
<span v-if="pos.current_price">现价 ¥{{ pos.current_price.toFixed(2) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- 模拟交易子页面结束 -->
|
||
|
||
</div><!-- 模型页面结束 -->
|
||
|
||
<!-- 交易记录页面 -->
|
||
<div v-show="activeTab === 'trades'">
|
||
|
||
<!-- 交易统计卡片 -->
|
||
<div class="trade-stats-card" v-if="tradeStats && tradeStats.total_trades > 0">
|
||
<h4>我的交易统计</h4>
|
||
<div class="stats-grid">
|
||
<div class="stat-item">
|
||
<div class="stat-value">{{ tradeStats.total_trades }}</div>
|
||
<div class="stat-label">交易次数</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value">{{ formatMoney(tradeStats.total_market_value) }}</div>
|
||
<div class="stat-label">持仓市值</div>
|
||
</div>
|
||
<div class="stat-item" :class="tradeStats.total_profit >= 0 ? 'profit' : 'loss'">
|
||
<div class="stat-value">{{ tradeStats.total_profit >= 0 ? '+' : '' }}{{ formatMoney(tradeStats.total_profit) }}</div>
|
||
<div class="stat-label">总盈亏</div>
|
||
</div>
|
||
</div>
|
||
<div class="stats-detail">
|
||
<span :class="tradeStats.realized_profit >= 0 ? 'profit' : 'loss'">
|
||
已清仓 {{ tradeStats.realized_profit >= 0 ? '+' : '' }}{{ formatMoney(tradeStats.realized_profit || 0) }}
|
||
</span>
|
||
<span :class="tradeStats.unrealized_profit >= 0 ? 'profit' : 'loss'">
|
||
浮动 {{ tradeStats.unrealized_profit >= 0 ? '+' : '' }}{{ formatMoney(tradeStats.unrealized_profit) }}
|
||
</span>
|
||
</div>
|
||
<!-- 可用资金 -->
|
||
<div class="available-cash-row">
|
||
<span class="cash-label">可用资金</span>
|
||
<template v-if="!editingCash">
|
||
<span class="cash-value">{{ formatMoney(availableCash) }}</span>
|
||
<button type="button" class="cash-edit-btn" @click="startEditCash" title="编辑可用资金">✏️</button>
|
||
</template>
|
||
<template v-else>
|
||
<input type="number" ref="cashInput" v-model="cashInputValue" class="cash-input"
|
||
@keyup.enter="saveCash" @keyup.esc="cancelEditCash" placeholder="输入金额">
|
||
<button type="button" class="cash-save-btn" @click="saveCash">✓</button>
|
||
<button type="button" class="cash-cancel-btn" @click="cancelEditCash">✕</button>
|
||
</template>
|
||
</div>
|
||
<!-- 总资产 = 持仓市值 + 可用资金 -->
|
||
<div class="total-assets-row">
|
||
<span class="assets-label">总资产</span>
|
||
<span class="assets-value">{{ formatMoney((tradeStats.total_market_value || 0) + availableCash) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 止损预警区(可折叠) -->
|
||
<div v-if="stopLossAlerts && stopLossAlerts.length > 0" class="stoploss-alerts-card stoploss-collapsible">
|
||
<div class="stoploss-alerts-header" role="button" tabindex="0" :aria-expanded="stopLossAlertsExpanded" @click="stopLossAlertsExpanded = !stopLossAlertsExpanded" @keyup.enter="stopLossAlertsExpanded = !stopLossAlertsExpanded">
|
||
<h4>⚠️ 止损预警 <span class="stoploss-badge">{{ stopLossAlerts.length }}</span></h4>
|
||
<span :class="['toggle-icon', { expanded: stopLossAlertsExpanded }]">▼</span>
|
||
</div>
|
||
<div v-show="stopLossAlertsExpanded" class="stoploss-alerts-body">
|
||
<div v-for="alert in stopLossAlerts" :key="alert.code" class="stoploss-alert-item">
|
||
<div class="stoploss-alert-header">
|
||
<span class="alert-stock">{{ alert.code }} {{ alert.name }}</span>
|
||
<span class="alert-loss">{{ alert.profit_percent.toFixed(1) }}%</span>
|
||
</div>
|
||
<div class="stoploss-alert-detail">
|
||
<span>成本 ¥{{ alert.avg_cost }}</span>
|
||
<span>现价 ¥{{ alert.current_price }}</span>
|
||
<span v-if="alert.stop_loss_price">止损 ¥{{ alert.stop_loss_price }}</span>
|
||
</div>
|
||
<div class="stoploss-alert-message">{{ alert.message }}</div>
|
||
<button type="button" class="stoploss-sell-btn" @click="quickSellFromStopLoss(alert)">立即卖出</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮区 -->
|
||
<div class="trade-actions-bar">
|
||
<button type="button" class="add-trade-btn" @click="openTradeForm" :disabled="allStocksForTrade.length === 0">
|
||
{{ allStocksForTrade.length === 0 ? '请先分析股票' : '记录交易' }}
|
||
</button>
|
||
<button type="button" class="refresh-price-btn" @click="refreshHoldingPrices" :disabled="priceRefreshing">
|
||
{{ priceRefreshing ? '刷新中...' : '刷新价格' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 交易记录表单弹窗 -->
|
||
<div v-if="showTradeForm" class="modal-overlay" @click.self="showTradeForm = false" role="dialog" aria-modal="true" aria-label="记录交易">
|
||
<div class="modal-content">
|
||
<h3>记录交易</h3>
|
||
<form @submit.prevent="submitTrade">
|
||
<div class="form-row">
|
||
<label>股票 (关注/持有)</label>
|
||
<select v-model="selectedStockFromHistory" @change="onStockSelected" required>
|
||
<option value="">请选择股票</option>
|
||
<option v-for="item in allStocksForTrade" :key="item.code" :value="item.code">
|
||
{{ item.isHolding ? '📦 ' : '' }}{{ item.code }} - {{ item.name }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row" v-if="selectedStockFromHistory">
|
||
<label>交易信号(自动获取)</label>
|
||
<div class="signal-display" :class="selectedStockSignal.type">
|
||
{{ selectedStockSignal.icon }} {{ selectedStockSignal.title }}: {{ selectedStockSignal.description }}
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>交易类型</label>
|
||
<select v-model="newTrade.trade_type" required>
|
||
<option value="buy">买入</option>
|
||
<option value="sell">卖出</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>交易价格</label>
|
||
<input v-model="newTrade.price" type="number" step="0.0001" required>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>交易数量</label>
|
||
<input v-model="newTrade.quantity" type="number" required>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>交易日期</label>
|
||
<input v-model="newTrade.trade_date" type="date" required>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>交易结果</label>
|
||
<select v-model="newTrade.result">
|
||
<option value="pending">待定</option>
|
||
<option value="profit">盈利</option>
|
||
<option value="loss">亏损</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row" v-if="newTrade.result !== 'pending'">
|
||
<label>盈亏金额</label>
|
||
<input v-model="newTrade.profit_amount" type="number" step="0.0001">
|
||
</div>
|
||
<div class="form-row" v-if="newTrade.trade_type === 'buy'">
|
||
<label>止损价(跌破此价提醒卖出)</label>
|
||
<input v-model="newTrade.stop_loss_price" type="number" step="0.01" placeholder="建议设为成本价的95%">
|
||
</div>
|
||
<div class="form-row">
|
||
<label>备注</label>
|
||
<textarea v-model="newTrade.notes" rows="2"></textarea>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" @click="showTradeForm = false">取消</button>
|
||
<button type="submit">保存</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 持有/清仓 子Tab -->
|
||
<div class="trade-sub-tabs">
|
||
<button type="button" :class="['trade-sub-tab', { active: tradeViewType === 'holding' }]" @click="tradeViewType = 'holding'">
|
||
持有 <span class="tab-count">{{ holdingGroupCount }}</span>
|
||
</button>
|
||
<button type="button" :class="['trade-sub-tab', { active: tradeViewType === 'cleared' }]" @click="tradeViewType = 'cleared'">
|
||
清仓 <span class="tab-count">{{ clearedGroupCount }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 交易记录列表(按股票分组、时间倒序) -->
|
||
<div class="trades-list">
|
||
<div v-if="filteredGroupedTrades.length === 0" class="empty-trades">
|
||
{{ tradeViewType === 'holding' ? '暂无持有股票' : '暂无清仓记录' }}
|
||
</div>
|
||
|
||
<!-- 按股票分组显示 -->
|
||
<div v-for="group in filteredGroupedTrades" :key="group.code" class="trade-group">
|
||
<!-- 股票分组标题 -->
|
||
<div class="trade-group-header">
|
||
<button type="button" class="toggle-btn" @click.stop="toggleTradeGroup(group.code)">
|
||
{{ expandedTradeGroups[group.code] ? '▼' : '▶' }}
|
||
</button>
|
||
<div class="trade-group-content" role="button" tabindex="0" @click="loadFundamental(group.code, group.name)" @keyup.enter="loadFundamental(group.code, group.name)">
|
||
<!-- 第一行:股票信息 -->
|
||
<div class="trade-group-info">
|
||
<span class="trade-group-code">{{ group.code }}</span>
|
||
<span class="trade-group-name">{{ group.name }}</span>
|
||
<span class="trade-group-count">{{ group.trades.length }}笔</span>
|
||
<span v-if="holdingPositions[group.code]?.quantity > 0" class="holding-badge">{{ holdingPositions[group.code].quantity }}股</span>
|
||
</div>
|
||
<!-- 第二行:持有中显示现价、总市值和浮动盈亏 -->
|
||
<div class="trade-summary-stats" v-if="holdingPositions[group.code]?.quantity > 0 && holdingPositions[group.code].currentPrice">
|
||
<div class="summary-stat-item">
|
||
<div :class="['summary-stat-value', holdingPositions[group.code].priceDirection === 'up' ? 'price-up' : (holdingPositions[group.code].priceDirection === 'down' ? 'price-down' : '')]">{{ holdingPositions[group.code].currentPrice.toFixed(2) }}</div>
|
||
<div class="summary-stat-label">现价</div>
|
||
</div>
|
||
<div class="summary-stat-item">
|
||
<div class="summary-stat-value">{{ (holdingPositions[group.code].totalCost / holdingPositions[group.code].quantity).toFixed(2) }}</div>
|
||
<div class="summary-stat-label">平衡价</div>
|
||
</div>
|
||
<div class="summary-stat-item">
|
||
<div class="summary-stat-value">{{ formatMoney(holdingPositions[group.code].currentPrice * holdingPositions[group.code].quantity) }}</div>
|
||
<div class="summary-stat-label">市值</div>
|
||
</div>
|
||
<div class="summary-stat-item" :class="(holdingPositions[group.code].currentPrice * holdingPositions[group.code].quantity - holdingPositions[group.code].totalCost) >= 0 ? 'profit' : 'loss'">
|
||
<div class="summary-stat-value">{{ (holdingPositions[group.code].currentPrice * holdingPositions[group.code].quantity - holdingPositions[group.code].totalCost) >= 0 ? '+' : '' }}{{ formatMoney(holdingPositions[group.code].currentPrice * holdingPositions[group.code].quantity - holdingPositions[group.code].totalCost) }}</div>
|
||
<div class="summary-stat-label">盈亏</div>
|
||
</div>
|
||
</div>
|
||
<!-- 已清仓:显示已实现盈亏 -->
|
||
<div class="trade-group-summary" v-else-if="group.realizedProfit !== 0">
|
||
<span class="holding-badge cleared">已清仓</span>
|
||
<span :class="['holding-profit', group.realizedProfit >= 0 ? 'profit' : 'loss']">
|
||
已实现 {{ group.realizedProfit >= 0 ? '+' : '' }}{{ formatMoney(group.realizedProfit) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 该股票的交易记录(按时间倒序) -->
|
||
<div v-for="trade in group.trades" :key="trade.id" v-show="expandedTradeGroups[group.code]" :class="['trade-item', trade.trade_type]">
|
||
<div class="trade-type-indicator" :class="trade.trade_type">
|
||
{{ trade.trade_type === 'buy' ? '买' : '卖' }}
|
||
</div>
|
||
<div class="trade-content">
|
||
<div class="trade-header" v-if="trade.result !== 'pending'">
|
||
<span :class="['trade-result', trade.result]">
|
||
{{ trade.result === 'profit' ? '盈利' : '亏损' }}
|
||
</span>
|
||
</div>
|
||
<div class="trade-amount-row">
|
||
<div class="trade-amount">
|
||
¥{{ formatMoney(trade.price * trade.quantity) }}
|
||
</div>
|
||
<div v-if="holdingPositions[trade.stock_code]?.currentPrice && trade.trade_type === 'buy'"
|
||
:class="['trade-profit', (holdingPositions[trade.stock_code].currentPrice - trade.price) * trade.quantity >= 0 ? 'profit' : 'loss']">
|
||
{{ (holdingPositions[trade.stock_code].currentPrice - trade.price) * trade.quantity >= 0 ? '+' : '' }}{{ formatMoney((holdingPositions[trade.stock_code].currentPrice - trade.price) * trade.quantity) }}
|
||
</div>
|
||
</div>
|
||
<div class="trade-details">
|
||
<span>{{ trade.price }}元 × {{ trade.quantity }}股</span>
|
||
<span v-if="holdingPositions[trade.stock_code]?.currentPrice && trade.trade_type === 'buy'"
|
||
:class="[holdingPositions[trade.stock_code].priceDirection === 'up' ? 'price-up' : (holdingPositions[trade.stock_code].priceDirection === 'down' ? 'price-down' : '')]">
|
||
现价 {{ holdingPositions[trade.stock_code].currentPrice.toFixed(2) }}元
|
||
</span>
|
||
</div>
|
||
<div class="trade-reason" v-if="trade.reason">
|
||
信号: {{ trade.reason }}
|
||
</div>
|
||
<div class="trade-notes" v-if="trade.notes">
|
||
备注: {{ trade.notes }}
|
||
</div>
|
||
<div class="trade-date">{{ trade.trade_date }}</div>
|
||
</div>
|
||
<div class="trade-actions-vertical">
|
||
<button type="button" @click="editTrade(trade)">编辑</button>
|
||
<button type="button" @click="deleteTrade(trade.id)">删除</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- 交易记录页面结束 -->
|
||
</template><!-- 已登录内容结束 -->
|
||
|
||
<!-- 修改密码弹窗(独立于条件链) -->
|
||
<div v-if="showPasswordModal" class="modal-overlay" @click.self="showPasswordModal = false">
|
||
<div class="login-modal">
|
||
<div class="login-header">
|
||
<h2>修改密码</h2>
|
||
</div>
|
||
<div class="login-body">
|
||
<div class="login-field">
|
||
<label>当前密码</label>
|
||
<input v-model="passwordForm.oldPassword" type="password" placeholder="请输入当前密码">
|
||
</div>
|
||
<div class="login-field">
|
||
<label>新密码</label>
|
||
<input v-model="passwordForm.newPassword" type="password" placeholder="请输入新密码(至少6位)">
|
||
</div>
|
||
<div class="login-field">
|
||
<label>确认新密码</label>
|
||
<input v-model="passwordForm.confirmPassword" type="password" placeholder="请再次输入新密码" @keyup.enter="handleChangePassword">
|
||
</div>
|
||
<div v-if="passwordError" class="login-error">{{ passwordError }}</div>
|
||
<div v-if="passwordSuccess" class="login-success">{{ passwordSuccess }}</div>
|
||
<div class="password-buttons">
|
||
<button type="button" class="btn-cancel" @click="showPasswordModal = false">关闭</button>
|
||
<button type="button" class="login-submit" @click="handleChangePassword" :disabled="passwordLoading">
|
||
{{ passwordLoading ? '处理中...' : '确认修改' }}
|
||
</button>
|
||
</div>
|
||
<a v-if="currentUser && currentUser.is_admin" href="/admin" target="_blank" class="admin-link-btn">进入管理后台</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
{% endraw %}
|
||
<script src="/static/js/app.js?v=20260722v1"></script>
|
||
</body>
|
||
</html>
|