feat: add trade reflection frontend sub-tabs and views

- Added subtabs to trade-review view: 统计分析 / 交易反思 / 经验库
- Implemented 交易反思 view with date selector, daily stats, daily reflection section, trade pair cards, unpaired trades list
- Implemented reflection edit modal, daily reflection modal, tag management modal, AI analysis result modal
- Implemented 经验库 view with search, filters, list/timeline/category views
- Added '进入反思' links in daily summary table to switch to reflection tab
- Reused existing CSS variables and Apple-style design
- Added frontend JS integration with /api/v1/trade-review endpoints
refactor3.0
Lxy 2 weeks ago
parent f85ae2bced
commit 476ea0930f

@ -582,12 +582,133 @@
.tr-empty-icon { font-size: 56px; opacity: 0.3; } .tr-empty-icon { font-size: 56px; opacity: 0.3; }
.tr-empty-text { font-size: 15px; color: var(--text-tertiary); } .tr-empty-text { font-size: 15px; color: var(--text-tertiary); }
/* ============================================
交易反思子系统样式
============================================ */
.tr-subtabs { display: flex; gap: 8px; margin-bottom: 24px; background: rgba(255,255,255,0.6); padding: 4px; border-radius: 14px; backdrop-filter: blur(8px); width: fit-content; }
.tr-subtab { padding: 8px 20px; border-radius: 10px; font-size: 14px; font-weight: 500; cursor: pointer; border: none; background: transparent; color: var(--text-secondary); transition: all 0.2s; font-family: inherit; }
.tr-subtab:hover { background: rgba(0,0,0,0.03); color: var(--text-primary); }
.tr-subtab.active { background: #fff; color: var(--color-brand); box-shadow: var(--shadow-sm); font-weight: 600; }
.tr-tab-panel { display: none; }
.tr-tab-panel.active { display: block; }
/* 交易反思 - 日期导航 */
.tr-refl-datebar { display: flex; align-items: center; justify-content: center; gap: 16px; margin-bottom: 24px; }
.tr-refl-datebtn { width: 36px; height: 36px; border-radius: 50%; border: none; background: #fff; color: var(--text-secondary); cursor: pointer; box-shadow: var(--shadow-sm); transition: all 0.2s; font-size: 16px; }
.tr-refl-datebtn:hover { background: var(--color-brand); color: #fff; }
.tr-refl-datetext { font-size: 20px; font-weight: 700; color: var(--text-primary); min-width: 120px; text-align: center; }
.tr-refl-actions { display: flex; gap: 10px; margin-left: auto; }
/* 反思统计卡片 */
.tr-refl-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
.tr-refl-statcard { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; text-align: center; box-shadow: var(--shadow-sm); transition: transform 0.2s; }
.tr-refl-statcard:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.tr-refl-statlabel { font-size: 12px; color: var(--text-tertiary); margin-bottom: 8px; font-weight: 600; }
.tr-refl-statvalue { font-size: 26px; font-weight: 700; }
.tr-refl-statvalue.profit { color: var(--color-down); }
.tr-refl-statvalue.loss { color: var(--color-up); }
.tr-refl-reflected { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 9999px; font-size: 12px; font-weight: 600; }
.tr-refl-reflected.yes { background: rgba(52,199,89,0.12); color: var(--color-down); }
.tr-refl-reflected.no { background: rgba(255,59,48,0.12); color: var(--color-up); }
/* 当日反思区域 */
.tr-refl-daily { background: var(--bg-card); border-radius: var(--radius-card); padding: 24px; margin-bottom: 24px; box-shadow: var(--shadow-sm); }
.tr-refl-daily-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.tr-refl-daily-title { font-size: 17px; font-weight: 700; color: var(--text-primary); }
.tr-refl-daily-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.tr-refl-daily-item { background: #F5F5F7; border-radius: 12px; padding: 14px; }
.tr-refl-daily-label { font-size: 11px; color: var(--text-tertiary); margin-bottom: 6px; font-weight: 600; text-transform: uppercase; }
.tr-refl-daily-value { font-size: 14px; color: var(--text-primary); font-weight: 500; line-height: 1.4; }
.tr-refl-daily-value.empty { color: var(--text-tertiary); font-style: italic; }
/* 交易配对卡片 */
.tr-refl-section-title { font-size: 17px; font-weight: 700; margin-bottom: 16px; color: var(--text-primary); }
.tr-refl-pairs { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; margin-bottom: 24px; }
.tr-refl-card { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; box-shadow: var(--shadow-sm); transition: transform 0.2s, box-shadow 0.2s; position: relative; overflow: hidden; }
.tr-refl-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.tr-refl-card::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--color-brand); }
.tr-refl-card.profit::before { background: var(--color-down); }
.tr-refl-card.loss::before { background: var(--color-up); }
.tr-refl-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
.tr-refl-card-symbol { font-size: 16px; font-weight: 700; }
.tr-refl-card-dir { font-size: 12px; padding: 3px 10px; border-radius: 6px; font-weight: 600; }
.tr-refl-card-dir.long { background: rgba(52,199,89,0.12); color: var(--color-down); }
.tr-refl-card-dir.short { background: rgba(255,59,48,0.12); color: var(--color-up); }
.tr-refl-card-pnl { font-size: 20px; font-weight: 700; text-align: right; }
.tr-refl-card-pnl.profit { color: var(--color-down); }
.tr-refl-card-pnl.loss { color: var(--color-up); }
.tr-refl-card-nodes { font-size: 13px; color: var(--text-secondary); margin-bottom: 14px; line-height: 1.6; }
.tr-refl-card-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.tr-refl-tag { font-size: 11px; padding: 4px 10px; border-radius: 9999px; background: #F5F5F7; color: var(--text-secondary); font-weight: 500; }
.tr-refl-tag.active { background: rgba(0,122,255,0.12); color: var(--color-brand); }
.tr-refl-card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.tr-refl-card-actions .tr-btn { padding: 6px 12px; font-size: 12px; }
/* 未配对交易 */
.tr-refl-unpaired { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; box-shadow: var(--shadow-sm); }
.tr-refl-unpaired-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.tr-refl-unpaired-table th { background: #F5F5F7; padding: 10px 8px; text-align: left; font-weight: 600; color: var(--text-tertiary); font-size: 12px; }
.tr-refl-unpaired-table td { padding: 10px 8px; border-bottom: 1px solid rgba(0,0,0,0.04); color: var(--text-primary); }
/* 经验库 */
.tr-exp-toolbar { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
.tr-exp-search { background: #fff; border: 1px solid rgba(0,0,0,0.05); border-radius: 12px; padding: 0 14px; height: 40px; font-size: 14px; width: 260px; outline: none; }
.tr-exp-search:focus { border-color: var(--color-brand); box-shadow: 0 0 0 3px rgba(0,122,255,0.15); }
.tr-exp-filter { background: #fff; border: 1px solid rgba(0,0,0,0.05); border-radius: 10px; padding: 0 12px; height: 36px; font-size: 13px; color: var(--text-primary); outline: none; }
.tr-exp-viewtabs { display: flex; gap: 6px; margin-left: auto; }
.tr-exp-viewtab { padding: 6px 14px; border-radius: 10px; font-size: 13px; cursor: pointer; border: none; background: #fff; color: var(--text-secondary); transition: all 0.2s; }
.tr-exp-viewtab:hover { background: #F5F5F7; }
.tr-exp-viewtab.active { background: var(--color-brand); color: #fff; }
.tr-exp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
.tr-exp-card { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; box-shadow: var(--shadow-sm); transition: all 0.2s; }
.tr-exp-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.tr-exp-card-title { font-size: 15px; font-weight: 700; margin-bottom: 10px; color: var(--text-primary); }
.tr-exp-card-meta { font-size: 12px; color: var(--text-tertiary); margin-bottom: 12px; }
.tr-exp-card-content { font-size: 13px; color: var(--text-secondary); line-height: 1.7; margin-bottom: 14px; }
.tr-exp-card-tags { display: flex; flex-wrap: wrap; gap: 6px; }
/* Modal 表单 */
.tr-form-group { margin-bottom: 18px; }
.tr-form-label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; }
.tr-form-input, .tr-form-select, .tr-form-textarea { width: 100%; background: #F5F5F7; border: 1px solid transparent; border-radius: 10px; padding: 10px 12px; font-size: 14px; color: var(--text-primary); font-family: inherit; outline: none; transition: all 0.2s; }
.tr-form-input:focus, .tr-form-select:focus, .tr-form-textarea:focus { border-color: var(--color-brand); background: #fff; box-shadow: 0 0 0 3px rgba(0,122,255,0.1); }
.tr-form-textarea { min-height: 90px; resize: vertical; }
.tr-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.tr-form-rating { display: flex; gap: 6px; }
.tr-form-star { font-size: 22px; color: #E5E5E7; cursor: pointer; transition: color 0.15s; }
.tr-form-star.active { color: #FF9F0A; }
.tr-form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; }
/* 标签管理 */
.tr-tag-section { margin-bottom: 18px; }
.tr-tag-section-title { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 10px; }
.tr-tag-pool { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.tr-tag-chip { padding: 6px 12px; border-radius: 9999px; font-size: 12px; cursor: pointer; border: 1px solid rgba(0,0,0,0.06); background: #fff; color: var(--text-secondary); transition: all 0.15s; }
.tr-tag-chip:hover { border-color: var(--color-brand); color: var(--color-brand); }
.tr-tag-chip.selected { background: rgba(0,122,255,0.12); border-color: var(--color-brand); color: var(--color-brand); font-weight: 500; }
.tr-tag-custom { display: flex; gap: 8px; margin-top: 12px; }
.tr-tag-custom .tr-form-input { flex: 1; }
/* AI 分析结果 */
.tr-ai-result-section { margin-bottom: 18px; }
.tr-ai-result-title { font-size: 14px; font-weight: 700; color: var(--color-brand); margin-bottom: 8px; }
.tr-ai-result-content { font-size: 14px; color: var(--text-primary); line-height: 1.8; background: #F5F5F7; border-radius: 12px; padding: 14px; }
.tr-ai-list { padding-left: 18px; margin: 0; }
.tr-ai-list li { margin-bottom: 6px; line-height: 1.6; }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.tr-stats-grid { grid-template-columns: repeat(3, 1fr); } .tr-stats-grid { grid-template-columns: repeat(3, 1fr); }
.tr-refl-stats { grid-template-columns: repeat(2, 1fr); }
.tr-refl-daily-grid { grid-template-columns: repeat(2, 1fr); }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.tr-stats-grid { grid-template-columns: repeat(2, 1fr); } .tr-stats-grid { grid-template-columns: repeat(2, 1fr); }
.tr-overview-grid { grid-template-columns: repeat(2, 1fr); } .tr-overview-grid { grid-template-columns: repeat(2, 1fr); }
.tr-refl-stats { grid-template-columns: 1fr; }
.tr-refl-daily-grid { grid-template-columns: 1fr; }
.tr-form-row { grid-template-columns: 1fr; }
.tr-refl-pairs { grid-template-columns: 1fr; }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -974,6 +1095,15 @@
<!-- 交易复盘视图 --> <!-- 交易复盘视图 -->
<div id="trade-review-view" class="view"> <div id="trade-review-view" class="view">
<!-- 子 Tab 切换栏 -->
<div class="tr-subtabs" id="tr-subtabs">
<button class="tr-subtab active" data-subtab="stats">统计分析</button>
<button class="tr-subtab" data-subtab="reflection">交易反思</button>
<button class="tr-subtab" data-subtab="experience">经验库</button>
</div>
<!-- 统计分析子 Tab -->
<div id="tr-tab-stats" class="tr-tab-panel active">
<!-- 工具栏 --> <!-- 工具栏 -->
<div class="tr-toolbar"> <div class="tr-toolbar">
<div class="tr-toolbar-left"> <div class="tr-toolbar-left">
@ -1023,7 +1153,7 @@
<tr> <tr>
<th>日期</th><th>笔数</th><th>平仓盈亏</th><th>手续费</th> <th>日期</th><th>笔数</th><th>平仓盈亏</th><th>手续费</th>
<th>净盈亏</th><th>盈利笔</th><th>亏损笔</th><th>胜率</th> <th>净盈亏</th><th>盈利笔</th><th>亏损笔</th><th>胜率</th>
<th>最大盈利</th><th>最大亏损</th><th>品种数</th> <th>最大盈利</th><th>最大亏损</th><th>品种数</th><th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody id="tr-daily-body"></tbody> <tbody id="tr-daily-body"></tbody>
@ -1054,6 +1184,311 @@
</div> </div>
</div> </div>
<!-- 交易反思子 Tab -->
<div id="tr-tab-reflection" class="tr-tab-panel">
<div class="tr-refl-datebar">
<button class="tr-refl-datebtn" id="tr-refl-prev" title="前一天"></button>
<span class="tr-refl-datetext" id="tr-refl-current-date">-</span>
<button class="tr-refl-datebtn" id="tr-refl-next" title="后一天"></button>
<div class="tr-refl-actions">
<button class="tr-btn tr-btn-secondary" id="tr-refl-edit-daily">
<span>✏️</span><span>当日反思编辑</span>
</button>
</div>
</div>
<div class="tr-refl-stats">
<div class="tr-refl-statcard">
<div class="tr-refl-statlabel">总盈亏</div>
<div class="tr-refl-statvalue" id="tr-refl-stat-pnl">-</div>
</div>
<div class="tr-refl-statcard">
<div class="tr-refl-statlabel">交易笔数</div>
<div class="tr-refl-statvalue" id="tr-refl-stat-trades">-</div>
</div>
<div class="tr-refl-statcard">
<div class="tr-refl-statlabel">胜率</div>
<div class="tr-refl-statvalue" id="tr-refl-stat-winrate">-</div>
</div>
<div class="tr-refl-statcard">
<div class="tr-refl-statlabel">已反思</div>
<div id="tr-refl-stat-reflected">-</div>
</div>
</div>
<div class="tr-refl-daily">
<div class="tr-refl-daily-header">
<div class="tr-refl-daily-title">当日反思</div>
</div>
<div class="tr-refl-daily-grid">
<div class="tr-refl-daily-item">
<div class="tr-refl-daily-label">情绪状态</div>
<div class="tr-refl-daily-value" id="tr-refl-emotion">-</div>
</div>
<div class="tr-refl-daily-item">
<div class="tr-refl-daily-label">执行纪律</div>
<div class="tr-refl-daily-value" id="tr-refl-discipline">-</div>
</div>
<div class="tr-refl-daily-item">
<div class="tr-refl-daily-label">总体评价</div>
<div class="tr-refl-daily-value" id="tr-refl-overall">-</div>
</div>
<div class="tr-refl-daily-item">
<div class="tr-refl-daily-label">市场判断</div>
<div class="tr-refl-daily-value" id="tr-refl-market">-</div>
</div>
</div>
</div>
<div class="tr-refl-section-title">交易配对</div>
<div id="tr-refl-pairs" class="tr-refl-pairs"></div>
<div class="tr-refl-unpaired">
<div class="tr-refl-section-title">未配对交易</div>
<table class="tr-refl-unpaired-table">
<thead>
<tr><th>时间</th><th>品种</th><th>方向</th><th>价格</th><th>手数</th><th>盈亏</th></tr>
</thead>
<tbody id="tr-refl-unpaired-body"></tbody>
</table>
<div id="tr-refl-unpaired-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-text">暂无未配对交易</div>
</div>
</div>
</div>
<!-- 经验库子 Tab -->
<div id="tr-tab-experience" class="tr-tab-panel">
<div class="tr-exp-toolbar">
<input type="text" class="tr-exp-search" id="tr-exp-search" placeholder="搜索经验...">
<select class="tr-exp-filter" id="tr-exp-tag-filter">
<option value="">全部标签</option>
</select>
<select class="tr-exp-filter" id="tr-exp-type-filter">
<option value="">全部类型</option>
<option value="lesson">教训</option>
<option value="insight">心得</option>
<option value="strategy">策略</option>
<option value="discipline">纪律</option>
</select>
<div class="tr-exp-viewtabs">
<button class="tr-exp-viewtab active" data-view="list">列表</button>
<button class="tr-exp-viewtab" data-view="timeline">时间线</button>
<button class="tr-exp-viewtab" data-view="category">分类</button>
</div>
</div>
<div id="tr-exp-container" class="tr-exp-grid"></div>
<div id="tr-exp-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📚</div>
<div class="tr-empty-text">暂无经验记录</div>
</div>
</div>
<!-- 反思编辑 Modal -->
<div id="tr-refl-edit-modal" class="tr-modal-overlay" onclick="if(event.target===this)trReflCloseEditModal()">
<div class="tr-modal" style="max-width:720px;">
<div class="tr-modal-header">
<span class="tr-modal-title">写反思</span>
<button class="tr-modal-close" onclick="trReflCloseEditModal()"></button>
</div>
<div class="tr-modal-body">
<form id="tr-refl-edit-form">
<input type="hidden" id="tr-refl-edit-pair-id">
<div class="tr-form-row">
<div class="tr-form-group">
<label class="tr-form-label">入场理由</label>
<textarea class="tr-form-textarea" id="tr-refl-entry-reason" placeholder="当时为什么入场?"></textarea>
</div>
<div class="tr-form-group">
<label class="tr-form-label">出场理由</label>
<textarea class="tr-form-textarea" id="tr-refl-exit-reason" placeholder="为什么在这个位置出场?"></textarea>
</div>
</div>
<div class="tr-form-row">
<div class="tr-form-group">
<label class="tr-form-label">入场时机评价</label>
<select class="tr-form-select" id="tr-refl-entry-timing">
<option value="">请选择</option>
<option value="excellent">优秀</option>
<option value="good">良好</option>
<option value="average">一般</option>
<option value="poor">较差</option>
<option value="bad">糟糕</option>
</select>
</div>
<div class="tr-form-group">
<label class="tr-form-label">出场时机评价</label>
<select class="tr-form-select" id="tr-refl-exit-timing">
<option value="">请选择</option>
<option value="excellent">优秀</option>
<option value="good">良好</option>
<option value="average">一般</option>
<option value="poor">较差</option>
<option value="bad">糟糕</option>
</select>
</div>
</div>
<div class="tr-form-group">
<label class="tr-form-label">仓位管理反思</label>
<textarea class="tr-form-textarea" id="tr-refl-position" placeholder="仓位是否合理?是否有改进空间?"></textarea>
</div>
<div class="tr-form-group">
<label class="tr-form-label">纪律评分</label>
<div class="tr-form-rating" id="tr-refl-discipline-score">
<span class="tr-form-star" data-score="1"></span>
<span class="tr-form-star" data-score="2"></span>
<span class="tr-form-star" data-score="3"></span>
<span class="tr-form-star" data-score="4"></span>
<span class="tr-form-star" data-score="5"></span>
</div>
</div>
<div class="tr-form-group">
<label class="tr-form-label">自由反思</label>
<textarea class="tr-form-textarea" id="tr-refl-free" placeholder="还有什么想记录的?"></textarea>
</div>
<div class="tr-form-group">
<label class="tr-form-label">标签</label>
<div id="tr-refl-edit-tags" class="tr-tag-pool"></div>
</div>
<div class="tr-form-actions">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trReflCloseEditModal()">取消</button>
<button type="submit" class="tr-btn tr-btn-primary">保存反思</button>
</div>
</form>
</div>
</div>
</div>
<!-- 当日反思编辑 Modal -->
<div id="tr-refl-daily-modal" class="tr-modal-overlay" onclick="if(event.target===this)trReflCloseDailyModal()">
<div class="tr-modal" style="max-width:640px;">
<div class="tr-modal-header">
<span class="tr-modal-title">当日反思编辑</span>
<button class="tr-modal-close" onclick="trReflCloseDailyModal()"></button>
</div>
<div class="tr-modal-body">
<form id="tr-refl-daily-form">
<div class="tr-form-row">
<div class="tr-form-group">
<label class="tr-form-label">情绪状态</label>
<select class="tr-form-select" id="tr-refl-daily-emotion">
<option value="">请选择</option>
<option value="calm">平静</option>
<option value="confident">自信</option>
<option value="anxious">焦虑</option>
<option value="greedy">贪婪</option>
<option value="fearful">恐惧</option>
<option value="impulsive">冲动</option>
<option value="tired">疲惫</option>
</select>
</div>
<div class="tr-form-group">
<label class="tr-form-label">执行纪律评分</label>
<div class="tr-form-rating" id="tr-refl-daily-discipline-score">
<span class="tr-form-star" data-score="1"></span>
<span class="tr-form-star" data-score="2"></span>
<span class="tr-form-star" data-score="3"></span>
<span class="tr-form-star" data-score="4"></span>
<span class="tr-form-star" data-score="5"></span>
</div>
</div>
</div>
<div class="tr-form-row">
<div class="tr-form-group">
<label class="tr-form-label">总体评价</label>
<select class="tr-form-select" id="tr-refl-daily-overall">
<option value="">请选择</option>
<option value="excellent">优秀</option>
<option value="good">良好</option>
<option value="average">一般</option>
<option value="poor">较差</option>
<option value="bad">糟糕</option>
</select>
</div>
<div class="tr-form-group">
<label class="tr-form-label">市场判断</label>
<select class="tr-form-select" id="tr-refl-daily-market">
<option value="">请选择</option>
<option value="accurate">准确</option>
<option value="basically">基本准确</option>
<option value="deviated">有所偏差</option>
<option value="wrong">错误</option>
</select>
</div>
</div>
<div class="tr-form-group">
<label class="tr-form-label">总结</label>
<textarea class="tr-form-textarea" id="tr-refl-daily-summary" placeholder="今天交易的最大收获是什么?"></textarea>
</div>
<div class="tr-form-group">
<label class="tr-form-label">改进方向</label>
<textarea class="tr-form-textarea" id="tr-refl-daily-improvement" placeholder="明天要注意什么?"></textarea>
</div>
<div class="tr-form-actions">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trReflCloseDailyModal()">取消</button>
<button type="submit" class="tr-btn tr-btn-primary">保存当日反思</button>
</div>
</form>
</div>
</div>
</div>
<!-- 标签管理 Modal -->
<div id="tr-refl-tags-modal" class="tr-modal-overlay" onclick="if(event.target===this)trReflCloseTagsModal()">
<div class="tr-modal" style="max-width:560px;">
<div class="tr-modal-header">
<span class="tr-modal-title">标签管理</span>
<button class="tr-modal-close" onclick="trReflCloseTagsModal()"></button>
</div>
<div class="tr-modal-body">
<input type="hidden" id="tr-refl-tags-pair-id">
<div id="tr-refl-tags-preset"></div>
<div class="tr-tag-custom">
<input type="text" class="tr-form-input" id="tr-refl-tag-input" placeholder="输入自定义标签">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trReflAddCustomTag()">添加</button>
</div>
<div class="tr-form-actions">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trReflCloseTagsModal()">取消</button>
<button type="button" class="tr-btn tr-btn-primary" onclick="trReflSaveTags()">保存标签</button>
</div>
</div>
</div>
</div>
<!-- AI 分析结果 Modal -->
<div id="tr-refl-ai-modal" class="tr-modal-overlay" onclick="if(event.target===this)trReflCloseAIModal()">
<div class="tr-modal" style="max-width:680px;">
<div class="tr-modal-header">
<span class="tr-modal-title">AI 分析结果</span>
<button class="tr-modal-close" onclick="trReflCloseAIModal()"></button>
</div>
<div class="tr-modal-body">
<input type="hidden" id="tr-refl-ai-pair-id">
<div class="tr-ai-result-section">
<div class="tr-ai-result-title">综合评价</div>
<div class="tr-ai-result-content" id="tr-refl-ai-overall">-</div>
</div>
<div class="tr-ai-result-section">
<div class="tr-ai-result-title">优点</div>
<ul class="tr-ai-list" id="tr-refl-ai-strengths"></ul>
</div>
<div class="tr-ai-result-section">
<div class="tr-ai-result-title">不足</div>
<ul class="tr-ai-list" id="tr-refl-ai-weaknesses"></ul>
</div>
<div class="tr-ai-result-section">
<div class="tr-ai-result-title">经验提炼建议</div>
<div class="tr-ai-result-content" id="tr-refl-ai-suggestion">-</div>
</div>
<div class="tr-form-actions">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trReflCloseAIModal()">关闭</button>
<button type="button" class="tr-btn tr-btn-primary" onclick="trReflSaveExperience()">保存到经验库</button>
</div>
</div>
</div>
</div>
</div>
<!-- 每日详情弹窗 --> <!-- 每日详情弹窗 -->
<div id="tr-daily-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseDailyModal()"> <div id="tr-daily-modal" class="rv-modal-overlay" onclick="if(event.target===this)trCloseDailyModal()">
<div class="rv-modal" style="max-width:1000px;"> <div class="rv-modal" style="max-width:1000px;">

@ -627,6 +627,623 @@ function renderFuturesGrid(data) {
`}).join(''); `}).join('');
} }
// ==================== 交易反思子系统 ====================
let trReflCurrentDate = null;
let trReflData = null;
let trReflTags = [];
let trReflExperiences = [];
let trReflTagState = new Set();
let trReflCurrentPairId = null;
let trReflCurrentAIResult = null;
function trSwitchSubtab(tab) {
document.querySelectorAll('#tr-subtabs .tr-subtab').forEach(btn => {
btn.classList.toggle('active', btn.dataset.subtab === tab);
});
document.querySelectorAll('#trade-review-view > .tr-tab-panel').forEach(panel => {
panel.classList.toggle('active', panel.id === 'tr-tab-' + tab);
});
if (tab === 'experience') trExpLoad();
}
function trSwitchToReflection(date) {
trSwitchSubtab('reflection');
trReflSetDate(date);
}
function trReflInit() {
const dateInput = document.getElementById('tr-refl-current-date');
if (!dateInput) return;
// 默认日期取统计分析的最新交易日或今天
const today = new Date().toISOString().slice(0, 10);
trReflSetDate(today);
document.getElementById('tr-refl-prev').addEventListener('click', () => trReflChangeDate(-1));
document.getElementById('tr-refl-next').addEventListener('click', () => trReflChangeDate(1));
document.getElementById('tr-refl-edit-daily').addEventListener('click', trReflOpenDailyModal);
document.getElementById('tr-refl-edit-form').addEventListener('submit', e => {
e.preventDefault();
trReflSaveEdit();
});
document.getElementById('tr-refl-daily-form').addEventListener('submit', e => {
e.preventDefault();
trReflSaveDaily();
});
document.querySelectorAll('#tr-refl-discipline-score .tr-form-star, #tr-refl-daily-discipline-score .tr-form-star').forEach(star => {
star.addEventListener('click', () => {
const container = star.parentElement;
const score = parseInt(star.dataset.score);
container.dataset.value = score;
trReflUpdateStars(container, score);
});
});
document.getElementById('tr-exp-search').addEventListener('input', trExpRender);
document.getElementById('tr-exp-tag-filter').addEventListener('change', trExpRender);
document.getElementById('tr-exp-type-filter').addEventListener('change', trExpRender);
document.querySelectorAll('.tr-exp-viewtab').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tr-exp-viewtab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
trExpRender();
});
});
trReflLoadTags();
}
function trReflSetDate(date) {
trReflCurrentDate = date;
document.getElementById('tr-refl-current-date').textContent = date;
trReflLoadData();
}
function trReflChangeDate(days) {
const d = new Date(trReflCurrentDate);
d.setDate(d.getDate() + days);
trReflSetDate(d.toISOString().slice(0, 10));
}
async function trReflLoadData() {
if (!trReflCurrentDate) return;
try {
const res = await fetch(`${TR_API_BASE}/daily-trades/${trReflCurrentDate}`);
const json = await res.json();
trReflData = json.success ? json.data : null;
if (!trReflData) trReflData = { pairs: [], unpaired: [], daily_reflection: null, summary: {} };
trReflRender();
} catch (e) {
console.error('加载反思数据失败', e);
trReflData = { pairs: [], unpaired: [], daily_reflection: null, summary: {} };
trReflRender();
}
}
function trReflRender() {
const data = trReflData || { pairs: [], unpaired: [], daily_reflection: null, summary: {} };
const summary = data.summary || {};
const daily = data.daily_reflection || {};
const pnl = summary.total_pnl || 0;
const pnlEl = document.getElementById('tr-refl-stat-pnl');
pnlEl.textContent = (pnl >= 0 ? '+' : '') + pnl.toFixed(2);
pnlEl.className = 'tr-refl-statvalue ' + (pnl >= 0 ? 'profit' : 'loss');
document.getElementById('tr-refl-stat-trades').textContent = summary.total_trades || 0;
document.getElementById('tr-refl-stat-winrate').textContent = (summary.win_rate || 0) + '%';
const reflectedEl = document.getElementById('tr-refl-stat-reflected');
const reflected = summary.reflected_count || 0;
const totalPairs = (data.pairs || []).length;
if (totalPairs === 0) {
reflectedEl.innerHTML = '<span class="tr-refl-reflected no">无交易</span>';
} else if (reflected >= totalPairs) {
reflectedEl.innerHTML = '<span class="tr-refl-reflected yes">✓ 已完成</span>';
} else {
reflectedEl.innerHTML = `<span class="tr-refl-reflected no">${reflected}/${totalPairs}</span>`;
}
document.getElementById('tr-refl-emotion').textContent = trReflTextMap.emotion[daily.emotion_state] || daily.emotion_state || '未填写';
document.getElementById('tr-refl-emotion').className = 'tr-refl-daily-value ' + (daily.emotion_state ? '' : 'empty');
document.getElementById('tr-refl-discipline').textContent = daily.discipline_score ? daily.discipline_score + '/5' : '未填写';
document.getElementById('tr-refl-discipline').className = 'tr-refl-daily-value ' + (daily.discipline_score ? '' : 'empty');
document.getElementById('tr-refl-overall').textContent = trReflTextMap.overall[daily.overall_evaluation] || daily.overall_evaluation || '未填写';
document.getElementById('tr-refl-overall').className = 'tr-refl-daily-value ' + (daily.overall_evaluation ? '' : 'empty');
document.getElementById('tr-refl-market').textContent = trReflTextMap.market[daily.market_judgement] || daily.market_judgement || '未填写';
document.getElementById('tr-refl-market').className = 'tr-refl-daily-value ' + (daily.market_judgement ? '' : 'empty');
const pairsContainer = document.getElementById('tr-refl-pairs');
const pairs = data.pairs || [];
if (!pairs.length) {
pairsContainer.innerHTML = '<div class="tr-empty" style="grid-column:1/-1;"><div class="tr-empty-icon">📝</div><div class="tr-empty-text">当日无交易配对</div></div>';
} else {
pairsContainer.innerHTML = pairs.map(p => trReflRenderPairCard(p)).join('');
}
const unpairedBody = document.getElementById('tr-refl-unpaired-body');
const unpairedEmpty = document.getElementById('tr-refl-unpaired-empty');
const unpaired = data.unpaired || [];
if (!unpaired.length) {
unpairedBody.innerHTML = '';
unpairedEmpty.style.display = 'flex';
} else {
unpairedEmpty.style.display = 'none';
unpairedBody.innerHTML = unpaired.map(u => {
const pnl = u.net_pnl || u.close_pnl || 0;
const pnlCls = pnl > 0 ? 'tr-pnl-positive' : (pnl < 0 ? 'tr-pnl-negative' : '');
return `<tr>
<td>${u.trade_time || '-'}</td>
<td>${u.variety}${u.symbol_name ? '(' + u.symbol_name + ')' : ''}</td>
<td>${u.direction || '-'}</td>
<td>${(u.price || 0).toFixed(2)}</td>
<td>${u.volume || 0}</td>
<td class="${pnlCls}">${pnl !== 0 ? (pnl >= 0 ? '+' : '') + pnl.toFixed(2) : '-'}</td>
</tr>`;
}).join('');
}
}
function trReflRenderPairCard(p) {
const pnl = p.net_pnl || 0;
const pnlCls = pnl >= 0 ? 'profit' : 'loss';
const dirCls = p.direction === '多' ? 'long' : 'short';
const dirText = p.direction === '多' ? '做多' : '做空';
const tags = (p.tags || []).map(t => `<span class="tr-refl-tag">${t}</span>`).join('');
const hasReflection = p.reflection && (p.reflection.entry_reason || p.reflection.free_reflection);
return `<div class="tr-refl-card ${pnlCls}">
<div class="tr-refl-card-header">
<div>
<div class="tr-refl-card-symbol">${p.symbol_name || p.variety || p.symbol || '-'}</div>
<span class="tr-refl-card-dir ${dirCls}">${dirText}</span>
</div>
<div class="tr-refl-card-pnl ${pnlCls}">${pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}</div>
</div>
<div class="tr-refl-card-nodes">
开仓${p.open_date || '-'} ${p.open_time || ''} @ ${(p.open_price || 0).toFixed(2)}<br>
平仓${p.close_date || '-'} ${p.close_time || ''} @ ${(p.close_price || 0).toFixed(2)}
</div>
<div class="tr-refl-card-tags">${tags}</div>
<div class="tr-refl-card-actions">
<button class="tr-btn tr-btn-primary" onclick='trReflOpenEditModal(${p.id})'>
<span>${hasReflection ? '✏️' : '✍️'}</span><span>${hasReflection ? '' : ''}</span>
</button>
<button class="tr-btn tr-btn-secondary" onclick='trReflOpenTagsModal(${p.id})'>
<span>🏷</span><span></span>
</button>
<button class="tr-btn tr-btn-secondary" onclick='trReflOpenAIModal(${p.id})'>
<span>🤖</span><span></span>
</button>
</div>
</div>`;
}
async function trReflLoadTags() {
try {
const res = await fetch(`${TR_API_BASE}/tags`);
const json = await res.json();
if (json.success) trReflTags = json.data || [];
} catch (e) { console.error('加载标签失败', e); }
}
// ===== 反思编辑 Modal =====
async function trReflOpenEditModal(pairId) {
const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId);
if (!pair) return;
trReflCurrentPairId = pairId;
const reflection = pair.reflection || {};
document.getElementById('tr-refl-edit-pair-id').value = pairId;
document.getElementById('tr-refl-entry-reason').value = reflection.entry_reason || '';
document.getElementById('tr-refl-exit-reason').value = reflection.exit_reason || '';
document.getElementById('tr-refl-entry-timing').value = reflection.entry_timing || '';
document.getElementById('tr-refl-exit-timing').value = reflection.exit_timing || '';
document.getElementById('tr-refl-position').value = reflection.position_management || '';
document.getElementById('tr-refl-free').value = reflection.free_reflection || '';
const score = reflection.discipline_score || 0;
const container = document.getElementById('tr-refl-discipline-score');
container.dataset.value = score;
trReflUpdateStars(container, score);
// 渲染标签选择
const tagPool = document.getElementById('tr-refl-edit-tags');
const selected = new Set(reflection.tags || pair.tags || []);
tagPool.innerHTML = trReflTags.map(t => {
const name = typeof t === 'string' ? t : t.name;
const active = selected.has(name) ? 'selected' : '';
return `<span class="tr-tag-chip ${active}" onclick="this.classList.toggle('selected')">${name}</span>`;
}).join('');
document.getElementById('tr-refl-edit-modal').classList.add('show');
}
function trReflCloseEditModal() {
document.getElementById('tr-refl-edit-modal').classList.remove('show');
trReflCurrentPairId = null;
}
async function trReflSaveEdit() {
const pairId = parseInt(document.getElementById('tr-refl-edit-pair-id').value);
if (!pairId) return;
const selectedTags = Array.from(document.querySelectorAll('#tr-refl-edit-tags .tr-tag-chip.selected')).map(el => el.textContent);
const body = {
trade_pair_id: pairId,
entry_reason: document.getElementById('tr-refl-entry-reason').value,
exit_reason: document.getElementById('tr-refl-exit-reason').value,
entry_timing: document.getElementById('tr-refl-entry-timing').value,
exit_timing: document.getElementById('tr-refl-exit-timing').value,
position_management: document.getElementById('tr-refl-position').value,
discipline_score: parseInt(document.getElementById('tr-refl-discipline-score').dataset.value || '0'),
free_reflection: document.getElementById('tr-refl-free').value,
tags: selectedTags,
};
try {
const res = await fetch(`${TR_API_BASE}/reflections`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.success) {
showToast('success', '保存成功', '反思已保存');
trReflCloseEditModal();
trReflLoadData();
} else {
showToast('error', '保存失败', json.message || '请重试');
}
} catch (e) {
showToast('error', '保存失败', e.message);
}
}
// ===== 当日反思 Modal =====
function trReflOpenDailyModal() {
const daily = (trReflData && trReflData.daily_reflection) || {};
document.getElementById('tr-refl-daily-emotion').value = daily.emotion_state || '';
document.getElementById('tr-refl-daily-overall').value = daily.overall_evaluation || '';
document.getElementById('tr-refl-daily-market').value = daily.market_judgement || '';
document.getElementById('tr-refl-daily-summary').value = daily.summary || '';
document.getElementById('tr-refl-daily-improvement').value = daily.improvement || '';
const score = daily.discipline_score || 0;
const container = document.getElementById('tr-refl-daily-discipline-score');
container.dataset.value = score;
trReflUpdateStars(container, score);
document.getElementById('tr-refl-daily-modal').classList.add('show');
}
function trReflCloseDailyModal() {
document.getElementById('tr-refl-daily-modal').classList.remove('show');
}
async function trReflSaveDaily() {
const body = {
trade_date: trReflCurrentDate,
emotion_state: document.getElementById('tr-refl-daily-emotion').value,
overall_evaluation: document.getElementById('tr-refl-daily-overall').value,
market_judgement: document.getElementById('tr-refl-daily-market').value,
discipline_score: parseInt(document.getElementById('tr-refl-daily-discipline-score').dataset.value || '0'),
summary: document.getElementById('tr-refl-daily-summary').value,
improvement: document.getElementById('tr-refl-daily-improvement').value,
};
try {
const res = await fetch(`${TR_API_BASE}/daily-reflections`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.success) {
showToast('success', '保存成功', '当日反思已保存');
trReflCloseDailyModal();
trReflLoadData();
} else {
showToast('error', '保存失败', json.message || '请重试');
}
} catch (e) {
showToast('error', '保存失败', e.message);
}
}
// ===== 标签管理 Modal =====
async function trReflOpenTagsModal(pairId) {
const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId);
if (!pair) return;
trReflCurrentPairId = pairId;
trReflTagState = new Set((pair.tags || []).map(t => typeof t === 'string' ? t : t.name));
document.getElementById('tr-refl-tags-pair-id').value = pairId;
trReflRenderTagPreset();
document.getElementById('tr-refl-tags-modal').classList.add('show');
}
function trReflRenderTagPreset() {
const container = document.getElementById('tr-refl-tags-preset');
const grouped = trReflGroupTags(trReflTags);
container.innerHTML = Object.entries(grouped).map(([category, tags]) => `
<div class="tr-tag-section">
<div class="tr-tag-section-title">${category}</div>
<div class="tr-tag-pool">
${tags.map(t => {
const name = typeof t === 'string' ? t : t.name;
const selected = trReflTagState.has(name) ? 'selected' : '';
return `<span class="tr-tag-chip ${selected}" onclick="trReflToggleTag('${name.replace(/'/g, "\\'")}')">${name}</span>`;
}).join('')}
</div>
</div>
`).join('');
}
function trReflGroupTags(tags) {
const groups = {};
tags.forEach(t => {
const category = (typeof t === 'object' && t.category) ? t.category : '常用';
if (!groups[category]) groups[category] = [];
groups[category].push(t);
});
return groups;
}
function trReflToggleTag(tag) {
if (trReflTagState.has(tag)) trReflTagState.delete(tag);
else trReflTagState.add(tag);
trReflRenderTagPreset();
}
function trReflAddCustomTag() {
const input = document.getElementById('tr-refl-tag-input');
const val = input.value.trim();
if (!val) return;
trReflTagState.add(val);
input.value = '';
trReflRenderTagPreset();
}
async function trReflSaveTags() {
const pairId = parseInt(document.getElementById('tr-refl-tags-pair-id').value);
if (!pairId) return;
const currentTags = Array.from(trReflTagState);
const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId);
const oldTags = (pair && pair.tags) || [];
const oldNames = new Set(oldTags.map(t => typeof t === 'string' ? t : t.name));
try {
for (const tag of currentTags) {
if (!oldNames.has(tag)) {
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag }),
});
}
}
for (const tag of oldTags) {
const name = typeof tag === 'string' ? tag : tag.name;
if (!trReflTagState.has(name)) {
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag: name }),
});
}
}
showToast('success', '保存成功', '标签已更新');
trReflCloseTagsModal();
trReflLoadData();
} catch (e) {
showToast('error', '保存失败', e.message);
}
}
function trReflCloseTagsModal() {
document.getElementById('tr-refl-tags-modal').classList.remove('show');
trReflCurrentPairId = null;
}
// ===== AI 分析结果 Modal =====
async function trReflOpenAIModal(pairId) {
const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId);
if (!pair) return;
trReflCurrentPairId = pairId;
trReflCurrentAIResult = pair.ai_analysis || null;
if (!trReflCurrentAIResult) {
// 如果没有缓存的 AI 结果,尝试请求分析接口
try {
const res = await fetch(`${TR_API_BASE}/analyze-trade`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
symbol: pair.symbol,
open_date: pair.open_date,
open_time: pair.open_time,
close_date: pair.close_date,
close_time: pair.close_time,
direction: pair.direction,
open_price: pair.open_price,
close_price: pair.close_price,
}),
});
const json = await res.json();
if (json.success && json.data) {
trReflCurrentAIResult = json.data;
}
} catch (e) { console.error('AI分析请求失败', e); }
}
const result = trReflCurrentAIResult || {};
document.getElementById('tr-refl-ai-pair-id').value = pairId;
document.getElementById('tr-refl-ai-overall').textContent = result.overall_evaluation || result.analysis || '暂无 AI 分析结果';
document.getElementById('tr-refl-ai-strengths').innerHTML = (result.strengths || []).map(s => `<li>${s}</li>`).join('') || '<li>-</li>';
document.getElementById('tr-refl-ai-weaknesses').innerHTML = (result.weaknesses || []).map(s => `<li>${s}</li>`).join('') || '<li>-</li>';
document.getElementById('tr-refl-ai-suggestion').textContent = result.suggestion || result.experience_suggestion || '暂无建议';
document.getElementById('tr-refl-ai-modal').classList.add('show');
}
async function trReflSaveExperience() {
const pairId = parseInt(document.getElementById('tr-refl-ai-pair-id').value);
const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId);
if (!pair || !trReflCurrentAIResult) return;
const result = trReflCurrentAIResult;
const body = {
title: `${pair.symbol_name || pair.variety || pair.symbol} ${pair.direction === '多' ? '做多' : '做空'} 经验`,
content: result.overall_evaluation || result.analysis || '',
type: pair.net_pnl >= 0 ? 'insight' : 'lesson',
tags: ['AI分析', ...(pair.tags || [])].slice(0, 5),
source_trade_pair_id: pairId,
strengths: result.strengths || [],
weaknesses: result.weaknesses || [],
suggestion: result.suggestion || result.experience_suggestion || '',
};
try {
const res = await fetch(`${TR_API_BASE}/experiences`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.success) {
showToast('success', '保存成功', '已保存到经验库');
trReflCloseAIModal();
} else {
showToast('error', '保存失败', json.message || '请重试');
}
} catch (e) {
showToast('error', '保存失败', e.message);
}
}
function trReflCloseAIModal() {
document.getElementById('tr-refl-ai-modal').classList.remove('show');
trReflCurrentPairId = null;
trReflCurrentAIResult = null;
}
// ===== 经验库 =====
async function trExpLoad() {
try {
const res = await fetch(`${TR_API_BASE}/experiences`);
const json = await res.json();
trReflExperiences = json.success ? (json.data || []) : [];
trExpPopulateTagFilter();
trExpRender();
} catch (e) {
console.error('加载经验库失败', e);
trReflExperiences = [];
trExpRender();
}
}
function trExpPopulateTagFilter() {
const select = document.getElementById('tr-exp-tag-filter');
const allTags = new Set();
trReflExperiences.forEach(e => (e.tags || []).forEach(t => allTags.add(typeof t === 'string' ? t : t.name)));
const current = select.value;
select.innerHTML = '<option value="">全部标签</option>' + Array.from(allTags).sort().map(t => `<option value="${t}">${t}</option>`).join('');
select.value = current;
}
function trExpRender() {
const container = document.getElementById('tr-exp-container');
const empty = document.getElementById('tr-exp-empty');
const keyword = document.getElementById('tr-exp-search').value.toLowerCase();
const tag = document.getElementById('tr-exp-tag-filter').value;
const type = document.getElementById('tr-exp-type-filter').value;
const view = document.querySelector('.tr-exp-viewtab.active').dataset.view;
let list = trReflExperiences.filter(e => {
const matchKeyword = !keyword || (e.title || '').toLowerCase().includes(keyword) || (e.content || '').toLowerCase().includes(keyword);
const matchTag = !tag || (e.tags || []).some(t => (typeof t === 'string' ? t : t.name) === tag);
const matchType = !type || e.type === type;
return matchKeyword && matchTag && matchType;
});
if (!list.length) {
container.innerHTML = '';
container.className = 'tr-exp-grid';
empty.style.display = 'flex';
return;
}
empty.style.display = 'none';
if (view === 'timeline') {
container.className = '';
list = [...list].sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
container.innerHTML = `<div style="display:flex;flex-direction:column;gap:16px;">${list.map(e => trExpRenderCard(e, true)).join('')}</div>`;
} else if (view === 'category') {
container.className = '';
const grouped = {};
list.forEach(e => {
const k = e.type || '其他';
if (!grouped[k]) grouped[k] = [];
grouped[k].push(e);
});
const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' };
container.innerHTML = Object.entries(grouped).map(([k, items]) => `
<div style="margin-bottom:24px;">
<div style="font-size:16px;font-weight:700;margin-bottom:12px;color:var(--text-primary);">${typeNames[k] || k}</div>
<div class="tr-exp-grid">${items.map(e => trExpRenderCard(e)).join('')}</div>
</div>
`).join('');
} else {
container.className = 'tr-exp-grid';
container.innerHTML = list.map(e => trExpRenderCard(e)).join('');
}
}
function trExpRenderCard(e, timeline = false) {
const typeNames = { lesson: '教训', insight: '心得', strategy: '策略', discipline: '纪律' };
const tags = (e.tags || []).map(t => `<span class="tr-refl-tag">${typeof t === 'string' ? t : t.name}</span>`).join('');
const date = e.created_at ? e.created_at.slice(0, 10) : '';
return `<div class="tr-exp-card">
<div class="tr-exp-card-title">${e.title || '未命名经验'}</div>
<div class="tr-exp-card-meta">${typeNames[e.type] || e.type || '其他'} ${date ? '· ' + date : ''}</div>
<div class="tr-exp-card-content">${(e.content || '').slice(0, 120)}${(e.content || '').length > 120 ? '...' : ''}</div>
<div class="tr-exp-card-tags">${tags}</div>
</div>`;
}
// ===== 星级评分辅助 =====
function trReflUpdateStars(container, score) {
container.querySelectorAll('.tr-form-star').forEach(s => {
s.classList.toggle('active', parseInt(s.dataset.score) <= score);
});
}
// ===== 文本映射 =====
const trReflTextMap = {
emotion: {
calm: '平静', confident: '自信', anxious: '焦虑', greedy: '贪婪',
fearful: '恐惧', impulsive: '冲动', tired: '疲惫'
},
overall: {
excellent: '优秀', good: '良好', average: '一般', poor: '较差', bad: '糟糕'
},
market: {
accurate: '准确', basically: '基本准确', deviated: '有所偏差', wrong: '错误'
},
timing: {
excellent: '优秀', good: '良好', average: '一般', poor: '较差', bad: '糟糕'
}
};
// ==================== 交易复盘功能 ==================== // ==================== 交易复盘功能 ====================
const TR_API_BASE = '/api/v1/trade-review'; const TR_API_BASE = '/api/v1/trade-review';
@ -671,8 +1288,16 @@ function initTradeReview() {
// 批次管理 // 批次管理
document.getElementById('tr-btn-batches').addEventListener('click', trShowBatchModal); document.getElementById('tr-btn-batches').addEventListener('click', trShowBatchModal);
// 子 Tab 切换
document.querySelectorAll('#tr-subtabs .tr-subtab').forEach(btn => {
btn.addEventListener('click', () => trSwitchSubtab(btn.dataset.subtab));
});
// 初始加载 // 初始加载
trInitDefaultDate(); trInitDefaultDate();
// 初始化交易反思子系统
trReflInit();
} }
async function trInitDefaultDate() { async function trInitDefaultDate() {
@ -879,18 +1504,19 @@ function trRenderDailyTable(daily) {
const closePnl = net + d.total_commission; const closePnl = net + d.total_commission;
const netCls = net >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative'; const netCls = net >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
const cpCls = closePnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative'; const cpCls = closePnl >= 0 ? 'tr-pnl-positive' : 'tr-pnl-negative';
return `<tr style="cursor:pointer" onclick="trOpenDailyModal('${d.trade_date}')"> return `<tr>
<td class="tr-link">${d.trade_date}</td> <td class="tr-link" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.trade_date}</td>
<td>${d.total_trades}</td> <td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.total_trades}</td>
<td class="${cpCls}">${closePnl >= 0 ? '+' : ''}${closePnl.toFixed(2)}</td> <td class="${cpCls}" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${closePnl >= 0 ? '+' : ''}${closePnl.toFixed(2)}</td>
<td>${d.total_commission.toFixed(2)}</td> <td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.total_commission.toFixed(2)}</td>
<td class="${netCls}">${net >= 0 ? '+' : ''}${net.toFixed(2)}</td> <td class="${netCls}" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${net >= 0 ? '+' : ''}${net.toFixed(2)}</td>
<td>${d.win_count}</td> <td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.win_count}</td>
<td>${d.loss_count}</td> <td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.loss_count}</td>
<td>${d.win_rate}%</td> <td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.win_rate}%</td>
<td class="tr-pnl-positive">+${d.max_win.toFixed(2)}</td> <td class="tr-pnl-positive" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">+${d.max_win.toFixed(2)}</td>
<td class="tr-pnl-negative">${d.max_loss.toFixed(2)}</td> <td class="tr-pnl-negative" onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.max_loss.toFixed(2)}</td>
<td>${d.variety_count}</td> <td onclick="trOpenDailyModal('${d.trade_date}')" style="cursor:pointer">${d.variety_count}</td>
<td><span class="tr-link" onclick="event.stopPropagation(); trSwitchToReflection('${d.trade_date}')">进入反思</span></td>
</tr>`; </tr>`;
}).join(''); }).join('');
} }

Loading…
Cancel
Save