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; --up: #ff4444;
--down: #00c853; --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] { [v-cloak] {
@@ -43,7 +81,7 @@
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'PingFang SC', sans-serif; font-family: var(--font-sans);
background: var(--bg-dark); background: var(--bg-dark);
min-height: 100vh; min-height: 100vh;
color: var(--text-primary); color: var(--text-primary);
@@ -564,7 +602,7 @@
top: calc(100% + 4px); top: calc(100% + 4px);
left: 0; left: 0;
right: 0; right: 0;
background: #1a1a1a; background: var(--bg-input);
border: none; border: none;
border-radius: 12px; border-radius: 12px;
z-index: 100; z-index: 100;
+36 -36
View File
@@ -28,7 +28,7 @@
} }
.tech-btn.batch { background: var(--warning); color: #333; } .tech-btn.batch { background: var(--warning); color: #333; }
.tech-btn.fullscan { background: var(--primary); color: #fff; } .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 { .tech-btn.rescan {
background: transparent; border: 1px solid rgba(255,255,255,0.15); background: transparent; border: 1px solid rgba(255,255,255,0.15);
color: var(--text-muted); font-size: 11px; color: var(--text-muted); font-size: 11px;
@@ -189,7 +189,7 @@
} }
.sim-btn.auto { .sim-btn.auto {
background: linear-gradient(135deg, #00ff88, #00ccff); background: var(--grad-auto);
color: #000; color: #000;
} }
@@ -701,12 +701,12 @@
background: #555; background: #555;
} }
.rank-1 .rank-badge { background: linear-gradient(135deg, var(--danger), var(--warning)); } .rank-1 .rank-badge { background: linear-gradient(135deg, var(--danger), var(--warning)); }
.rank-2 .rank-badge { background: linear-gradient(135deg, var(--success), #00b4d8); } .rank-2 .rank-badge { background: var(--grad-rank2); }
.rank-3 .rank-badge { background: linear-gradient(135deg, #748ffc, #9775fa); } .rank-3 .rank-badge { background: var(--grad-rank3); }
.rank-name { font-weight: 600; color: var(--text-primary); } .rank-name { font-weight: 600; color: var(--text-primary); }
.rank-1 .rank-name { color: var(--warning); } .rank-1 .rank-name { color: var(--warning); }
.rank-2 .rank-name { color: var(--success); } .rank-2 .rank-name { color: var(--success); }
.rank-3 .rank-name { color: #748ffc; } .rank-3 .rank-name { color: var(--step-watch); }
.rank-rate { .rank-rate {
font-weight: 700; font-weight: 700;
color: var(--success); color: var(--success);
@@ -842,7 +842,7 @@
font-size: 12px; font-size: 12px;
color: var(--text-secondary); 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-buy .step-action { color: var(--danger); }
.step-add .step-action { color: var(--success); } .step-add .step-action { color: var(--success); }
.step-hold .step-action { color: var(--warning); } .step-hold .step-action { color: var(--warning); }
@@ -1383,7 +1383,7 @@
} }
.algo-badge { .algo-badge {
background: linear-gradient(135deg, #667eea, #764ba2); background: var(--grad-algo);
color: #fff; color: #fff;
padding: 3px 10px; padding: 3px 10px;
border-radius: 6px; border-radius: 6px;
@@ -1564,7 +1564,7 @@
padding: 10px 16px; padding: 10px 16px;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
background: #2196F3; background: var(--info-bg);
color: #fff; color: #fff;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
@@ -1684,11 +1684,11 @@
border-radius: 6px; border-radius: 6px;
flex-wrap: wrap; 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 { .deep-sig-strength {
font-size: 11px; font-size: 11px;
background: rgba(255,152,0,0.2); background: rgba(255,152,0,0.2);
color: #ffb74d; color: var(--sig-strength);
padding: 1px 6px; padding: 1px 6px;
border-radius: 4px; border-radius: 4px;
} }
@@ -1707,26 +1707,26 @@
.pos-bar-wrap { display: flex; align-items: center; gap: 4px; } .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-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 { height: 100%; border-radius: 3px; transition: width 0.5s; }
.pos-bar-fill.high { background: #f44336; } .pos-bar-fill.high { background: var(--up); }
.pos-bar-fill.mid { background: #ff9800; } .pos-bar-fill.mid { background: var(--warning); }
.pos-bar-fill.low { background: #4caf50; } .pos-bar-fill.low { background: var(--down); }
.pos-pct { font-size: 11px; color: rgba(255,255,255,0.5); min-width: 28px; } .pos-pct { font-size: 11px; color: rgba(255,255,255,0.5); min-width: 28px; }
.pos-space { display: flex; gap: 6px; font-size: 11px; } .pos-space { display: flex; gap: 6px; font-size: 11px; }
.space-up { color: #f44336; } .space-up { color: var(--up); }
.space-down { color: #4caf50; } .space-down { color: var(--down); }
.deep-sr-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .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); } .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; } .support-title { color: var(--down); }
.resist-title { color: #f44336; } .resist-title { color: var(--up); }
.sr-item { .sr-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 4px 0; padding: 4px 0;
font-size: 13px; font-size: 13px;
} }
.sr-item.support .sr-level { color: #4caf50; font-weight: 600; } .sr-item.support .sr-level { color: var(--down); font-weight: 600; }
.sr-item.resist .sr-level { color: #f44336; font-weight: 600; } .sr-item.resist .sr-level { color: var(--up); font-weight: 600; }
.sr-name { color: rgba(255,255,255,0.5); } .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; } .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); background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.7); color: rgba(255,255,255,0.7);
} }
.vol-tag.vol-up { background: rgba(244,67,54,0.15); color: #f44336; } .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: #4caf50; } .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); } .vol-detail { font-size: 12px; color: rgba(255,255,255,0.4); }
.deep-patterns { display: flex; flex-wrap: wrap; gap: 6px; } .deep-patterns { display: flex; flex-wrap: wrap; gap: 6px; }
@@ -1749,8 +1749,8 @@
background: rgba(255,255,255,0.06); background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.6); color: rgba(255,255,255,0.6);
} }
.pattern-tag.bullish { background: rgba(244,67,54,0.12); color: #ef9a9a; } .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: #a5d6a7; } .pattern-tag.bearish { background: rgba(76,175,80,0.12); color: var(--down-soft); }
.pattern-tag small { opacity: 0.7; } .pattern-tag small { opacity: 0.7; }
.deep-ma-info { .deep-ma-info {
@@ -1764,9 +1764,9 @@
font-size: 12px; font-weight: 600; font-size: 12px; font-weight: 600;
padding: 2px 8px; border-radius: 4px; padding: 2px 8px; border-radius: 4px;
} }
.ma-trend-tag.bullish { background: rgba(244,67,54,0.15); color: #f44336; } .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: #4caf50; } .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: #ff9800; } .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); } .ma-val { font-size: 12px; color: rgba(255,255,255,0.4); }
.deep-fundamental { .deep-fundamental {
@@ -1782,15 +1782,15 @@
padding: 3px 8px; padding: 3px 8px;
border-radius: 4px; border-radius: 4px;
} }
.score-reason-tag.positive { background: rgba(244,67,54,0.12); color: #ef9a9a; } .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: #a5d6a7; } .score-reason-tag.negative { background: rgba(76,175,80,0.12); color: var(--down-soft); }
/* AI通俗解说 */ /* AI通俗解说 */
.deep-ai-summary { .deep-ai-summary {
background: linear-gradient(135deg, rgba(33,150,243,0.08), rgba(156,39,176,0.06)); background: linear-gradient(135deg, rgba(33,150,243,0.08), rgba(156,39,176,0.06));
border-radius: 12px; border-radius: 12px;
padding: 14px 16px; padding: 14px 16px;
border-left: 3px solid #2196F3; border-left: 3px solid var(--info-bg);
} }
.deep-ai-title { .deep-ai-title {
display: flex; display: flex;
@@ -1798,7 +1798,7 @@
gap: 6px; gap: 6px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #64b5f6; color: var(--info-light);
margin-bottom: 10px; margin-bottom: 10px;
} }
.deep-ai-text { .deep-ai-text {
@@ -1820,7 +1820,7 @@
} }
.ai-action-label { .ai-action-label {
font-weight: 600; font-weight: 600;
color: #ff9800; color: var(--warning);
margin-right: 4px; margin-right: 4px;
} }
@@ -1876,17 +1876,17 @@
.buy-card-center { text-align: center; min-width: 70px; } .buy-card-center { text-align: center; min-width: 70px; }
.buy-card-price { font-size: 14px; font-weight: 600; color: #fff; } .buy-card-price { font-size: 14px; font-weight: 600; color: #fff; }
.buy-card-change { font-size: 12px; display: block; } .buy-card-change { font-size: 12px; display: block; }
.buy-card-change.up { color: #f44336; } .buy-card-change.up { color: var(--up); }
.buy-card-change.down { color: #4caf50; } .buy-card-change.down { color: var(--down); }
.buy-card-right { text-align: center; min-width: 50px; } .buy-card-right { text-align: center; min-width: 50px; }
.buy-card-score { .buy-card-score {
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
display: block; display: block;
} }
.buy-card-score.score-high { color: #f44336; } .buy-card-score.score-high { color: var(--up); }
.buy-card-score.score-mid { color: #ff9800; } .buy-card-score.score-mid { color: var(--warning); }
.buy-card-score.score-low { color: #78909c; } .buy-card-score.score-low { color: var(--score-low); }
.buy-card-verdict { font-size: 11px; color: rgba(255,255,255,0.5); } .buy-card-verdict { font-size: 11px; color: rgba(255,255,255,0.5); }
.buy-card-arrow { .buy-card-arrow {
font-size: 14px; font-size: 14px;
+5 -5
View File
@@ -369,17 +369,17 @@
/* 涨跌色保持可区分(改为深色) */ /* 涨跌色保持可区分(改为深色) */
.deep-change.up, .batch-change.up, .alert-change.up, .deep-change.up, .batch-change.up, .alert-change.up,
.positive, .trade-profit.profit, .holding-profit.profit { .positive, .trade-profit.profit, .holding-profit.profit {
color: #c0392b !important; color: var(--print-up) !important;
} }
.deep-change.down, .batch-change.down, .alert-change.down, .deep-change.down, .batch-change.down, .alert-change.down,
.negative, .trade-profit.loss, .holding-profit.loss { .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-high { background: var(--print-up) !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-mid { background: var(--print-mid) !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-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; } .score-num { color: #fff !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
/* SVG 保色 */ /* SVG 保色 */
+42 -40
View File
@@ -93,16 +93,16 @@
text-align: center; text-align: center;
padding: 2px 10px; padding: 2px 10px;
border-radius: 10px; border-radius: 10px;
color: #4fc3f7; color: var(--info);
background: rgba(79,195,247,0.10); background: rgba(79,195,247,0.10);
} }
.batch-count-badge small { font-size: 10px; font-weight: 400; opacity: 0.6; } .batch-count-badge small { font-size: 10px; font-weight: 400; opacity: 0.6; }
.batch-count-badge.warm { .batch-count-badge.warm {
color: #00c853; color: var(--down);
background: rgba(0,200,83,0.12); background: rgba(0,200,83,0.12);
} }
.batch-count-badge.hot { .batch-count-badge.hot {
color: #ff6b35; color: var(--hot);
background: rgba(255,107,53,0.14); background: rgba(255,107,53,0.14);
} }
.batch-count-badge.empty { .batch-count-badge.empty {
@@ -175,6 +175,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
@@ -207,7 +208,7 @@
.batch-rec-tag.rec-watch, .batch-rec-tag.rec-watch,
.batch-rec-tag.rec-hold { .batch-rec-tag.rec-hold {
background: rgba(100,149,237,0.18); background: rgba(100,149,237,0.18);
color: #6495ed; color: var(--watch);
} }
.batch-rec-reason { .batch-rec-reason {
font-size: 12px; font-size: 12px;
@@ -303,7 +304,7 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
border: 2px solid rgba(108, 92, 231, 0.3); border: 2px solid rgba(108, 92, 231, 0.3);
border-top-color: #6c5ce7; border-top-color: var(--primary);
border-radius: 50%; border-radius: 50%;
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite;
} }
@@ -352,14 +353,14 @@
} }
.progress-fill { .progress-fill {
height: 100%; height: 100%;
background: linear-gradient(90deg, #6c5ce7, #00ce9e); background: linear-gradient(90deg, var(--primary), var(--success));
border-radius: 3px; border-radius: 3px;
transition: width 0.5s ease; transition: width 0.5s ease;
} }
.progress-done { .progress-done {
margin-top: 8px; margin-top: 8px;
font-size: 13px; font-size: 13px;
color: #00ce9e; color: var(--success);
font-weight: 600; font-weight: 600;
} }
@@ -401,7 +402,7 @@
} }
.section-close:hover { .section-close:hover {
background: rgba(255, 107, 107, 0.2); background: rgba(255, 107, 107, 0.2);
color: #ff6b6b; color: var(--danger);
} }
.section-close.inline { .section-close.inline {
display: inline-flex; display: inline-flex;
@@ -449,7 +450,7 @@
font-size: 12px; font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
} }
.scan-summary-text .highlight { color: #ffd93d; } .scan-summary-text .highlight { color: var(--hold); }
.scan-summary-toggle { .scan-summary-toggle {
font-size: 10px; font-size: 10px;
color: var(--text-muted); color: var(--text-muted);
@@ -484,7 +485,7 @@
background: rgba(108, 92, 231, 0.3); background: rgba(108, 92, 231, 0.3);
color: #fff; color: #fff;
} }
.dist-tag b { margin-left: 4px; color: #ffd93d; } .dist-tag b { margin-left: 4px; color: var(--hold); }
.scan-filters { .scan-filters {
display: flex; display: flex;
@@ -522,10 +523,10 @@
color: #fff; color: #fff;
} }
.filter-tag.rec-all.active { background: var(--primary, #6c5ce7); 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-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: #ff4444; } .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: #ffd93d; } .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: #6495ed; } .filter-tag.rec-watch.active { background: rgba(100, 149, 237, 0.35); color: var(--watch); }
.full-scan-list { .full-scan-list {
display: flex; display: flex;
@@ -551,10 +552,11 @@
} }
.scan-result-card.is-watched { .scan-result-card.is-watched {
/* 不再使用左边线颜色,仅保留星标等标识 */ /* 不再使用左边线颜色,仅保留星标等标识 */
border-color: var(--border);
} }
.scan-result-card:hover { background: rgba(255,255,255,0.06); } .scan-result-card:hover { background: rgba(255,255,255,0.06); }
.scan-watched-icon { .scan-watched-icon {
color: #f59e0b; color: var(--warning);
font-size: 16px; font-size: 16px;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -566,7 +568,7 @@
margin-bottom: 4px; margin-bottom: 4px;
} }
.scan-code { .scan-code {
font-family: 'Courier New', monospace; font-family: var(--font-mono);
font-size: 13px; font-size: 13px;
color: var(--primary, #6c5ce7); color: var(--primary, #6c5ce7);
font-weight: 600; font-weight: 600;
@@ -574,15 +576,15 @@
.scan-name { font-size: 13px; color: var(--text-primary); } .scan-name { font-size: 13px; color: var(--text-primary); }
.scan-price { .scan-price {
font-size: 12px; font-size: 12px;
color: #ffd700; color: var(--gold);
} }
.scan-change { .scan-change {
font-size: 11px; font-size: 11px;
margin-left: 4px; margin-left: 4px;
font-weight: 500; font-weight: 500;
} }
.scan-change.up { color: #ff4757; } .scan-change.up { color: var(--up); }
.scan-change.down { color: #00ce9e; } .scan-change.down { color: var(--down); }
.scan-recommend { .scan-recommend {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
@@ -590,10 +592,10 @@
border-radius: 3px; border-radius: 3px;
margin-left: 4px; margin-left: 4px;
} }
.scan-recommend.buy { background: rgba(0, 206, 158, 0.2); color: #00ce9e; } .scan-recommend.buy { background: rgba(0, 206, 158, 0.2); color: var(--success); }
.scan-recommend.sell { background: rgba(255, 68, 68, 0.2); color: #ff4444; } .scan-recommend.sell { background: rgba(255, 68, 68, 0.2); color: var(--danger); }
.scan-recommend.hold { background: rgba(255, 217, 61, 0.2); color: #ffd93d; } .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: #6495ed; } .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-recommend.watch { background: rgba(255, 255, 255, 0.08); color: var(--text-muted); }
.scan-card-info { .scan-card-info {
display: flex; display: flex;
@@ -605,7 +607,7 @@
.scan-info-left { display: flex; align-items: center; gap: 6px; } .scan-info-left { display: flex; align-items: center; gap: 6px; }
.scan-triggered { .scan-triggered {
font-size: 12px; font-size: 12px;
color: #ffd93d; color: var(--hold);
font-weight: 600; font-weight: 600;
} }
.scan-no-signal { .scan-no-signal {
@@ -625,7 +627,7 @@
} }
.scan-signal-tag.active { .scan-signal-tag.active {
background: rgba(0, 206, 158, 0.15); background: rgba(0, 206, 158, 0.15);
color: #00ce9e; color: var(--success);
} }
.scan-signal-tag.inactive { .scan-signal-tag.inactive {
background: rgba(255,255,255,0.03); background: rgba(255,255,255,0.03);
@@ -737,13 +739,13 @@
padding: 1px 8px; padding: 1px 8px;
border-radius: 10px; 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-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-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-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); } .step-main-wave.active { border-color: rgba(255, 68, 68, 0.4); }
.bull-flow-arrow { .bull-flow-arrow {
color: var(--text-muted); color: var(--text-muted);
@@ -928,7 +930,7 @@
.bull-stock-card.stage-s5 { border-left: 3px solid rgba(156, 39, 176, 0.5); } .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); } .step-rebound.active { border-color: rgba(156, 39, 176, 0.4); }
/* 信号标签列表 */ /* 信号标签列表 */
@@ -943,7 +945,7 @@
padding: 1px 6px; padding: 1px 6px;
border-radius: 6px; border-radius: 6px;
background: rgba(0, 206, 158, 0.12); background: rgba(0, 206, 158, 0.12);
color: #00ce9e; color: var(--success);
white-space: nowrap; white-space: nowrap;
} }
/* 推荐标签 + 下一信号 */ /* 推荐标签 + 下一信号 */
@@ -960,12 +962,12 @@
font-weight: 600; font-weight: 600;
white-space: nowrap; white-space: nowrap;
} }
.rec-buy { background: rgba(244, 67, 54, 0.15); color: #f44336; } .rec-buy { background: rgba(244, 67, 54, 0.15); color: var(--up); }
.rec-sell { background: rgba(33, 150, 243, 0.15); color: #2196F3; } .rec-sell { background: rgba(33, 150, 243, 0.15); color: var(--info-bg); }
.rec-add { background: rgba(255, 152, 0, 0.15); color: #FF9800; } .rec-add { background: rgba(255, 152, 0, 0.15); color: var(--sig-name); }
.rec-hold { background: rgba(76, 175, 80, 0.15); color: #4CAF50; } .rec-hold { background: rgba(76, 175, 80, 0.15); color: var(--sig-75); }
.rec-watch { background: rgba(156, 39, 176, 0.15); color: #ce93d8; } .rec-watch { background: rgba(156, 39, 176, 0.15); color: var(--purple-light); }
.rec-observe { background: rgba(158, 158, 158, 0.15); color: #9E9E9E; } .rec-observe { background: rgba(158, 158, 158, 0.15); color: var(--gray-mid); }
.rec-reason { .rec-reason {
font-size: 10px; font-size: 10px;
color: var(--text-muted); color: var(--text-muted);
@@ -987,8 +989,8 @@
text-align: right; text-align: right;
margin-top: 2px; margin-top: 2px;
} }
.bull-card-change.up { color: #ff4444; } .bull-card-change.up { color: var(--up); }
.bull-card-change.down { color: #00c853; } .bull-card-change.down { color: var(--down); }
/* 迷你进度条 */ /* 迷你进度条 */
.bull-card-progress { .bull-card-progress {
display: flex; display: flex;
@@ -1007,7 +1009,7 @@
.mini-progress-fill { .mini-progress-fill {
height: 100%; height: 100%;
border-radius: 2px; border-radius: 2px;
background: linear-gradient(90deg, #2196F3, #4CAF50); background: var(--grad-progress);
transition: width 0.3s; transition: width 0.3s;
} }
.bull-card-progress .progress-label { .bull-card-progress .progress-label {
+16
View File
@@ -843,6 +843,7 @@
} catch (err) { } catch (err) {
console.error('请求错误:', err); console.error('请求错误:', err);
this.error = err.response?.data?.error || err.message || '请求失败'; this.error = err.response?.data?.error || err.message || '请求失败';
this.showToast(this.error, 'error');
} finally { } finally {
this.loading = false; this.loading = false;
} }
@@ -1160,6 +1161,7 @@
} }
} catch (e) { } catch (e) {
console.error('加载关注列表失败', e); console.error('加载关注列表失败', e);
this.showToast('加载关注列表失败', 'error');
} }
}, },
@@ -1189,6 +1191,7 @@
} }
} catch (e) { } catch (e) {
console.error('移除关注失败', e); console.error('移除关注失败', e);
this.showToast('移除关注失败', 'error');
} }
}, },
@@ -1432,6 +1435,7 @@
} }
} catch (err) { } catch (err) {
console.error('增量信号分析失败:', err); console.error('增量信号分析失败:', err);
this.showToast('信号分析失败,请稍后重试', 'error');
} }
this.alertsLoading = false; this.alertsLoading = false;
this.saveAlertsCache(this.stockAlerts); this.saveAlertsCache(this.stockAlerts);
@@ -1507,6 +1511,7 @@
} }
} catch (e) { } catch (e) {
console.error('移除关注失败', e); console.error('移除关注失败', e);
this.showToast('移除关注失败', 'error');
} }
// 从提醒列表中移除 // 从提醒列表中移除
@@ -1574,6 +1579,7 @@
} }
} catch (err) { } catch (err) {
console.error('加载可用资金失败:', err); console.error('加载可用资金失败:', err);
this.showToast('加载可用资金失败', 'error');
} }
}, },
@@ -1739,6 +1745,7 @@
await this.checkStopLoss(); await this.checkStopLoss();
} catch (e) { } catch (e) {
console.error('刷新持仓价格异常:', e); console.error('刷新持仓价格异常:', e);
this.showToast('刷新持仓价格失败,请稍后重试', 'error');
} finally { } finally {
this.priceRefreshing = false; this.priceRefreshing = false;
} }
@@ -1801,6 +1808,7 @@
} }
} catch (error) { } catch (error) {
console.error('获取数据失败:', error); console.error('获取数据失败:', error);
this.showToast('获取基本面数据失败', 'error');
} finally { } finally {
this.fundamentalLoading = false; this.fundamentalLoading = false;
// 绘制K线图 // 绘制K线图
@@ -1824,6 +1832,7 @@
} }
} catch (error) { } catch (error) {
console.error('获取K线数据失败:', error); console.error('获取K线数据失败:', error);
this.showToast('获取K线数据失败', 'error');
} finally { } finally {
this.klineLoading = false; this.klineLoading = false;
} }
@@ -2681,6 +2690,7 @@
} }
} catch (e) { } catch (e) {
console.error('加载模拟交易统计失败', e); console.error('加载模拟交易统计失败', e);
this.showToast('加载模拟交易统计失败', 'error');
} }
}, },
@@ -2692,6 +2702,7 @@
} }
} catch (e) { } catch (e) {
console.error('加载模拟持仓失败', e); console.error('加载模拟持仓失败', e);
this.showToast('加载模拟持仓失败', 'error');
} }
}, },
@@ -2703,6 +2714,7 @@
} }
} catch (e) { } catch (e) {
console.error('加载模拟交易记录失败', e); console.error('加载模拟交易记录失败', e);
this.showToast('加载交易记录失败', 'error');
} }
}, },
@@ -2717,6 +2729,7 @@
} }
} catch (e) { } catch (e) {
console.error('加载算法配置失败', e); console.error('加载算法配置失败', e);
this.showToast('加载算法配置失败', 'error');
} }
}, },
@@ -2728,6 +2741,7 @@
} }
} catch (e) { } catch (e) {
console.error('加载算法模板失败', e); console.error('加载算法模板失败', e);
this.showToast('加载算法模板失败', 'error');
} }
}, },
@@ -2739,6 +2753,7 @@
} }
} catch (e) { } catch (e) {
console.error('加载信号日志失败', e); console.error('加载信号日志失败', e);
this.showToast('加载信号日志失败', 'error');
} }
}, },
@@ -2750,6 +2765,7 @@
} }
} catch (e) { } catch (e) {
console.error('加载持仓元数据失败', e); console.error('加载持仓元数据失败', e);
this.showToast('加载持仓元数据失败', 'error');
} }
}, },
+10 -8
View File
@@ -22,6 +22,8 @@
--danger: #ff4444; --danger: #ff4444;
--warning: #fbbf24; --warning: #fbbf24;
--info: #3b82f6; --info: #3b82f6;
--info-light: #60a5fa;
--purple: #a855f7;
} }
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { body {
@@ -60,11 +62,11 @@
.stat-card { background: var(--bg-glass); border-radius: 14px; padding: 14px; display: flex; align-items: center; gap: 12px; } .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 { 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 svg { width: 18px; height: 18px; }
.stat-icon.blue { background: rgba(59,130,246,0.15); color: #3b82f6; } .stat-icon.blue { background: rgba(59,130,246,0.15); color: var(--info); }
.stat-icon.green { background: rgba(0,255,136,0.15); color: #00ff88; } .stat-icon.green { background: rgba(0,255,136,0.15); color: var(--success); }
.stat-icon.orange { background: rgba(255,170,0,0.15); color: #ffaa00; } .stat-icon.orange { background: rgba(255,170,0,0.15); color: var(--warning); }
.stat-icon.red { background: rgba(255,68,68,0.15); color: #ff4444; } .stat-icon.red { background: rgba(255,68,68,0.15); color: var(--danger); }
.stat-icon.purple { background: rgba(168,85,247,0.15); color: #a855f7; } .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-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; } .stat-info .stat-lbl { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
@@ -338,7 +340,7 @@
</div> </div>
<div class="sys-info-row"> <div class="sys-info-row">
<span class="lbl">🌐 外网IP</span> <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>
<div class="sys-info-row"> <div class="sys-info-row">
<span class="lbl">🏠 内网IP</span> <span class="lbl">🏠 内网IP</span>
@@ -346,11 +348,11 @@
</div> </div>
<div class="sys-info-row"> <div class="sys-info-row">
<span class="lbl">🔌 应用端口</span> <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>
<div class="sys-info-row"> <div class="sys-info-row">
<span class="lbl">🌍 访问域名</span> <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>
<div class="sys-info-row"> <div class="sys-info-row">
<span class="lbl">🖥️ 主机名</span> <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>"> <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://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.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/components.css?v=20260718v1">
<link rel="stylesheet" href="/static/css/auth.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/pages.css?v=20260719v2">
<link rel="stylesheet" href="/static/css/scan.css?v=20260718v1"> <link rel="stylesheet" href="/static/css/scan.css?v=20260719v3">
<link rel="stylesheet" href="/static/css/responsive.css?v=20260718v1"> <link rel="stylesheet" href="/static/css/responsive.css?v=20260719v1">
</head> </head>
<body> <body>
{% raw %} {% raw %}
@@ -47,8 +47,8 @@
<div class="confirm-dialog"> <div class="confirm-dialog">
<div class="confirm-message">{{ confirmMessage }}</div> <div class="confirm-message">{{ confirmMessage }}</div>
<div class="confirm-buttons"> <div class="confirm-buttons">
<button class="confirm-btn cancel" @click="handleConfirmCancel">取消</button> <button type="button" class="confirm-btn cancel" @click="handleConfirmCancel">取消</button>
<button class="confirm-btn ok" @click="handleConfirmOk">确定</button> <button type="button" class="confirm-btn ok" @click="handleConfirmOk">确定</button>
</div> </div>
</div> </div>
</div> </div>
@@ -65,7 +65,7 @@
</span> </span>
{{ smartResultData?.title || '执行结果' }} {{ smartResultData?.title || '执行结果' }}
</div> </div>
<button class="modal-close-btn" @click="smartResultVisible = false">×</button> <button type="button" class="modal-close-btn" @click="smartResultVisible = false">×</button>
</div> </div>
<div class="smart-result-body" v-if="smartResultData"> <div class="smart-result-body" v-if="smartResultData">
<!-- 算法标识 --> <!-- 算法标识 -->
@@ -105,7 +105,7 @@
</div> </div>
</div> </div>
<div class="smart-result-footer"> <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> </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 v-if="showLoginModal" class="modal-overlay" @click.self="showLoginModal = false" role="dialog" aria-modal="true" aria-label="登录">
<div class="login-modal"> <div class="login-modal">
<div class="login-tabs"> <div class="login-tabs">
<button :class="['login-tab', { active: !isRegister }]" @click="isRegister = false; loginError = ''">登录</button> <button type="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 = true; loginError = ''">注册</button>
<button class="modal-close-btn" @click="showLoginModal = false">×</button> <button type="button" class="modal-close-btn" @click="showLoginModal = false">×</button>
</div> </div>
<div class="login-body"> <div class="login-body">
<div class="login-field"> <div class="login-field">
@@ -130,7 +130,7 @@
<input v-model="loginForm.password" type="password" :placeholder="isRegister ? '设置密码(至少6位)' : '请输入密码'" @keyup.enter="handleLogin"> <input v-model="loginForm.password" type="password" :placeholder="isRegister ? '设置密码(至少6位)' : '请输入密码'" @keyup.enter="handleLogin">
</div> </div>
<div v-if="loginError" class="login-error">{{ loginError }}</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 ? '注册' : '登录') }} {{ loginLoading ? '处理中...' : (isRegister ? '注册' : '登录') }}
</button> </button>
</div> </div>
@@ -160,8 +160,8 @@
</div> </div>
<div class="login-modal login-modal-inline"> <div class="login-modal login-modal-inline">
<div class="login-tabs"> <div class="login-tabs">
<button :class="['login-tab', { active: !isRegister }]" @click="isRegister = false; loginError = ''">登录</button> <button type="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 = true; loginError = ''">注册</button>
</div> </div>
<div class="login-body"> <div class="login-body">
<div class="login-field"> <div class="login-field">
@@ -173,7 +173,7 @@
<input v-model="loginForm.password" type="password" :placeholder="isRegister ? '设置密码(至少6位)' : '请输入密码'" @keyup.enter="handleLogin"> <input v-model="loginForm.password" type="password" :placeholder="isRegister ? '设置密码(至少6位)' : '请输入密码'" @keyup.enter="handleLogin">
</div> </div>
<div v-if="loginError" class="login-error">{{ loginError }}</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 ? '注册' : '登录') }} {{ loginLoading ? '处理中...' : (isRegister ? '注册' : '登录') }}
</button> </button>
</div> </div>
@@ -194,16 +194,16 @@
</div> </div>
<div class="user-info"> <div class="user-info">
<span class="username" @click="showPasswordModal = true" class="cursor-pointer">{{ currentUser?.username }}</span> <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> </div>
<!-- 顶部导航 --> <!-- 顶部导航 -->
<div class="nav-tabs nav-tabs-compact" role="tablist"> <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 type="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 type="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 type="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 === 'model' }]" role="tab" :aria-selected="activeTab === 'model'" @click="activeTab = 'model'">模型</button>
</div> </div>
<!-- 提醒页面 --> <!-- 提醒页面 -->
@@ -226,26 +226,26 @@
<span class="summary-label">建议卖出</span> <span class="summary-label">建议卖出</span>
<span class="summary-value">{{ sellAlerts.length + holdingAlerts.filter(a => a.signalType === 'sell').length }}</span> <span class="summary-value">{{ sellAlerts.length + holdingAlerts.filter(a => a.signalType === 'sell').length }}</span>
</div> </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}` : '加载中...') : '刷新' }} {{ alertsLoading ? (alertsProgress.total > 0 ? `${alertsProgress.current}/${alertsProgress.total}` : '加载中...') : '刷新' }}
</button> </button>
</div> </div>
<!-- 子导航 --> <!-- 子导航 -->
<div class="sub-tabs"> <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> 持有 <span v-if="holdingStocks.length" class="sub-badge">{{ holdingStocks.length }}</span>
</button> </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> 关注 <span v-if="watchingStocks.length" class="sub-badge">{{ watchingStocks.length }}</span>
</button> </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>
<!-- 添加关注输入框 --> <!-- 添加关注输入框 -->
<div v-if="showAddWatch" class="add-watch-input"> <div v-if="showAddWatch" class="add-watch-input">
<input v-model="newWatchCode" type="text" placeholder="输入股票代码" maxlength="6" @keyup.enter="addToWatch"> <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>
<div v-if="alertsLoading" class="loading"> <div v-if="alertsLoading" class="loading">
@@ -285,7 +285,7 @@
<div class="alert-bottom"> <div class="alert-bottom">
<div class="alert-desc">{{ alert.reason }}</div> <div class="alert-desc">{{ alert.reason }}</div>
<span class="recommend-rate sell">{{ alert.recommendRate }}%</span> <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> </div>
</div> </div>
@@ -318,7 +318,7 @@
<div class="alert-bottom"> <div class="alert-bottom">
<div class="alert-desc">{{ alert.reason }}</div> <div class="alert-desc">{{ alert.reason }}</div>
<span class="recommend-rate" :class="alert.recommendText === '加仓' ? 'buy' : 'hold'">{{ alert.recommendRate }}%</span> <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> </div>
</div> </div>
@@ -387,7 +387,7 @@
<div class="alert-desc">{{ alert.reason }}</div> <div class="alert-desc">{{ alert.reason }}</div>
<div class="alert-desc holding-note" v-if="alert.holdingNote">{{ alert.holdingNote }}</div> <div class="alert-desc holding-note" v-if="alert.holdingNote">{{ alert.holdingNote }}</div>
<span class="recommend-rate buy">{{ alert.recommendRate }}%</span> <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> </div>
</div> </div>
@@ -419,7 +419,7 @@
<div class="alert-bottom"> <div class="alert-bottom">
<div class="alert-desc">{{ alert.reason }}</div> <div class="alert-desc">{{ alert.reason }}</div>
<span class="recommend-rate sell">{{ alert.recommendRate }}%</span> <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> </div>
</div> </div>
@@ -461,20 +461,20 @@
<div class="fundamental-header"> <div class="fundamental-header">
<h3>{{ fundamentalData?.stock_name || '股票' }} 基本面</h3> <h3>{{ fundamentalData?.stock_name || '股票' }} 基本面</h3>
<span class="fundamental-code">{{ fundamentalData?.stock_code }}</span> <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>
<div v-if="fundamentalData" class="fundamental-actions"> <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" class="watch-btn add"
@click="addToWatchFromFundamental"> @click="addToWatchFromFundamental">
★ 添加关注 ★ 添加关注
</button> </button>
<button v-else <button type="button" v-else
class="watch-btn remove" class="watch-btn remove"
@click="removeFromWatchFromFundamental"> @click="removeFromWatchFromFundamental">
☆ 取消关注 ☆ 取消关注
</button> </button>
<button class="ai-analyze-btn" <button type="button" class="ai-analyze-btn"
@click="aiAnalyze(fundamentalData.stock_code, fundamentalData.stock_name)" @click="aiAnalyze(fundamentalData.stock_code, fundamentalData.stock_name)"
:disabled="aiAnalyzing"> :disabled="aiAnalyzing">
{{ aiAnalyzing ? '分析中...' : 'AI分析' }} {{ aiAnalyzing ? '分析中...' : 'AI分析' }}
@@ -503,7 +503,7 @@
<span class="loading-dot"></span> AI正在分析中... <span class="loading-dot"></span> AI正在分析中...
</div> </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>
<div v-if="fundamentalLoading" class="fundamental-loading"> <div v-if="fundamentalLoading" class="fundamental-loading">
@@ -641,10 +641,10 @@
<div class="kline-header"> <div class="kline-header">
<h4>📈 股价走势</h4> <h4>📈 股价走势</h4>
<div class="kline-period-tabs"> <div class="kline-period-tabs">
<button :class="['kline-tab', { active: klinePeriod === 'weekly' }]" @click="changeKlinePeriod('weekly')"></button> <button type="button" :class="['kline-tab', { active: klinePeriod === 'weekly' }]" @click="changeKlinePeriod('weekly')"></button>
<button :class="['kline-tab', { active: klinePeriod === 'monthly' }]" @click="changeKlinePeriod('monthly')"></button> <button type="button" :class="['kline-tab', { active: klinePeriod === 'monthly' }]" @click="changeKlinePeriod('monthly')"></button>
<button :class="['kline-tab', { active: klinePeriod === 'quarterly' }]" @click="changeKlinePeriod('quarterly')"></button> <button type="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 === 'yearly' }]" @click="changeKlinePeriod('yearly')"></button>
</div> </div>
</div> </div>
<div class="kline-chart-container" :class="{ loading: klineLoading }"> <div class="kline-chart-container" :class="{ loading: klineLoading }">
@@ -665,13 +665,13 @@
<!-- 分析页子导航 --> <!-- 分析页子导航 -->
<div class="sub-tabs"> <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>
<button :class="['sub-tab', { active: analysisSubTab === 'bull' }]" @click="analysisSubTab = 'bull'"> <button type="button" :class="['sub-tab', { active: analysisSubTab === 'bull' }]" @click="analysisSubTab = 'bull'">
个股深析 个股深析
</button> </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> </button>
</div> </div>
@@ -819,10 +819,10 @@
<div class="tech-signal-header" style="margin-bottom:8px;"> <div class="tech-signal-header" style="margin-bottom:8px;">
<div class="tech-input-row"> <div class="tech-input-row">
<input v-model="techSignalCode" placeholder="输入股票代码" class="tech-input" @keyup.enter="fetchTechSignals()"> <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 ? '检测中...' : '检测信号' }} {{ techLoading ? '检测中...' : '检测信号' }}
</button> </button>
<button class="tech-btn batch" @click="batchTechSignals" :disabled="techLoading"> <button type="button" class="tech-btn batch" @click="batchTechSignals" :disabled="techLoading">
批量扫描关注 批量扫描关注
</button> </button>
</div> </div>
@@ -850,7 +850,7 @@
<div class="scan-summary-header" role="button" tabindex="0" :aria-expanded="!scanSummaryCollapsed" @click="scanSummaryCollapsed = !scanSummaryCollapsed" @keyup.enter="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-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"> <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 class="scan-summary-toggle">{{ scanSummaryCollapsed ? '▶' : '▼' }}</span>
</span> </span>
</div> </div>
@@ -917,9 +917,9 @@
</div> </div>
</div> </div>
<div v-if="fullScanTotalPages > 1" class="scan-pagination"> <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> <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> </div>
</template> </template>
</div> </div>
@@ -932,14 +932,14 @@
🔔 {{ (techSignalResult.signal_status || []).filter(s => s.triggered).length }} 个信号 🔔 {{ (techSignalResult.signal_status || []).filter(s => s.triggered).length }} 个信号
</span> </span>
<span class="signal-count none" v-else-if="!techSignalResult.has_scan_data">无信号</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>
<div class="tech-result-actions"> <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"> class="watch-btn add" @click="addToWatchFromSignal">
★ 添加关注 ★ 添加关注
</button> </button>
<button v-else class="watch-btn remove" @click="removeFromWatchFromSignal"> <button type="button" v-else class="watch-btn remove" @click="removeFromWatchFromSignal">
☆ 取消关注 ☆ 取消关注
</button> </button>
</div> </div>
@@ -1054,7 +1054,7 @@
</div> </div>
</template> </template>
<div v-if="(techSignalResult.signals || []).length > 0" class="tech-signal-list"> <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" <div v-for="sig in techSignalResult.signals" :key="sig.date + sig.type"
:class="['tech-signal-item', 'strength-' + sig.strength]"> :class="['tech-signal-item', 'strength-' + sig.strength]">
<div class="sig-header"> <div class="sig-header">
@@ -1072,7 +1072,7 @@
<div v-if="techBatchResults.length > 0" class="tech-batch-results"> <div v-if="techBatchResults.length > 0" class="tech-batch-results">
<div class="batch-results-header"> <div class="batch-results-header">
<h4 class="section-title">关注股票信号扫描 <span class="badge">{{ techBatchResults.length }}</span></h4> <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>
<div class="full-scan-list"> <div class="full-scan-list">
<div v-for="item in techBatchResults" :key="item.code" class="scan-result-card" <div v-for="item in techBatchResults" :key="item.code" class="scan-result-card"
@@ -1113,7 +1113,7 @@
<div class="deep-input-row"> <div class="deep-input-row">
<input v-model="deepCode" placeholder="输入股票代码,如 601609" <input v-model="deepCode" placeholder="输入股票代码,如 601609"
class="deep-code-input" @keyup.enter="fetchDeepAnalysis()" maxlength="6"> 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 ? '分析中...' : '深度分析' }} {{ deepLoading ? '分析中...' : '深度分析' }}
</button> </button>
</div> </div>
@@ -1310,7 +1310,7 @@
<div class="buy-analysis-header"> <div class="buy-analysis-header">
<div class="buy-analysis-title">买入股深度分析</div> <div class="buy-analysis-title">买入股深度分析</div>
<div class="buy-analysis-desc">对全景扫描中推荐买入的股票批量执行深度分析</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 ? '分析中...' : '刷新分析' }} {{ buyAnalysisLoading ? '分析中...' : '刷新分析' }}
</button> </button>
</div> </div>
@@ -1373,7 +1373,7 @@
<div class="buy-detail-grid"> <div class="buy-detail-grid">
<div class="buy-detail-item"> <div class="buy-detail-item">
<span class="buy-detail-label">系统推荐</span> <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>
<div class="buy-detail-item"> <div class="buy-detail-item">
<span class="buy-detail-label">推荐理由</span> <span class="buy-detail-label">推荐理由</span>
@@ -1402,7 +1402,7 @@
<span class="buy-detail-value">{{ item.ma_trend_label }}</span> <span class="buy-detail-value">{{ item.ma_trend_label }}</span>
</div> </div>
</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();"> @click="deepCode = item.stock_code; analysisSubTab = 'bull'; fetchDeepAnalysis();">
查看完整报告 → 查看完整报告 →
</button> </button>
@@ -1416,12 +1416,12 @@
<!-- 模拟交易统计卡片 --> <!-- 模拟交易统计卡片 -->
<div class="sim-stats-card"> <div class="sim-stats-card">
<div class="sim-stats-header"> <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"> <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 ? '执行中...' : '执行' }} {{ simAutoTrading ? '执行中...' : '执行' }}
</button> </button>
<button class="sim-btn reset" @click="confirmResetSim">重置</button> <button type="button" class="sim-btn reset" @click="confirmResetSim">重置</button>
</div> </div>
</div> </div>
<div class="sim-stats-grid"> <div class="sim-stats-grid">
@@ -1457,18 +1457,18 @@
<span :class="(simStats.realized_profit || 0) >= 0 ? 'profit' : 'loss'"> <span :class="(simStats.realized_profit || 0) >= 0 ? 'profit' : 'loss'">
已实现 {{ (simStats.realized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.realized_profit || 0) }} 已实现 {{ (simStats.realized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.realized_profit || 0) }}
</span> </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 ? '刷新中...' : '刷新现价' }} {{ simPriceRefreshing ? '刷新中...' : '刷新现价' }}
</button> </button>
</div> </div>
<!-- 模拟 vs 实盘 对比看板 --> <!-- 模拟 vs 实盘 对比看板 -->
<div class="sim-compare-panel" v-if="simStats.total_fees > 0 || simStats.total_trades > 0"> <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"> <div class="sim-compare-header" @click="compareExpanded = !compareExpanded" class="cursor-pointer">
<span style="font-size:12px;font-weight:600;color:#aaa;"> <span style="font-size:12px;font-weight:600;color:var(--text-secondary);">
<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> <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 实盘对比 模拟 vs 实盘对比
</span> </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>
<div v-show="compareExpanded" class="sim-compare-body"> <div v-show="compareExpanded" class="sim-compare-body">
<div class="sim-compare-row"> <div class="sim-compare-row">
@@ -1489,14 +1489,14 @@
</div> </div>
</div> </div>
<div class="sim-compare-fees"> <div class="sim-compare-fees">
<span>总手续费: <b style="color:#ff6b6b;">-{{ formatMoney(simStats.total_fees || 0) }}</b></span> <span>总手续费: <b style="color:var(--up);">-{{ formatMoney(simStats.total_fees || 0) }}</b></span>
<span style="color:#666;font-size:11px;"> <span style="color:var(--text-dim);font-size:11px;">
佣金 ¥{{ (simStats.total_commission || 0).toFixed(2) }} + 佣金 ¥{{ (simStats.total_commission || 0).toFixed(2) }} +
印花税 ¥{{ (simStats.total_stamp_tax || 0).toFixed(2) }} 印花税 ¥{{ (simStats.total_stamp_tax || 0).toFixed(2) }}
</span> </span>
</div> </div>
<div class="sim-compare-tip" v-if="simStats.total_fees > 0"> <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.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' }} | 每笔均费 ¥{{ simStats.total_trades > 0 ? (simStats.total_fees / simStats.total_trades).toFixed(2) : '0.00' }}
</span> </span>
@@ -1508,16 +1508,16 @@
<!-- 算法配置卡片 --> <!-- 算法配置卡片 -->
<div class="sim-rule-card collapsible"> <div class="sim-rule-card collapsible">
<div class="sim-rule-header" @click="smartConfigExpanded = !smartConfigExpanded"> <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> <span :class="['toggle-icon', { expanded: smartConfigExpanded }]"></span>
</div> </div>
<div v-show="smartConfigExpanded" class="padding-y-sm"> <div v-show="smartConfigExpanded" class="padding-y-sm">
<!-- 模板选择 --> <!-- 模板选择 -->
<div v-if="smartTemplates.length > 0" style="margin-bottom:12px;"> <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"> <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="sim-btn"
:class="{'auto': smartAlgoConfig?.algo_name === tpl.display_name}" :class="{'auto': smartAlgoConfig?.algo_name === tpl.display_name}"
@click="applyAlgoTemplate(tpl.name)" @click="applyAlgoTemplate(tpl.name)"
@@ -1529,18 +1529,18 @@
</div> </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;"> <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:var(--teal);">{{ smartAlgoConfig.take_profit_pct }}%</strong></span>
<span>止损: <strong style="color:#ff6b6b;">{{ smartAlgoConfig.stop_loss_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.position_pct }}%</strong> <span v-if="smartAlgoConfig.signal_weight">+信号加权</span></span>
<span>最长持仓: <strong>{{ smartAlgoConfig.max_hold_days }}</strong></span> <span>最长持仓: <strong>{{ smartAlgoConfig.max_hold_days }}</strong></span>
<span>部分止盈: <strong>{{ smartAlgoConfig.partial_exit_pct }}%</strong></span> <span>部分止盈: <strong>{{ smartAlgoConfig.partial_exit_pct }}%</strong></span>
<span>跟踪回撤: <strong>{{ smartAlgoConfig.momentum_trail_gap }}%</strong></span> <span>跟踪回撤: <strong>{{ smartAlgoConfig.momentum_trail_gap }}%</strong></span>
<span>保本触发: <strong>{{ smartAlgoConfig.breakeven_at > 0 ? smartAlgoConfig.breakeven_at + '%' : '关闭' }}</strong></span> <span>保本触发: <strong>{{ smartAlgoConfig.breakeven_at > 0 ? smartAlgoConfig.breakeven_at + '%' : '关闭' }}</strong></span>
<span>卖出确认: <strong>{{ smartAlgoConfig.sell_confirm_days }}</strong></span> <span>卖出确认: <strong>{{ smartAlgoConfig.sell_confirm_days }}</strong></span>
<span>买入时点: <strong style="color:#4ecdc4;">{{ smartAlgoConfig.buy_time }}</strong></span> <span>买入时点: <strong style="color:var(--accent-cyan);">{{ smartAlgoConfig.buy_time }}</strong></span>
<span>卖出时点: <strong style="color:#4ecdc4;">{{ smartAlgoConfig.sell_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.ignore_sell_signal ? '是' : '否' }}</strong></span>
<span>连涨不平仓: <strong>{{ smartAlgoConfig.no_timeout_if_rising ? '是' : '否' }}</strong></span> <span>连涨不平仓: <strong>{{ smartAlgoConfig.no_timeout_if_rising ? '是' : '否' }}</strong></span>
</div> </div>
@@ -1582,19 +1582,19 @@
</label> </label>
</div> </div>
<div style="margin-top:8px;display:flex;gap:8px;"> <div style="margin-top:8px;display:flex;gap:8px;">
<button class="sim-btn auto" @click="saveSmartConfig" style="font-size:12px;">保存配置</button> <button type="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" @click="smartConfigEditing = false; loadSmartAlgoConfig();" style="font-size:12px;">取消</button>
</div> </div>
</div> </div>
<div v-else style="margin-top:6px;"> <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> </div>
<!-- 自动执行时间说明 --> <!-- 自动执行时间说明 -->
<div style="margin-top:8px;font-size:11px;color:#666;border-top:1px solid #222;padding-top:6px;"> <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:#4ecdc4;">{{ smartAlgoConfig?.buy_time || '09:35' }}</strong> 买入 / ⏰ 每个交易日 <strong style="color:var(--accent-cyan);">{{ smartAlgoConfig?.buy_time || '09:35' }}</strong> 买入 /
<strong style="color:#4ecdc4;">{{ smartAlgoConfig?.sell_time || '13:40' }}</strong> 卖出 / <strong style="color:var(--accent-cyan);">{{ smartAlgoConfig?.sell_time || '13:40' }}</strong> 卖出 /
<strong>15:05</strong> 收盘更新 — 基于v7回测最优时点 <strong>15:05</strong> 收盘更新 — 基于v7回测最优时点
</div> </div>
</div> </div>
@@ -1603,7 +1603,7 @@
<!-- 今日交易 --> <!-- 今日交易 -->
<div class="sim-section collapsible-section"> <div class="sim-section collapsible-section">
<div class="sim-section-header" @click="todayTradesExpanded = !todayTradesExpanded" class="cursor-pointer"> <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> <span :class="['toggle-icon', { expanded: todayTradesExpanded }]" class="text-muted-2"></span>
</div> </div>
<div v-show="todayTradesExpanded && todayTrades.length === 0" class="empty-tip">今日暂无交易</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>¥{{ trade.price?.toFixed(2) }} × {{ trade.quantity }}股</span>
<span class="sim-trade-amount">{{ formatMoney(trade.price * trade.quantity) }}</span> <span class="sim-trade-amount">{{ formatMoney(trade.price * trade.quantity) }}</span>
</div> </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) }} 手续费 ¥{{ trade.total_fee?.toFixed(2) }}
</div> </div>
<div class="sim-trade-reason" v-if="trade.signal_reason">{{ trade.signal_reason }}</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 collapsible-section">
<div class="sim-section-header" @click="simPositionsExpanded = !simPositionsExpanded" class="cursor-pointer"> <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> <span :class="['toggle-icon', { expanded: simPositionsExpanded }]" class="text-muted-2"></span>
</div> </div>
<div v-show="simPositionsExpanded && simPositions.length === 0" class="empty-tip">暂无持仓</div> <div v-show="simPositionsExpanded && simPositions.length === 0" class="empty-tip">暂无持仓</div>
@@ -1655,16 +1655,16 @@
</div> </div>
<!-- 算法状态标签 --> <!-- 算法状态标签 -->
<div v-if="getActiveRules(pos.stock_code).length > 0 || getPositionMeta(pos.stock_code)" <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" <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> 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:#666;"> <span v-if="getPositionMeta(pos.stock_code)?.days_held" style="color:var(--text-dim);">
持{{ getPositionMeta(pos.stock_code).days_held }}天 持{{ getPositionMeta(pos.stock_code).days_held }}天
</span> </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 }}天 连涨{{ getPositionMeta(pos.stock_code).consecutive_up_days }}天
</span> </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 }}天 卖信号{{ getPositionMeta(pos.stock_code).consecutive_sell_signals }}天
</span> </span>
</div> </div>
@@ -1675,32 +1675,32 @@
<!-- 信号日志(可折叠) --> <!-- 信号日志(可折叠) -->
<div class="sim-section collapsible-section"> <div class="sim-section collapsible-section">
<div class="sim-section-header" @click="smartSignalExpanded = !smartSignalExpanded" class="cursor-pointer"> <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> <span :class="['toggle-icon', { expanded: smartSignalExpanded }]" class="text-muted-2"></span>
</div> </div>
<div v-show="smartSignalExpanded"> <div v-show="smartSignalExpanded">
<div v-if="smartSignals.length === 0" class="empty-tip">暂无信号记录</div> <div v-if="smartSignals.length === 0" class="empty-tip">暂无信号记录</div>
<div v-else class="max-height-scroll"> <div v-else class="max-height-scroll">
<div v-for="sig in smartSignals" :key="sig.id" <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="{ <span :style="{
color: sig.action === 'buy' ? '#ff6b6b' : color: sig.action === 'buy' ? 'var(--up)' :
sig.action === 'sell' ? '#00ce9e' : sig.action === 'sell' ? 'var(--teal)' :
sig.action === 'partial_sell' ? '#ffd93d' : '#888', sig.action === 'partial_sell' ? 'var(--hold)' : 'var(--text-muted)',
fontWeight: 600, minWidth: '48px' fontWeight: 600, minWidth: '48px'
}"> }">
{{ sig.action === 'buy' ? '买入' : sig.action === 'sell' ? '卖出' : {{ sig.action === 'buy' ? '买入' : sig.action === 'sell' ? '卖出' :
sig.action === 'partial_sell' ? '减仓' : sig.action === 'hold' ? '持有' : sig.action }} sig.action === 'partial_sell' ? '减仓' : sig.action === 'hold' ? '持有' : sig.action }}
</span> </span>
<span style="color:#ccc;flex:1;"> <span style="color:var(--text-secondary);flex:1;">
{{ sig.stock_code }} {{ 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 }} {{ 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) }}%) ({{ sig.profit_pct >= 0 ? '+' : '' }}{{ sig.profit_pct?.toFixed(1) }}%)
</span> </span>
</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> </div>
</div> </div>
@@ -1709,7 +1709,7 @@
<!-- 模拟交易记录 --> <!-- 模拟交易记录 -->
<div class="sim-section collapsible-section"> <div class="sim-section collapsible-section">
<div class="sim-section-header" @click="simTradesExpanded = !simTradesExpanded" class="cursor-pointer"> <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> <span :class="['toggle-icon', { expanded: simTradesExpanded }]" class="text-muted-2"></span>
</div> </div>
<div v-show="simTradesExpanded && simTrades.length === 0" class="empty-tip">暂无交易记录</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>¥{{ trade.price?.toFixed(2) }} × {{ trade.quantity }}股</span>
<span class="sim-trade-amount">{{ formatMoney(trade.price * trade.quantity) }}</span> <span class="sim-trade-amount">{{ formatMoney(trade.price * trade.quantity) }}</span>
</div> </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) }} 手续费 ¥{{ 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>
<div class="sim-trade-reason" v-if="trade.signal_reason">{{ trade.signal_reason }}</div> <div class="sim-trade-reason" v-if="trade.signal_reason">{{ trade.signal_reason }}</div>
<div class="sim-trade-date">{{ trade.trade_date }} {{ trade.trade_time?.substring(0,5) }}</div> <div 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 v-show="activeTab === 'model'" class="model-page">
<!-- 模型页子导航 --> <!-- 模型页子导航 -->
<div class="sub-tabs"> <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>
<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> </button>
</div> </div>
@@ -1875,12 +1875,12 @@
<!-- 模拟交易统计卡片 --> <!-- 模拟交易统计卡片 -->
<div class="sim-stats-card"> <div class="sim-stats-card">
<div class="sim-stats-header"> <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"> <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 ? '执行中...' : '执行' }} {{ simAutoTrading ? '执行中...' : '执行' }}
</button> </button>
<button class="sim-btn reset" @click="confirmResetSim">重置</button> <button type="button" class="sim-btn reset" @click="confirmResetSim">重置</button>
</div> </div>
</div> </div>
<div class="sim-stats-grid"> <div class="sim-stats-grid">
@@ -1916,7 +1916,7 @@
<span :class="(simStats.realized_profit || 0) >= 0 ? 'profit' : 'loss'"> <span :class="(simStats.realized_profit || 0) >= 0 ? 'profit' : 'loss'">
已实现 {{ (simStats.realized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.realized_profit || 0) }} 已实现 {{ (simStats.realized_profit || 0) >= 0 ? '+' : '' }}{{ formatMoney(simStats.realized_profit || 0) }}
</span> </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 ? '刷新中...' : '刷新现价' }} {{ simPriceRefreshing ? '刷新中...' : '刷新现价' }}
</button> </button>
</div> </div>
@@ -1982,13 +1982,13 @@
<span class="cash-label">可用资金</span> <span class="cash-label">可用资金</span>
<template v-if="!editingCash"> <template v-if="!editingCash">
<span class="cash-value">{{ formatMoney(availableCash) }}</span> <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>
<template v-else> <template v-else>
<input type="number" ref="cashInput" v-model="cashInputValue" class="cash-input" <input type="number" ref="cashInput" v-model="cashInputValue" class="cash-input"
@keyup.enter="saveCash" @keyup.esc="cancelEditCash" placeholder="输入金额"> @keyup.enter="saveCash" @keyup.esc="cancelEditCash" placeholder="输入金额">
<button class="cash-save-btn" @click="saveCash"></button> <button type="button" class="cash-save-btn" @click="saveCash"></button>
<button class="cash-cancel-btn" @click="cancelEditCash"></button> <button type="button" class="cash-cancel-btn" @click="cancelEditCash"></button>
</template> </template>
</div> </div>
<!-- 总资产 = 持仓市值 + 可用资金 --> <!-- 总资产 = 持仓市值 + 可用资金 -->
@@ -2016,17 +2016,17 @@
<span v-if="alert.stop_loss_price">止损 ¥{{ alert.stop_loss_price }}</span> <span v-if="alert.stop_loss_price">止损 ¥{{ alert.stop_loss_price }}</span>
</div> </div>
<div class="stoploss-alert-message">{{ alert.message }}</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>
</div> </div>
<!-- 操作按钮区 --> <!-- 操作按钮区 -->
<div class="trade-actions-bar"> <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 ? '请先分析股票' : '记录交易' }} {{ allStocksForTrade.length === 0 ? '请先分析股票' : '记录交易' }}
</button> </button>
<button class="refresh-price-btn" @click="refreshHoldingPrices" :disabled="priceRefreshing"> <button type="button" class="refresh-price-btn" @click="refreshHoldingPrices" :disabled="priceRefreshing">
{{ priceRefreshing ? '刷新中...' : '刷新价格' }} {{ priceRefreshing ? '刷新中...' : '刷新价格' }}
</button> </button>
</div> </div>
@@ -2100,10 +2100,10 @@
<!-- 持有/清仓 子Tab --> <!-- 持有/清仓 子Tab -->
<div class="trade-sub-tabs"> <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> 持有 <span class="tab-count">{{ holdingGroupCount }}</span>
</button> </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> 清仓 <span class="tab-count">{{ clearedGroupCount }}</span>
</button> </button>
</div> </div>
@@ -2118,7 +2118,7 @@
<div v-for="group in filteredGroupedTrades" :key="group.code" class="trade-group"> <div v-for="group in filteredGroupedTrades" :key="group.code" class="trade-group">
<!-- 股票分组标题 --> <!-- 股票分组标题 -->
<div class="trade-group-header"> <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] ? '▼' : '▶' }} {{ expandedTradeGroups[group.code] ? '▼' : '▶' }}
</button> </button>
<div class="trade-group-content" role="button" tabindex="0" @click="loadFundamental(group.code, group.name)" @keyup.enter="loadFundamental(group.code, group.name)"> <div class="trade-group-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 class="trade-date">{{ trade.trade_date }}</div>
</div> </div>
<div class="trade-actions-vertical"> <div class="trade-actions-vertical">
<button @click="editTrade(trade)">编辑</button> <button type="button" @click="editTrade(trade)">编辑</button>
<button @click="deleteTrade(trade.id)">删除</button> <button type="button" @click="deleteTrade(trade.id)">删除</button>
</div> </div>
</div> </div>
</div> </div>
@@ -2225,8 +2225,8 @@
<div v-if="passwordError" class="login-error">{{ passwordError }}</div> <div v-if="passwordError" class="login-error">{{ passwordError }}</div>
<div v-if="passwordSuccess" class="login-success">{{ passwordSuccess }}</div> <div v-if="passwordSuccess" class="login-success">{{ passwordSuccess }}</div>
<div class="password-buttons"> <div class="password-buttons">
<button class="btn-cancel" @click="showPasswordModal = false">关闭</button> <button type="button" class="btn-cancel" @click="showPasswordModal = false">关闭</button>
<button class="login-submit" @click="handleChangePassword" :disabled="passwordLoading"> <button type="button" class="login-submit" @click="handleChangePassword" :disabled="passwordLoading">
{{ passwordLoading ? '处理中...' : '确认修改' }} {{ passwordLoading ? '处理中...' : '确认修改' }}
</button> </button>
</div> </div>