feat: UI/UX全面优化 — CSS变量体系/a11y无障碍/Admin角色化/骨架屏/打印样式
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user