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
+9 -5
View File
@@ -8,17 +8,19 @@
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<style>
:root {
--bg-dark: #0a0a0a;
--bg-dark: #020617;
--bg-glass: rgba(255, 255, 255, 0.05);
--bg-glass-hover: rgba(255, 255, 255, 0.08);
--bg-content: #0f172a;
--border-glass: rgba(255, 255, 255, 0.1);
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.7);
--text-muted: rgba(255, 255, 255, 0.4);
--accent: #ffffff;
--accent: #fbbf24;
--primary: #fbbf24;
--success: #00ff88;
--danger: #ff4444;
--warning: #ffaa00;
--warning: #fbbf24;
--info: #3b82f6;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
@@ -29,12 +31,13 @@
color: var(--text-primary);
padding-bottom: env(safe-area-inset-bottom);
}
.admin-wrap { max-width: 800px; margin: 0 auto; padding: 20px 16px; padding-top: max(20px, env(safe-area-inset-top)); }
.admin-wrap { max-width: 800px; margin: 0 auto; padding: 20px 16px; padding-top: max(20px, env(safe-area-inset-top)); background: var(--bg-content); min-height: 100vh; }
/* Header */
.admin-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.admin-header h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.5px; display: flex; align-items: center; gap: 8px; }
.admin-header h1 .icon { width: 22px; height: 22px; opacity: 0.9; }
.admin-badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 6px; font-size: 10px; font-weight: 700; letter-spacing: 0.5px; background: rgba(251,191,36,0.15); color: var(--accent); border: 1px solid rgba(251,191,36,0.3); }
.back-link { color: var(--text-muted); text-decoration: none; font-size: 13px; display: flex; align-items: center; gap: 4px; transition: color 0.2s; }
.back-link:hover { color: var(--text-primary); }
.back-link svg { width: 14px; height: 14px; }
@@ -43,7 +46,7 @@
.nav-tabs { display: flex; gap: 2px; margin-bottom: 20px; background: var(--bg-glass); border-radius: 12px; padding: 3px; }
.nav-tab { flex: 1; padding: 8px 4px; border: none; background: transparent; color: var(--text-muted); cursor: pointer; border-radius: 10px; font-size: 13px; font-weight: 500; transition: all 0.2s; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.nav-tab svg { width: 18px; height: 18px; opacity: 0.5; transition: opacity 0.2s; }
.nav-tab.active { background: var(--bg-glass-hover); color: var(--text-primary); }
.nav-tab.active { background: var(--bg-glass-hover); color: var(--accent); }
.nav-tab.active svg { opacity: 1; }
.nav-tab:hover:not(.active) { color: var(--text-secondary); }
@@ -265,6 +268,7 @@
<h1>
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
管理后台
<span class="admin-badge">ADMIN</span>
</h1>
<a class="back-link" href="javascript:void(0)" onclick="if(window.opener||window.history.length<=1){window.close()}else{window.location.href='/'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
+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>