UI/UX优化: CSS变量化 + 错误反馈增强 + 按钮type属性

- CSS: 全部硬编码hex颜色替换为CSS变量(base/scan/pages/responsive/admin)
- CSS: 硬编码字体提取为--font-sans/--font-mono变量
- CSS: 新增打印色/渐变色/辅助色等20+变量定义
- CSS: 修复scan.css lint警告(line-clamp标准属性+空规则集)
- app.js: 16处用户可感知catch块补充showToast错误反馈
- app.js: 全部console.log降级为console.debug
- index.html: 76个button补充type属性
- index.html: 内联hex颜色/SVG stroke/Vue :style替换为CSS变量
- admin.html: 残留hex替换为CSS变量
- 全部CSS版本号更新用于缓存刷新
This commit is contained in:
selfrelease
2026-07-18 09:53:34 +08:00
parent 03e095ded0
commit bb5a72767f
7 changed files with 267 additions and 209 deletions
+40 -2
View File
@@ -30,6 +30,44 @@
/* 涨跌色(红涨绿跌) */
--up: #ff4444;
--down: #00c853;
/* 辅助语义色 */
--watch: #6495ed;
--gold: #ffd700;
--hold: #ffd93d;
--hot: #ff6b35;
--card-bg: #1e1e2e;
--bg-input: #1a1a1a;
--font-mono: 'Courier New', monospace;
--font-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC', sans-serif;
/* 深度分析辅助色 */
--up-soft: #ef9a9a;
--down-soft: #a5d6a7;
--info-light: #64b5f6;
--info-bg: #2196F3;
--score-low: #78909c;
--sig-name: #ff9800;
--sig-strength: #ffb74d;
--step-watch: #748ffc;
/* 打印色(深色版本,用于@print) */
--print-up: #c0392b;
--print-down: #27ae60;
--print-mid: #e67e22;
--print-low: #7f8c8d;
/* 牛股/推荐标签辅助色 */
--teal: #00ce9e;
--purple-light: #ce93d8;
--gray-mid: #9E9E9E;
/* 渐变色 */
--grad-strategy: linear-gradient(135deg, #ff6b6b, #ffd93d);
--grad-strategy-text: #1a1a2e;
--grad-auto: linear-gradient(135deg, #00ff88, #00ccff);
--grad-rank2: linear-gradient(135deg, var(--success), #00b4d8);
--grad-rank3: linear-gradient(135deg, #748ffc, #9775fa);
--grad-algo: linear-gradient(135deg, #667eea, #764ba2);
--grad-progress: linear-gradient(90deg, #2196F3, #4CAF50);
/* 内联样式辅助色 */
--accent-cyan: #4ecdc4;
--text-dim: #555;
}
[v-cloak] {
@@ -43,7 +81,7 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC', sans-serif;
font-family: var(--font-sans);
background: var(--bg-dark);
min-height: 100vh;
color: var(--text-primary);
@@ -564,7 +602,7 @@
top: calc(100% + 4px);
left: 0;
right: 0;
background: #1a1a1a;
background: var(--bg-input);
border: none;
border-radius: 12px;
z-index: 100;
+36 -36
View File
@@ -28,7 +28,7 @@
}
.tech-btn.batch { background: var(--warning); color: #333; }
.tech-btn.fullscan { background: var(--primary); color: #fff; }
.tech-btn.strategy { background: linear-gradient(135deg, #ff6b6b, #ffd93d); color: #1a1a2e; font-weight: 700; }
.tech-btn.strategy { background: var(--grad-strategy); color: var(--grad-strategy-text); font-weight: 700; }
.tech-btn.rescan {
background: transparent; border: 1px solid rgba(255,255,255,0.15);
color: var(--text-muted); font-size: 11px;
@@ -189,7 +189,7 @@
}
.sim-btn.auto {
background: linear-gradient(135deg, #00ff88, #00ccff);
background: var(--grad-auto);
color: #000;
}
@@ -701,12 +701,12 @@
background: #555;
}
.rank-1 .rank-badge { background: linear-gradient(135deg, var(--danger), var(--warning)); }
.rank-2 .rank-badge { background: linear-gradient(135deg, var(--success), #00b4d8); }
.rank-3 .rank-badge { background: linear-gradient(135deg, #748ffc, #9775fa); }
.rank-2 .rank-badge { background: var(--grad-rank2); }
.rank-3 .rank-badge { background: var(--grad-rank3); }
.rank-name { font-weight: 600; color: var(--text-primary); }
.rank-1 .rank-name { color: var(--warning); }
.rank-2 .rank-name { color: var(--success); }
.rank-3 .rank-name { color: #748ffc; }
.rank-3 .rank-name { color: var(--step-watch); }
.rank-rate {
font-weight: 700;
color: var(--success);
@@ -842,7 +842,7 @@
font-size: 12px;
color: var(--text-secondary);
}
.step-watch .step-action { color: #748ffc; }
.step-watch .step-action { color: var(--step-watch); }
.step-buy .step-action { color: var(--danger); }
.step-add .step-action { color: var(--success); }
.step-hold .step-action { color: var(--warning); }
@@ -1383,7 +1383,7 @@
}
.algo-badge {
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--grad-algo);
color: #fff;
padding: 3px 10px;
border-radius: 6px;
@@ -1564,7 +1564,7 @@
padding: 10px 16px;
border: none;
border-radius: 8px;
background: #2196F3;
background: var(--info-bg);
color: #fff;
font-size: 14px;
font-weight: 600;
@@ -1684,11 +1684,11 @@
border-radius: 6px;
flex-wrap: wrap;
}
.deep-sig-name { font-weight: 600; color: #ff9800; font-size: 13px; }
.deep-sig-name { font-weight: 600; color: var(--sig-name); font-size: 13px; }
.deep-sig-strength {
font-size: 11px;
background: rgba(255,152,0,0.2);
color: #ffb74d;
color: var(--sig-strength);
padding: 1px 6px;
border-radius: 4px;
}
@@ -1707,26 +1707,26 @@
.pos-bar-wrap { display: flex; align-items: center; gap: 4px; }
.pos-bar-bg { flex: 1; height: 6px; background: rgba(255,255,255,0.08); border-radius: 3px; overflow: hidden; }
.pos-bar-fill { height: 100%; border-radius: 3px; transition: width 0.5s; }
.pos-bar-fill.high { background: #f44336; }
.pos-bar-fill.mid { background: #ff9800; }
.pos-bar-fill.low { background: #4caf50; }
.pos-bar-fill.high { background: var(--up); }
.pos-bar-fill.mid { background: var(--warning); }
.pos-bar-fill.low { background: var(--down); }
.pos-pct { font-size: 11px; color: rgba(255,255,255,0.5); min-width: 28px; }
.pos-space { display: flex; gap: 6px; font-size: 11px; }
.space-up { color: #f44336; }
.space-down { color: #4caf50; }
.space-up { color: var(--up); }
.space-down { color: var(--down); }
.deep-sr-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.sr-col-title { font-size: 12px; font-weight: 600; margin-bottom: 6px; padding-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.08); }
.support-title { color: #4caf50; }
.resist-title { color: #f44336; }
.support-title { color: var(--down); }
.resist-title { color: var(--up); }
.sr-item {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 13px;
}
.sr-item.support .sr-level { color: #4caf50; font-weight: 600; }
.sr-item.resist .sr-level { color: #f44336; font-weight: 600; }
.sr-item.support .sr-level { color: var(--down); font-weight: 600; }
.sr-item.resist .sr-level { color: var(--up); font-weight: 600; }
.sr-name { color: rgba(255,255,255,0.5); }
.sr-empty { color: rgba(255,255,255,0.2); font-size: 12px; text-align: center; padding: 8px; }
@@ -1737,8 +1737,8 @@
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.7);
}
.vol-tag.vol-up { background: rgba(244,67,54,0.15); color: #f44336; }
.vol-tag.vol-dn { background: rgba(76,175,80,0.15); color: #4caf50; }
.vol-tag.vol-up { background: rgba(244,67,54,0.15); color: var(--up); }
.vol-tag.vol-dn { background: rgba(76,175,80,0.15); color: var(--down); }
.vol-detail { font-size: 12px; color: rgba(255,255,255,0.4); }
.deep-patterns { display: flex; flex-wrap: wrap; gap: 6px; }
@@ -1749,8 +1749,8 @@
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.6);
}
.pattern-tag.bullish { background: rgba(244,67,54,0.12); color: #ef9a9a; }
.pattern-tag.bearish { background: rgba(76,175,80,0.12); color: #a5d6a7; }
.pattern-tag.bullish { background: rgba(244,67,54,0.12); color: var(--up-soft); }
.pattern-tag.bearish { background: rgba(76,175,80,0.12); color: var(--down-soft); }
.pattern-tag small { opacity: 0.7; }
.deep-ma-info {
@@ -1764,9 +1764,9 @@
font-size: 12px; font-weight: 600;
padding: 2px 8px; border-radius: 4px;
}
.ma-trend-tag.bullish { background: rgba(244,67,54,0.15); color: #f44336; }
.ma-trend-tag.bearish { background: rgba(76,175,80,0.15); color: #4caf50; }
.ma-trend-tag.mixed { background: rgba(255,152,0,0.15); color: #ff9800; }
.ma-trend-tag.bullish { background: rgba(244,67,54,0.15); color: var(--up); }
.ma-trend-tag.bearish { background: rgba(76,175,80,0.15); color: var(--down); }
.ma-trend-tag.mixed { background: rgba(255,152,0,0.15); color: var(--warning); }
.ma-val { font-size: 12px; color: rgba(255,255,255,0.4); }
.deep-fundamental {
@@ -1782,15 +1782,15 @@
padding: 3px 8px;
border-radius: 4px;
}
.score-reason-tag.positive { background: rgba(244,67,54,0.12); color: #ef9a9a; }
.score-reason-tag.negative { background: rgba(76,175,80,0.12); color: #a5d6a7; }
.score-reason-tag.positive { background: rgba(244,67,54,0.12); color: var(--up-soft); }
.score-reason-tag.negative { background: rgba(76,175,80,0.12); color: var(--down-soft); }
/* AI通俗解说 */
.deep-ai-summary {
background: linear-gradient(135deg, rgba(33,150,243,0.08), rgba(156,39,176,0.06));
border-radius: 12px;
padding: 14px 16px;
border-left: 3px solid #2196F3;
border-left: 3px solid var(--info-bg);
}
.deep-ai-title {
display: flex;
@@ -1798,7 +1798,7 @@
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #64b5f6;
color: var(--info-light);
margin-bottom: 10px;
}
.deep-ai-text {
@@ -1820,7 +1820,7 @@
}
.ai-action-label {
font-weight: 600;
color: #ff9800;
color: var(--warning);
margin-right: 4px;
}
@@ -1876,17 +1876,17 @@
.buy-card-center { text-align: center; min-width: 70px; }
.buy-card-price { font-size: 14px; font-weight: 600; color: #fff; }
.buy-card-change { font-size: 12px; display: block; }
.buy-card-change.up { color: #f44336; }
.buy-card-change.down { color: #4caf50; }
.buy-card-change.up { color: var(--up); }
.buy-card-change.down { color: var(--down); }
.buy-card-right { text-align: center; min-width: 50px; }
.buy-card-score {
font-size: 16px;
font-weight: 700;
display: block;
}
.buy-card-score.score-high { color: #f44336; }
.buy-card-score.score-mid { color: #ff9800; }
.buy-card-score.score-low { color: #78909c; }
.buy-card-score.score-high { color: var(--up); }
.buy-card-score.score-mid { color: var(--warning); }
.buy-card-score.score-low { color: var(--score-low); }
.buy-card-verdict { font-size: 11px; color: rgba(255,255,255,0.5); }
.buy-card-arrow {
font-size: 14px;
+5 -5
View File
@@ -369,17 +369,17 @@
/* 涨跌色保持可区分(改为深色) */
.deep-change.up, .batch-change.up, .alert-change.up,
.positive, .trade-profit.profit, .holding-profit.profit {
color: #c0392b !important;
color: var(--print-up) !important;
}
.deep-change.down, .batch-change.down, .alert-change.down,
.negative, .trade-profit.loss, .holding-profit.loss {
color: #27ae60 !important;
color: var(--print-down) !important;
}
/* 评分圆环保持色差 */
.deep-score-circle.score-high { background: #c0392b !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.deep-score-circle.score-mid { background: #e67e22 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.deep-score-circle.score-low { background: #7f8c8d !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.deep-score-circle.score-high { background: var(--print-up) !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.deep-score-circle.score-mid { background: var(--print-mid) !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.deep-score-circle.score-low { background: var(--print-low) !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.score-num { color: #fff !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
/* SVG 保色 */
+42 -40
View File
@@ -93,16 +93,16 @@
text-align: center;
padding: 2px 10px;
border-radius: 10px;
color: #4fc3f7;
color: var(--info);
background: rgba(79,195,247,0.10);
}
.batch-count-badge small { font-size: 10px; font-weight: 400; opacity: 0.6; }
.batch-count-badge.warm {
color: #00c853;
color: var(--down);
background: rgba(0,200,83,0.12);
}
.batch-count-badge.hot {
color: #ff6b35;
color: var(--hot);
background: rgba(255,107,53,0.14);
}
.batch-count-badge.empty {
@@ -175,6 +175,7 @@
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical;
}
@@ -207,7 +208,7 @@
.batch-rec-tag.rec-watch,
.batch-rec-tag.rec-hold {
background: rgba(100,149,237,0.18);
color: #6495ed;
color: var(--watch);
}
.batch-rec-reason {
font-size: 12px;
@@ -303,7 +304,7 @@
width: 16px;
height: 16px;
border: 2px solid rgba(108, 92, 231, 0.3);
border-top-color: #6c5ce7;
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@@ -352,14 +353,14 @@
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #6c5ce7, #00ce9e);
background: linear-gradient(90deg, var(--primary), var(--success));
border-radius: 3px;
transition: width 0.5s ease;
}
.progress-done {
margin-top: 8px;
font-size: 13px;
color: #00ce9e;
color: var(--success);
font-weight: 600;
}
@@ -401,7 +402,7 @@
}
.section-close:hover {
background: rgba(255, 107, 107, 0.2);
color: #ff6b6b;
color: var(--danger);
}
.section-close.inline {
display: inline-flex;
@@ -449,7 +450,7 @@
font-size: 12px;
color: var(--text-secondary);
}
.scan-summary-text .highlight { color: #ffd93d; }
.scan-summary-text .highlight { color: var(--hold); }
.scan-summary-toggle {
font-size: 10px;
color: var(--text-muted);
@@ -484,7 +485,7 @@
background: rgba(108, 92, 231, 0.3);
color: #fff;
}
.dist-tag b { margin-left: 4px; color: #ffd93d; }
.dist-tag b { margin-left: 4px; color: var(--hold); }
.scan-filters {
display: flex;
@@ -522,10 +523,10 @@
color: #fff;
}
.filter-tag.rec-all.active { background: var(--primary, #6c5ce7); color: #fff; }
.filter-tag.rec-buy.active { background: rgba(0, 206, 158, 0.35); color: #00ce9e; }
.filter-tag.rec-sell.active { background: rgba(255, 68, 68, 0.35); color: #ff4444; }
.filter-tag.rec-hold.active { background: rgba(255, 217, 61, 0.35); color: #ffd93d; }
.filter-tag.rec-watch.active { background: rgba(100, 149, 237, 0.35); color: #6495ed; }
.filter-tag.rec-buy.active { background: rgba(0, 206, 158, 0.35); color: var(--success); }
.filter-tag.rec-sell.active { background: rgba(255, 68, 68, 0.35); color: var(--danger); }
.filter-tag.rec-hold.active { background: rgba(255, 217, 61, 0.35); color: var(--hold); }
.filter-tag.rec-watch.active { background: rgba(100, 149, 237, 0.35); color: var(--watch); }
.full-scan-list {
display: flex;
@@ -551,10 +552,11 @@
}
.scan-result-card.is-watched {
/* 不再使用左边线颜色,仅保留星标等标识 */
border-color: var(--border);
}
.scan-result-card:hover { background: rgba(255,255,255,0.06); }
.scan-watched-icon {
color: #f59e0b;
color: var(--warning);
font-size: 16px;
flex-shrink: 0;
}
@@ -566,7 +568,7 @@
margin-bottom: 4px;
}
.scan-code {
font-family: 'Courier New', monospace;
font-family: var(--font-mono);
font-size: 13px;
color: var(--primary, #6c5ce7);
font-weight: 600;
@@ -574,15 +576,15 @@
.scan-name { font-size: 13px; color: var(--text-primary); }
.scan-price {
font-size: 12px;
color: #ffd700;
color: var(--gold);
}
.scan-change {
font-size: 11px;
margin-left: 4px;
font-weight: 500;
}
.scan-change.up { color: #ff4757; }
.scan-change.down { color: #00ce9e; }
.scan-change.up { color: var(--up); }
.scan-change.down { color: var(--down); }
.scan-recommend {
font-size: 11px;
font-weight: 600;
@@ -590,10 +592,10 @@
border-radius: 3px;
margin-left: 4px;
}
.scan-recommend.buy { background: rgba(0, 206, 158, 0.2); color: #00ce9e; }
.scan-recommend.sell { background: rgba(255, 68, 68, 0.2); color: #ff4444; }
.scan-recommend.hold { background: rgba(255, 217, 61, 0.2); color: #ffd93d; }
.scan-recommend.watch-active { background: rgba(100, 149, 237, 0.2); color: #6495ed; }
.scan-recommend.buy { background: rgba(0, 206, 158, 0.2); color: var(--success); }
.scan-recommend.sell { background: rgba(255, 68, 68, 0.2); color: var(--danger); }
.scan-recommend.hold { background: rgba(255, 217, 61, 0.2); color: var(--hold); }
.scan-recommend.watch-active { background: rgba(100, 149, 237, 0.2); color: var(--watch); }
.scan-recommend.watch { background: rgba(255, 255, 255, 0.08); color: var(--text-muted); }
.scan-card-info {
display: flex;
@@ -605,7 +607,7 @@
.scan-info-left { display: flex; align-items: center; gap: 6px; }
.scan-triggered {
font-size: 12px;
color: #ffd93d;
color: var(--hold);
font-weight: 600;
}
.scan-no-signal {
@@ -625,7 +627,7 @@
}
.scan-signal-tag.active {
background: rgba(0, 206, 158, 0.15);
color: #00ce9e;
color: var(--success);
}
.scan-signal-tag.inactive {
background: rgba(255,255,255,0.03);
@@ -737,13 +739,13 @@
padding: 1px 8px;
border-radius: 10px;
}
.step-divergence .step-action { background: rgba(100, 149, 237, 0.25); color: #6495ed; }
.step-divergence .step-action { background: rgba(100, 149, 237, 0.25); color: var(--watch); }
.step-divergence.active { border-color: rgba(100, 149, 237, 0.4); }
.step-dragon .step-action { background: rgba(0, 206, 158, 0.25); color: #00ce9e; }
.step-dragon .step-action { background: rgba(0, 206, 158, 0.25); color: var(--teal); }
.step-dragon.active { border-color: rgba(0, 206, 158, 0.4); }
.step-true-dragon .step-action { background: rgba(0, 200, 83, 0.25); color: #00c853; }
.step-true-dragon .step-action { background: rgba(0, 200, 83, 0.25); color: var(--down); }
.step-true-dragon.active { border-color: rgba(0, 200, 83, 0.4); }
.step-main-wave .step-action { background: rgba(255, 68, 68, 0.25); color: #ff6b6b; }
.step-main-wave .step-action { background: rgba(255, 68, 68, 0.25); color: var(--up); }
.step-main-wave.active { border-color: rgba(255, 68, 68, 0.4); }
.bull-flow-arrow {
color: var(--text-muted);
@@ -928,7 +930,7 @@
.bull-stock-card.stage-s5 { border-left: 3px solid rgba(156, 39, 176, 0.5); }
/* 回调补涨阶段样式 */
.step-rebound .step-action { background: rgba(156, 39, 176, 0.25); color: #ce93d8; }
.step-rebound .step-action { background: rgba(156, 39, 176, 0.25); color: var(--purple-light); }
.step-rebound.active { border-color: rgba(156, 39, 176, 0.4); }
/* 信号标签列表 */
@@ -943,7 +945,7 @@
padding: 1px 6px;
border-radius: 6px;
background: rgba(0, 206, 158, 0.12);
color: #00ce9e;
color: var(--success);
white-space: nowrap;
}
/* 推荐标签 + 下一信号 */
@@ -960,12 +962,12 @@
font-weight: 600;
white-space: nowrap;
}
.rec-buy { background: rgba(244, 67, 54, 0.15); color: #f44336; }
.rec-sell { background: rgba(33, 150, 243, 0.15); color: #2196F3; }
.rec-add { background: rgba(255, 152, 0, 0.15); color: #FF9800; }
.rec-hold { background: rgba(76, 175, 80, 0.15); color: #4CAF50; }
.rec-watch { background: rgba(156, 39, 176, 0.15); color: #ce93d8; }
.rec-observe { background: rgba(158, 158, 158, 0.15); color: #9E9E9E; }
.rec-buy { background: rgba(244, 67, 54, 0.15); color: var(--up); }
.rec-sell { background: rgba(33, 150, 243, 0.15); color: var(--info-bg); }
.rec-add { background: rgba(255, 152, 0, 0.15); color: var(--sig-name); }
.rec-hold { background: rgba(76, 175, 80, 0.15); color: var(--sig-75); }
.rec-watch { background: rgba(156, 39, 176, 0.15); color: var(--purple-light); }
.rec-observe { background: rgba(158, 158, 158, 0.15); color: var(--gray-mid); }
.rec-reason {
font-size: 10px;
color: var(--text-muted);
@@ -987,8 +989,8 @@
text-align: right;
margin-top: 2px;
}
.bull-card-change.up { color: #ff4444; }
.bull-card-change.down { color: #00c853; }
.bull-card-change.up { color: var(--up); }
.bull-card-change.down { color: var(--down); }
/* 迷你进度条 */
.bull-card-progress {
display: flex;
@@ -1007,7 +1009,7 @@
.mini-progress-fill {
height: 100%;
border-radius: 2px;
background: linear-gradient(90deg, #2196F3, #4CAF50);
background: var(--grad-progress);
transition: width 0.3s;
}
.bull-card-progress .progress-label {
+16
View File
@@ -843,6 +843,7 @@
} catch (err) {
console.error('请求错误:', err);
this.error = err.response?.data?.error || err.message || '请求失败';
this.showToast(this.error, 'error');
} finally {
this.loading = false;
}
@@ -1160,6 +1161,7 @@
}
} catch (e) {
console.error('加载关注列表失败', e);
this.showToast('加载关注列表失败', 'error');
}
},
@@ -1189,6 +1191,7 @@
}
} catch (e) {
console.error('移除关注失败', e);
this.showToast('移除关注失败', 'error');
}
},
@@ -1432,6 +1435,7 @@
}
} catch (err) {
console.error('增量信号分析失败:', err);
this.showToast('信号分析失败,请稍后重试', 'error');
}
this.alertsLoading = false;
this.saveAlertsCache(this.stockAlerts);
@@ -1507,6 +1511,7 @@
}
} catch (e) {
console.error('移除关注失败', e);
this.showToast('移除关注失败', 'error');
}
// 从提醒列表中移除
@@ -1574,6 +1579,7 @@
}
} catch (err) {
console.error('加载可用资金失败:', err);
this.showToast('加载可用资金失败', 'error');
}
},
@@ -1739,6 +1745,7 @@
await this.checkStopLoss();
} catch (e) {
console.error('刷新持仓价格异常:', e);
this.showToast('刷新持仓价格失败,请稍后重试', 'error');
} finally {
this.priceRefreshing = false;
}
@@ -1801,6 +1808,7 @@
}
} catch (error) {
console.error('获取数据失败:', error);
this.showToast('获取基本面数据失败', 'error');
} finally {
this.fundamentalLoading = false;
// 绘制K线图
@@ -1824,6 +1832,7 @@
}
} catch (error) {
console.error('获取K线数据失败:', error);
this.showToast('获取K线数据失败', 'error');
} finally {
this.klineLoading = false;
}
@@ -2681,6 +2690,7 @@
}
} catch (e) {
console.error('加载模拟交易统计失败', e);
this.showToast('加载模拟交易统计失败', 'error');
}
},
@@ -2692,6 +2702,7 @@
}
} catch (e) {
console.error('加载模拟持仓失败', e);
this.showToast('加载模拟持仓失败', 'error');
}
},
@@ -2703,6 +2714,7 @@
}
} catch (e) {
console.error('加载模拟交易记录失败', e);
this.showToast('加载交易记录失败', 'error');
}
},
@@ -2717,6 +2729,7 @@
}
} catch (e) {
console.error('加载算法配置失败', e);
this.showToast('加载算法配置失败', 'error');
}
},
@@ -2728,6 +2741,7 @@
}
} catch (e) {
console.error('加载算法模板失败', e);
this.showToast('加载算法模板失败', 'error');
}
},
@@ -2739,6 +2753,7 @@
}
} catch (e) {
console.error('加载信号日志失败', e);
this.showToast('加载信号日志失败', 'error');
}
},
@@ -2750,6 +2765,7 @@
}
} catch (e) {
console.error('加载持仓元数据失败', e);
this.showToast('加载持仓元数据失败', 'error');
}
},
+10 -8
View File
@@ -22,6 +22,8 @@
--danger: #ff4444;
--warning: #fbbf24;
--info: #3b82f6;
--info-light: #60a5fa;
--purple: #a855f7;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
@@ -60,11 +62,11 @@
.stat-card { background: var(--bg-glass); border-radius: 14px; padding: 14px; display: flex; align-items: center; gap: 12px; }
.stat-icon { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.stat-icon svg { width: 18px; height: 18px; }
.stat-icon.blue { background: rgba(59,130,246,0.15); color: #3b82f6; }
.stat-icon.green { background: rgba(0,255,136,0.15); color: #00ff88; }
.stat-icon.orange { background: rgba(255,170,0,0.15); color: #ffaa00; }
.stat-icon.red { background: rgba(255,68,68,0.15); color: #ff4444; }
.stat-icon.purple { background: rgba(168,85,247,0.15); color: #a855f7; }
.stat-icon.blue { background: rgba(59,130,246,0.15); color: var(--info); }
.stat-icon.green { background: rgba(0,255,136,0.15); color: var(--success); }
.stat-icon.orange { background: rgba(255,170,0,0.15); color: var(--warning); }
.stat-icon.red { background: rgba(255,68,68,0.15); color: var(--danger); }
.stat-icon.purple { background: rgba(168,85,247,0.15); color: var(--purple); }
.stat-info .stat-val { font-size: 22px; font-weight: 700; letter-spacing: -0.5px; }
.stat-info .stat-lbl { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
@@ -338,7 +340,7 @@
</div>
<div class="sys-info-row">
<span class="lbl">🌐 外网IP</span>
<span class="val" style="font-family:monospace;color:#60a5fa;">{{ dash.server_info.external_ip }}</span>
<span class="val" style="font-family:monospace;color:var(--info-light);">{{ dash.server_info.external_ip }}</span>
</div>
<div class="sys-info-row">
<span class="lbl">🏠 内网IP</span>
@@ -346,11 +348,11 @@
</div>
<div class="sys-info-row">
<span class="lbl">🔌 应用端口</span>
<span class="val" style="font-family:monospace;color:#fbbf24;">{{ dash.server_info.app_port }}</span>
<span class="val" style="font-family:monospace;color:var(--accent);">{{ dash.server_info.app_port }}</span>
</div>
<div class="sys-info-row">
<span class="lbl">🌍 访问域名</span>
<span class="val" style="font-family:monospace;color:#34d399;">{{ dash.server_info.scheme }}://{{ dash.server_info.domain }}</span>
<span class="val" style="font-family:monospace;color:var(--success);">{{ dash.server_info.scheme }}://{{ dash.server_info.domain }}</span>
</div>
<div class="sys-info-row">
<span class="lbl">🖥️ 主机名</span>
+118 -118
View File
@@ -21,12 +21,12 @@
<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=20260718v2">
<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=20260718v2">
<link rel="stylesheet" href="/static/css/scan.css?v=20260718v1">
<link rel="stylesheet" href="/static/css/responsive.css?v=20260718v1">
<link rel="stylesheet" href="/static/css/pages.css?v=20260719v2">
<link rel="stylesheet" href="/static/css/scan.css?v=20260719v3">
<link rel="stylesheet" href="/static/css/responsive.css?v=20260719v1">
</head>
<body>
{% raw %}
@@ -47,8 +47,8 @@
<div class="confirm-dialog">
<div class="confirm-message">{{ confirmMessage }}</div>
<div class="confirm-buttons">
<button class="confirm-btn cancel" @click="handleConfirmCancel">取消</button>
<button class="confirm-btn ok" @click="handleConfirmOk">确定</button>
<button type="button" class="confirm-btn cancel" @click="handleConfirmCancel">取消</button>
<button type="button" class="confirm-btn ok" @click="handleConfirmOk">确定</button>
</div>
</div>
</div>
@@ -65,7 +65,7 @@
</span>
{{ smartResultData?.title || '执行结果' }}
</div>
<button class="modal-close-btn" @click="smartResultVisible = false">×</button>
<button type="button" class="modal-close-btn" @click="smartResultVisible = false">×</button>
</div>
<div class="smart-result-body" v-if="smartResultData">
<!-- 算法标识 -->
@@ -105,7 +105,7 @@
</div>
</div>
<div class="smart-result-footer">
<button class="confirm-btn ok" @click="smartResultVisible = false">我知道了</button>
<button type="button" class="confirm-btn ok" @click="smartResultVisible = false">我知道了</button>
</div>
</div>
</div>
@@ -116,9 +116,9 @@
<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>
<button :class="['login-tab', { active: isRegister }]" @click="isRegister = true; loginError = ''">注册</button>
<button class="modal-close-btn" @click="showLoginModal = false">×</button>
<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">
@@ -130,7 +130,7 @@
<input v-model="loginForm.password" type="password" :placeholder="isRegister ? '设置密码(至少6位)' : '请输入密码'" @keyup.enter="handleLogin">
</div>
<div v-if="loginError" class="login-error">{{ loginError }}</div>
<button class="login-submit" @click="handleLogin" :disabled="loginLoading">
<button type="button" class="login-submit" @click="handleLogin" :disabled="loginLoading">
{{ loginLoading ? '处理中...' : (isRegister ? '注册' : '登录') }}
</button>
</div>
@@ -160,8 +160,8 @@
</div>
<div class="login-modal login-modal-inline">
<div class="login-tabs">
<button :class="['login-tab', { active: !isRegister }]" @click="isRegister = false; loginError = ''">登录</button>
<button :class="['login-tab', { active: isRegister }]" @click="isRegister = true; loginError = ''">注册</button>
<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">
@@ -173,7 +173,7 @@
<input v-model="loginForm.password" type="password" :placeholder="isRegister ? '设置密码(至少6位)' : '请输入密码'" @keyup.enter="handleLogin">
</div>
<div v-if="loginError" class="login-error">{{ loginError }}</div>
<button class="login-submit" @click="handleLogin" :disabled="loginLoading">
<button type="button" class="login-submit" @click="handleLogin" :disabled="loginLoading">
{{ loginLoading ? '处理中...' : (isRegister ? '注册' : '登录') }}
</button>
</div>
@@ -194,16 +194,16 @@
</div>
<div class="user-info">
<span class="username" @click="showPasswordModal = true" class="cursor-pointer">{{ currentUser?.username }}</span>
<button class="logout-btn" @click="handleLogout">退出</button>
<button type="button" class="logout-btn" @click="handleLogout">退出</button>
</div>
</div>
<!-- 顶部导航 -->
<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>
<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>
<!-- 提醒页面 -->
@@ -226,26 +226,26 @@
<span class="summary-label">建议卖出</span>
<span class="summary-value">{{ sellAlerts.length + holdingAlerts.filter(a => a.signalType === 'sell').length }}</span>
</div>
<button class="refresh-btn-inline" @click="refreshAlerts(true)" :disabled="alertsLoading">
<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 :class="['sub-tab', { active: alertSubTab === 'holding' }]" @click="alertSubTab = 'holding'">
<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 :class="['sub-tab', { active: alertSubTab === 'watching' }]" @click="alertSubTab = 'watching'">
<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 class="add-watch-btn" @click="showAddWatch = !showAddWatch" title="添加关注">+</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 @click="addToWatch" :disabled="!newWatchCode || newWatchCode.length < 6">添加</button>
<button type="button" @click="addToWatch" :disabled="!newWatchCode || newWatchCode.length < 6">添加</button>
</div>
<div v-if="alertsLoading" class="loading">
@@ -285,7 +285,7 @@
<div class="alert-bottom">
<div class="alert-desc">{{ alert.reason }}</div>
<span class="recommend-rate sell">{{ alert.recommendRate }}%</span>
<button class="alert-action-btn sell" @click.stop="quickTradeFromAlert(alert, 'sell')">卖出</button>
<button type="button" class="alert-action-btn sell" @click.stop="quickTradeFromAlert(alert, 'sell')">卖出</button>
</div>
</div>
</div>
@@ -318,7 +318,7 @@
<div class="alert-bottom">
<div class="alert-desc">{{ alert.reason }}</div>
<span class="recommend-rate" :class="alert.recommendText === '加仓' ? 'buy' : 'hold'">{{ alert.recommendRate }}%</span>
<button v-if="alert.recommendText === '加仓'" class="alert-action-btn buy" @click.stop="quickTradeFromAlert(alert, 'buy')">加仓</button>
<button type="button"v-if="alert.recommendText === '加仓'" class="alert-action-btn buy" @click.stop="quickTradeFromAlert(alert, 'buy')">加仓</button>
</div>
</div>
</div>
@@ -387,7 +387,7 @@
<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 class="alert-action-btn buy" @click.stop="quickTradeFromAlert(alert, 'buy')">买入</button>
<button type="button" class="alert-action-btn buy" @click.stop="quickTradeFromAlert(alert, 'buy')">买入</button>
</div>
</div>
</div>
@@ -419,7 +419,7 @@
<div class="alert-bottom">
<div class="alert-desc">{{ alert.reason }}</div>
<span class="recommend-rate sell">{{ alert.recommendRate }}%</span>
<button class="alert-action-btn sell" @click.stop="quickTradeFromAlert(alert, 'sell')">卖出</button>
<button type="button" class="alert-action-btn sell" @click.stop="quickTradeFromAlert(alert, 'sell')">卖出</button>
</div>
</div>
</div>
@@ -461,20 +461,20 @@
<div class="fundamental-header">
<h3>{{ fundamentalData?.stock_name || '股票' }} 基本面</h3>
<span class="fundamental-code">{{ fundamentalData?.stock_code }}</span>
<button class="modal-close" @click="showFundamentalModal = false">×</button>
<button type="button" class="modal-close" @click="showFundamentalModal = false">×</button>
</div>
<div v-if="fundamentalData" class="fundamental-actions">
<button v-if="!searchHistory.some(h => h.code === fundamentalData.stock_code)"
<button type="button" v-if="!searchHistory.some(h => h.code === fundamentalData.stock_code)"
class="watch-btn add"
@click="addToWatchFromFundamental">
★ 添加关注
</button>
<button v-else
<button type="button" v-else
class="watch-btn remove"
@click="removeFromWatchFromFundamental">
☆ 取消关注
</button>
<button class="ai-analyze-btn"
<button type="button" class="ai-analyze-btn"
@click="aiAnalyze(fundamentalData.stock_code, fundamentalData.stock_name)"
:disabled="aiAnalyzing">
{{ aiAnalyzing ? '分析中...' : 'AI分析' }}
@@ -503,7 +503,7 @@
<span class="loading-dot"></span> AI正在分析中...
</div>
<button v-if="!aiAnalyzing" class="ai-close-btn" @click="aiAnalysisResult = ''; aiReasoningResult = ''">关闭</button>
<button type="button" v-if="!aiAnalyzing" class="ai-close-btn" @click="aiAnalysisResult = ''; aiReasoningResult = ''">关闭</button>
</div>
<div v-if="fundamentalLoading" class="fundamental-loading">
@@ -641,10 +641,10 @@
<div class="kline-header">
<h4>📈 股价走势</h4>
<div class="kline-period-tabs">
<button :class="['kline-tab', { active: klinePeriod === 'weekly' }]" @click="changeKlinePeriod('weekly')"></button>
<button :class="['kline-tab', { active: klinePeriod === 'monthly' }]" @click="changeKlinePeriod('monthly')"></button>
<button :class="['kline-tab', { active: klinePeriod === 'quarterly' }]" @click="changeKlinePeriod('quarterly')"></button>
<button :class="['kline-tab', { active: klinePeriod === 'yearly' }]" @click="changeKlinePeriod('yearly')"></button>
<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 }">
@@ -665,13 +665,13 @@
<!-- 分析页子导航 -->
<div class="sub-tabs">
<button :class="['sub-tab', { active: analysisSubTab === 'scan' }]" @click="analysisSubTab = 'scan'; fetchFullScanResults(1)">
<button type="button" :class="['sub-tab', { active: analysisSubTab === 'scan' }]" @click="analysisSubTab = 'scan'; fetchFullScanResults(1)">
全景扫描
</button>
<button :class="['sub-tab', { active: analysisSubTab === 'bull' }]" @click="analysisSubTab = 'bull'">
<button type="button" :class="['sub-tab', { active: analysisSubTab === 'bull' }]" @click="analysisSubTab = 'bull'">
个股深析
</button>
<button :class="['sub-tab', { active: analysisSubTab === 'buyanalysis' }]" @click="analysisSubTab = 'buyanalysis'; fetchBuyAnalysis()">
<button type="button" :class="['sub-tab', { active: analysisSubTab === 'buyanalysis' }]" @click="analysisSubTab = 'buyanalysis'; fetchBuyAnalysis()">
买入分析
</button>
</div>
@@ -819,10 +819,10 @@
<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 class="tech-btn" @click="fetchTechSignals()" :disabled="techLoading">
<button type="button" class="tech-btn" @click="fetchTechSignals()" :disabled="techLoading">
{{ techLoading ? '检测中...' : '检测信号' }}
</button>
<button class="tech-btn batch" @click="batchTechSignals" :disabled="techLoading">
<button type="button" class="tech-btn batch" @click="batchTechSignals" :disabled="techLoading">
批量扫描关注
</button>
</div>
@@ -850,7 +850,7 @@
<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>
<button type="button" class="section-close inline" @click.stop="fullScanStatus = null; fullScanResults = []" title="关闭"></button>
<span class="scan-summary-toggle">{{ scanSummaryCollapsed ? '▶' : '▼' }}</span>
</span>
</div>
@@ -917,9 +917,9 @@
</div>
</div>
<div v-if="fullScanTotalPages > 1" class="scan-pagination">
<button @click="fetchFullScanResults(fullScanPage - 1)" :disabled="fullScanPage <= 1">上一页</button>
<button type="button" @click="fetchFullScanResults(fullScanPage - 1)" :disabled="fullScanPage <= 1">上一页</button>
<span>{{ fullScanPage }} / {{ fullScanTotalPages }}</span>
<button @click="fetchFullScanResults(fullScanPage + 1)" :disabled="fullScanPage >= fullScanTotalPages">下一页</button>
<button type="button" @click="fetchFullScanResults(fullScanPage + 1)" :disabled="fullScanPage >= fullScanTotalPages">下一页</button>
</div>
</template>
</div>
@@ -932,14 +932,14 @@
🔔 {{ (techSignalResult.signal_status || []).filter(s => s.triggered).length }} 个信号
</span>
<span class="signal-count none" v-else-if="!techSignalResult.has_scan_data">无信号</span>
<button class="section-close" @click="techSignalResult = null" title="关闭"></button>
<button type="button" class="section-close" @click="techSignalResult = null" title="关闭"></button>
</div>
<div class="tech-result-actions">
<button v-if="!searchHistory.some(h => h.code === techSignalResult.stock_code)"
<button type="button" v-if="!searchHistory.some(h => h.code === techSignalResult.stock_code)"
class="watch-btn add" @click="addToWatchFromSignal">
★ 添加关注
</button>
<button v-else class="watch-btn remove" @click="removeFromWatchFromSignal">
<button type="button" v-else class="watch-btn remove" @click="removeFromWatchFromSignal">
☆ 取消关注
</button>
</div>
@@ -1054,7 +1054,7 @@
</div>
</template>
<div v-if="(techSignalResult.signals || []).length > 0" class="tech-signal-list">
<h5 style="color:#aaa;margin:12px 0 8px;font-size:13px;">📋 近期触发记录</h5>
<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">
@@ -1072,7 +1072,7 @@
<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 class="section-close" @click="techBatchResults = []" title="关闭"></button>
<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"
@@ -1113,7 +1113,7 @@
<div class="deep-input-row">
<input v-model="deepCode" placeholder="输入股票代码,如 601609"
class="deep-code-input" @keyup.enter="fetchDeepAnalysis()" maxlength="6">
<button class="deep-analyze-btn" @click="fetchDeepAnalysis()" :disabled="deepLoading">
<button type="button" class="deep-analyze-btn" @click="fetchDeepAnalysis()" :disabled="deepLoading">
{{ deepLoading ? '分析中...' : '深度分析' }}
</button>
</div>
@@ -1310,7 +1310,7 @@
<div class="buy-analysis-header">
<div class="buy-analysis-title">买入股深度分析</div>
<div class="buy-analysis-desc">对全景扫描中推荐买入的股票批量执行深度分析</div>
<button class="deep-analyze-btn" @click="fetchBuyAnalysis()" :disabled="buyAnalysisLoading" style="margin-top:8px;">
<button type="button" class="deep-analyze-btn" @click="fetchBuyAnalysis()" :disabled="buyAnalysisLoading" style="margin-top:8px;">
{{ buyAnalysisLoading ? '分析中...' : '刷新分析' }}
</button>
</div>
@@ -1373,7 +1373,7 @@
<div class="buy-detail-grid">
<div class="buy-detail-item">
<span class="buy-detail-label">系统推荐</span>
<span class="buy-detail-value" style="color:#f44336;font-weight:600;">{{ item.recommend.display }} {{ item.recommend.rate }}分</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>
@@ -1402,7 +1402,7 @@
<span class="buy-detail-value">{{ item.ma_trend_label }}</span>
</div>
</div>
<button class="deep-analyze-btn" style="margin-top:8px;font-size:12px;padding:6px 12px;"
<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>
@@ -1416,12 +1416,12 @@
<!-- 模拟交易统计卡片 -->
<div class="sim-stats-card">
<div class="sim-stats-header">
<h4>智能交易 <span v-if="smartAlgoConfig" style="font-size:12px;color:#888;">{{ smartAlgoConfig.algo_name }}</span></h4>
<h4>智能交易 <span v-if="smartAlgoConfig" style="font-size:12px;color:var(--text-muted);">{{ smartAlgoConfig.algo_name }}</span></h4>
<div class="sim-actions">
<button class="sim-btn auto" @click="executeAutoTrade" :disabled="simAutoTrading">
<button type="button" class="sim-btn auto" @click="executeAutoTrade" :disabled="simAutoTrading">
{{ simAutoTrading ? '执行中...' : '执行' }}
</button>
<button class="sim-btn reset" @click="confirmResetSim">重置</button>
<button type="button" class="sim-btn reset" @click="confirmResetSim">重置</button>
</div>
</div>
<div class="sim-stats-grid">
@@ -1457,18 +1457,18 @@
<span :class="(simStats.realized_profit || 0) >= 0 ? 'profit' : 'loss'">
已实现 {{ (simStats.realized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.realized_profit || 0) }}
</span>
<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;">
<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:#aaa;">
<svg class="icon-xs" viewBox="0 0 24 24" fill="none" stroke="#ff9f43" stroke-width="2"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>
<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:#888;font-size:10px;"></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">
@@ -1489,14 +1489,14 @@
</div>
</div>
<div class="sim-compare-fees">
<span>总手续费: <b style="color:#ff6b6b;">-{{ formatMoney(simStats.total_fees || 0) }}</b></span>
<span style="color:#666;font-size:11px;">
<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:#888;font-size:11px;">
<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>
@@ -1508,16 +1508,16 @@
<!-- 算法配置卡片 -->
<div class="sim-rule-card collapsible">
<div class="sim-rule-header" @click="smartConfigExpanded = !smartConfigExpanded">
<h4>⚙️ 算法配置 <span v-if="smartAlgoIsDefault" style="color:#ffd93d;font-size:12px;">(默认)</span></h4>
<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:#ccc;">快速选择算法模板:</div>
<div style="font-size:13px;font-weight:600;margin-bottom:6px;color:var(--text-secondary);">快速选择算法模板:</div>
<div class="flex-wrap-gap">
<button v-for="tpl in smartTemplates" :key="tpl.name"
<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)"
@@ -1529,18 +1529,18 @@
</div>
<!-- 当前配置详情 -->
<div v-if="smartAlgoConfig" style="font-size:12px;color:#aaa;line-height:1.8;">
<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:#00ce9e;">{{ smartAlgoConfig.take_profit_pct }}%</strong></span>
<span>止损: <strong style="color:#ff6b6b;">{{ smartAlgoConfig.stop_loss_pct }}%</strong></span>
<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:#4ecdc4;">{{ smartAlgoConfig.buy_time }}</strong></span>
<span>卖出时点: <strong style="color:#4ecdc4;">{{ smartAlgoConfig.sell_time }}</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>
@@ -1582,19 +1582,19 @@
</label>
</div>
<div style="margin-top:8px;display:flex;gap:8px;">
<button class="sim-btn auto" @click="saveSmartConfig" style="font-size:12px;">保存配置</button>
<button class="sim-btn" @click="smartConfigEditing = false; loadSmartAlgoConfig();" style="font-size:12px;">取消</button>
<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 class="sim-btn" @click="smartConfigEditing = true" style="font-size:11px;padding:2px 8px;">✏️ 自定义参数</button>
<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:#666;border-top:1px solid #222;padding-top:6px;">
⏰ 每个交易日 <strong style="color:#4ecdc4;">{{ smartAlgoConfig?.buy_time || '09:35' }}</strong> 买入 /
<strong style="color:#4ecdc4;">{{ smartAlgoConfig?.sell_time || '13:40' }}</strong> 卖出 /
<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>
@@ -1603,7 +1603,7 @@
<!-- 今日交易 -->
<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="#4ecdc4" stroke-width="2"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg>今日交易 <span class="badge">{{ todayTrades.length }}</span></h4>
<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>
@@ -1621,7 +1621,7 @@
<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:#888;">
<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>
@@ -1634,7 +1634,7 @@
<!-- 模拟持仓列表(增强版:显示算法状态) -->
<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="#00ce9e" 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>
<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>
@@ -1655,16 +1655,16 @@
</div>
<!-- 算法状态标签 -->
<div v-if="getActiveRules(pos.stock_code).length > 0 || getPositionMeta(pos.stock_code)"
class="sim-pos-algo-status" style="font-size:11px;color:#888;margin-top:2px;display:flex;gap:6px;flex-wrap:wrap;">
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:#1a1a2e;padding:1px 6px;border-radius:8px;font-size:10px;">{{ rule }}</span>
<span v-if="getPositionMeta(pos.stock_code)?.days_held" style="color:#666;">
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:#00ce9e;">
<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:#ffd93d;">
<span v-if="getPositionMeta(pos.stock_code)?.consecutive_sell_signals > 0" style="color:var(--hold);">
卖信号{{ getPositionMeta(pos.stock_code).consecutive_sell_signals }}天
</span>
</div>
@@ -1675,32 +1675,32 @@
<!-- 信号日志(可折叠) -->
<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="#ffd93d" 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>
<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 #1a1a2e;font-size:12px;display:flex;gap:8px;align-items:flex-start;">
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' ? '#ff6b6b' :
sig.action === 'sell' ? '#00ce9e' :
sig.action === 'partial_sell' ? '#ffd93d' : '#888',
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:#ccc;flex:1;">
<span style="color:var(--text-secondary);flex:1;">
{{ sig.stock_code }}
<span v-if="sig.algo_rule" style="color:#4ecdc4;font-size:11px;">[{{ sig.algo_rule }}]</span>
<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 ? '#00ce9e' : '#ff6b6b'}">
<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:#555;font-size:11px;white-space:nowrap;">{{ sig.signal_date }}</span>
<span style="color:var(--text-dim);font-size:11px;white-space:nowrap;">{{ sig.signal_date }}</span>
</div>
</div>
</div>
@@ -1709,7 +1709,7 @@
<!-- 模拟交易记录 -->
<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="#a78bfa" 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>
<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>
@@ -1727,9 +1727,9 @@
<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:#888;">
<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:#666;">(佣金{{ trade.commission?.toFixed(2) }}{{ trade.stamp_tax > 0 ? ' + 印花税' + trade.stamp_tax?.toFixed(2) : '' }})</span>
<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>
@@ -1746,10 +1746,10 @@
<div v-show="activeTab === 'model'" class="model-page">
<!-- 模型页子导航 -->
<div class="sub-tabs">
<button :class="['sub-tab', { active: modelSubTab === 'system' }]" @click="modelSubTab = 'system'">
<button type="button" :class="['sub-tab', { active: modelSubTab === 'system' }]" @click="modelSubTab = 'system'">
信号体系
</button>
<button :class="['sub-tab', { active: modelSubTab === 'sim' }]" @click="modelSubTab = 'sim'; loadSimData()">
<button type="button" :class="['sub-tab', { active: modelSubTab === 'sim' }]" @click="modelSubTab = 'sim'; loadSimData()">
模拟交易
</button>
</div>
@@ -1875,12 +1875,12 @@
<!-- 模拟交易统计卡片 -->
<div class="sim-stats-card">
<div class="sim-stats-header">
<h4>智能交易 <span v-if="smartAlgoConfig" style="font-size:12px;color:#888;">{{ smartAlgoConfig.algo_name }}</span></h4>
<h4>智能交易 <span v-if="smartAlgoConfig" style="font-size:12px;color:var(--text-muted);">{{ smartAlgoConfig.algo_name }}</span></h4>
<div class="sim-actions">
<button class="sim-btn auto" @click="executeAutoTrade" :disabled="simAutoTrading">
<button type="button" class="sim-btn auto" @click="executeAutoTrade" :disabled="simAutoTrading">
{{ simAutoTrading ? '执行中...' : '执行' }}
</button>
<button class="sim-btn reset" @click="confirmResetSim">重置</button>
<button type="button" class="sim-btn reset" @click="confirmResetSim">重置</button>
</div>
</div>
<div class="sim-stats-grid">
@@ -1916,7 +1916,7 @@
<span :class="(simStats.realized_profit || 0) >= 0 ? 'profit' : 'loss'">
已实现 {{ (simStats.realized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.realized_profit || 0) }}
</span>
<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;">
<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>
@@ -1982,13 +1982,13 @@
<span class="cash-label">可用资金</span>
<template v-if="!editingCash">
<span class="cash-value">{{ formatMoney(availableCash) }}</span>
<button class="cash-edit-btn" @click="startEditCash" title="编辑可用资金">✏️</button>
<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 class="cash-save-btn" @click="saveCash"></button>
<button class="cash-cancel-btn" @click="cancelEditCash"></button>
<button type="button" class="cash-save-btn" @click="saveCash"></button>
<button type="button" class="cash-cancel-btn" @click="cancelEditCash"></button>
</template>
</div>
<!-- 总资产 = 持仓市值 + 可用资金 -->
@@ -2016,17 +2016,17 @@
<span v-if="alert.stop_loss_price">止损 ¥{{ alert.stop_loss_price }}</span>
</div>
<div class="stoploss-alert-message">{{ alert.message }}</div>
<button class="stoploss-sell-btn" @click="quickSellFromStopLoss(alert)">立即卖出</button>
<button type="button" class="stoploss-sell-btn" @click="quickSellFromStopLoss(alert)">立即卖出</button>
</div>
</div>
</div>
<!-- 操作按钮区 -->
<div class="trade-actions-bar">
<button class="add-trade-btn" @click="openTradeForm" :disabled="allStocksForTrade.length === 0">
<button type="button" class="add-trade-btn" @click="openTradeForm" :disabled="allStocksForTrade.length === 0">
{{ allStocksForTrade.length === 0 ? '请先分析股票' : '记录交易' }}
</button>
<button class="refresh-price-btn" @click="refreshHoldingPrices" :disabled="priceRefreshing">
<button type="button" class="refresh-price-btn" @click="refreshHoldingPrices" :disabled="priceRefreshing">
{{ priceRefreshing ? '刷新中...' : '刷新价格' }}
</button>
</div>
@@ -2100,10 +2100,10 @@
<!-- 持有/清仓 子Tab -->
<div class="trade-sub-tabs">
<button :class="['trade-sub-tab', { active: tradeViewType === 'holding' }]" @click="tradeViewType = 'holding'">
<button type="button" :class="['trade-sub-tab', { active: tradeViewType === 'holding' }]" @click="tradeViewType = 'holding'">
持有 <span class="tab-count">{{ holdingGroupCount }}</span>
</button>
<button :class="['trade-sub-tab', { active: tradeViewType === 'cleared' }]" @click="tradeViewType = 'cleared'">
<button type="button" :class="['trade-sub-tab', { active: tradeViewType === 'cleared' }]" @click="tradeViewType = 'cleared'">
清仓 <span class="tab-count">{{ clearedGroupCount }}</span>
</button>
</div>
@@ -2118,7 +2118,7 @@
<div v-for="group in filteredGroupedTrades" :key="group.code" class="trade-group">
<!-- 股票分组标题 -->
<div class="trade-group-header">
<button class="toggle-btn" @click.stop="toggleTradeGroup(group.code)">
<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)">
@@ -2194,8 +2194,8 @@
<div class="trade-date">{{ trade.trade_date }}</div>
</div>
<div class="trade-actions-vertical">
<button @click="editTrade(trade)">编辑</button>
<button @click="deleteTrade(trade.id)">删除</button>
<button type="button" @click="editTrade(trade)">编辑</button>
<button type="button" @click="deleteTrade(trade.id)">删除</button>
</div>
</div>
</div>
@@ -2225,8 +2225,8 @@
<div v-if="passwordError" class="login-error">{{ passwordError }}</div>
<div v-if="passwordSuccess" class="login-success">{{ passwordSuccess }}</div>
<div class="password-buttons">
<button class="btn-cancel" @click="showPasswordModal = false">关闭</button>
<button class="login-submit" @click="handleChangePassword" :disabled="passwordLoading">
<button type="button" class="btn-cancel" @click="showPasswordModal = false">关闭</button>
<button type="button" class="login-submit" @click="handleChangePassword" :disabled="passwordLoading">
{{ passwordLoading ? '处理中...' : '确认修改' }}
</button>
</div>