feat: UI/UX全面优化 — CSS变量体系/a11y无障碍/Admin角色化/骨架屏/打印样式

This commit is contained in:
selfrelease
2026-07-18 07:57:23 +08:00
parent 2b5a32ca1e
commit 4b42eb80fd
15 changed files with 1519 additions and 442 deletions
+79 -32
View File
@@ -21,19 +21,19 @@
<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=20260318v2">
<link rel="stylesheet" href="/static/css/base.css?v=20260718v1">
<link rel="stylesheet" href="/static/css/components.css?v=20260318v2">
<link rel="stylesheet" href="/static/css/auth.css?v=20260318v1">
<link rel="stylesheet" href="/static/css/pages.css?v=20260511v3">
<link rel="stylesheet" href="/static/css/scan.css?v=20260318v2">
<link rel="stylesheet" href="/static/css/responsive.css?v=20260318v2">
<link rel="stylesheet" href="/static/css/pages.css?v=20260718v1">
<link rel="stylesheet" href="/static/css/scan.css?v=20260718v1">
<link rel="stylesheet" href="/static/css/responsive.css?v=20260718v1">
</head>
<body>
{% raw %}
<div id="app" v-cloak>
<!-- 全局 Toast 提示 -->
<transition name="toast-fade">
<div v-if="toastVisible" :class="['toast-container', toastType]">
<div v-if="toastVisible" :class="['toast-container', toastType]" role="alert" aria-live="assertive">
<span class="toast-icon">
{{ toastType === 'success' ? '✓' : toastType === 'error' ? '✕' : toastType === 'warning' ? '⚠' : '' }}
</span>
@@ -43,7 +43,7 @@
<!-- 确认对话框 -->
<transition name="modal-fade">
<div v-if="confirmVisible" class="confirm-overlay" @click.self="handleConfirmCancel">
<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">
@@ -113,7 +113,7 @@
<!-- 登录弹窗 -->
<transition name="modal-fade">
<div v-if="showLoginModal" class="modal-overlay" @click.self="showLoginModal = false">
<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 :class="['login-tab', { active: !isRegister }]" @click="isRegister = false; loginError = ''">登录</button>
@@ -199,11 +199,11 @@
</div>
<!-- 顶部导航 -->
<div class="nav-tabs nav-tabs-compact">
<button :class="['nav-tab', { active: activeTab === 'alerts' }]" @click="activeTab = 'alerts'">提醒</button>
<button :class="['nav-tab', { active: activeTab === 'trades' }]" @click="activeTab = 'trades'; loadTrades()">交易</button>
<button :class="['nav-tab', { active: activeTab === 'analysis' }]" @click="activeTab = 'analysis'; if (analysisSubTab === 'scan') fetchFullScanResults(1)">分析</button>
<button :class="['nav-tab', { active: activeTab === 'model' }]" @click="activeTab = 'model'">模型</button>
<div class="nav-tabs nav-tabs-compact" role="tablist">
<button :class="['nav-tab', { active: activeTab === 'alerts' }]" role="tab" :aria-selected="activeTab === 'alerts'" @click="activeTab = 'alerts'">提醒</button>
<button :class="['nav-tab', { active: activeTab === 'trades' }]" role="tab" :aria-selected="activeTab === 'trades'" @click="activeTab = 'trades'; loadTrades()">交易</button>
<button :class="['nav-tab', { active: activeTab === 'analysis' }]" role="tab" :aria-selected="activeTab === 'analysis'" @click="activeTab = 'analysis'; if (analysisSubTab === 'scan') fetchFullScanResults(1)">分析</button>
<button :class="['nav-tab', { active: activeTab === 'model' }]" role="tab" :aria-selected="activeTab === 'model'" @click="activeTab = 'model'">模型</button>
</div>
<!-- 提醒页面 -->
@@ -262,7 +262,7 @@
<!-- 卖出信号(持有的股票出现卖出信号需要优先关注) -->
<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" @click="loadFundamental(alert.code, alert.name)">
<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>
@@ -294,7 +294,7 @@
<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'" @click="loadFundamental(alert.code, alert.name)">
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>
@@ -326,7 +326,7 @@
<!-- 观望(持有的股票无明确信号) -->
<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" @click="loadFundamental(alert.code, alert.name)">
<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>
@@ -362,7 +362,7 @@
<!-- 买入提醒列表 -->
<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" @click="loadFundamental(alert.code, alert.name)">
<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>
@@ -395,7 +395,7 @@
<!-- 卖出提醒列表 -->
<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" @click="loadFundamental(alert.code, alert.name)">
<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>
@@ -427,7 +427,7 @@
<!-- 观望列表 -->
<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" @click="loadFundamental(alert.code, alert.name)">
<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>
@@ -847,7 +847,7 @@
<!-- 全量扫描结果 -->
<template v-if="fullScanStatus && (fullScanResults.length > 0 || fullScanFilterRecommend !== 'all')">
<div class="scan-summary-section">
<div class="scan-summary-header" @click="scanSummaryCollapsed = !scanSummaryCollapsed">
<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 class="section-close inline" @click.stop="fullScanStatus = null; fullScanResults = []" title="关闭"></button>
@@ -899,6 +899,11 @@
<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>
@@ -1115,8 +1120,16 @@
</div>
<!-- 加载状态 -->
<div v-if="deepLoading" class="bull-loading">
<span class="loading-dot"></span> 正在进行深度分析...
<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>
<!-- 分析报告 -->
@@ -1142,6 +1155,24 @@
</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>
@@ -1266,10 +1297,10 @@
</div>
<!-- 空状态 -->
<div v-if="!deepReport && !deepLoading" class="bull-empty-tip">
<div class="empty-icon" style="font-size:28px">个股深析</div>
<p>输入股票代码,获取深度技术分析报告</p>
<p class="sub-tip">包含价格位置、压力支撑、量价分析、空间估算、综合评分</p>
<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><!-- 个股深析子页面结束 -->
@@ -1288,10 +1319,10 @@
<span class="loading-dot"></span> 正在批量分析买入推荐股...{{ buyAnalysisProgress }}/{{ buyAnalysisTotal }}
</div>
<div v-if="!buyAnalysisLoading && buyAnalysisList.length === 0" class="bull-empty-tip">
<div class="empty-icon" style="font-size:28px">📊</div>
<p>暂无买入推荐股票</p>
<p class="sub-tip">当全景扫描中有买入推荐时,会自动进行深度分析</p>
<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">
@@ -1316,6 +1347,22 @@
<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>
@@ -1953,7 +2000,7 @@
<!-- 止损预警区(可折叠) -->
<div v-if="stopLossAlerts && stopLossAlerts.length > 0" class="stoploss-alerts-card stoploss-collapsible">
<div class="stoploss-alerts-header" @click="stopLossAlertsExpanded = !stopLossAlertsExpanded">
<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>
@@ -1985,7 +2032,7 @@
</div>
<!-- 交易记录表单弹窗 -->
<div v-if="showTradeForm" class="modal-overlay" @click.self="showTradeForm = false">
<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">
@@ -2074,7 +2121,7 @@
<button class="toggle-btn" @click.stop="toggleTradeGroup(group.code)">
{{ expandedTradeGroups[group.code] ? '▼' : '▶' }}
</button>
<div class="trade-group-content" @click="loadFundamental(group.code, group.name)">
<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>