@ -635,6 +635,7 @@ let trReflTags = [];
let trReflExperiences = [ ] ;
let trReflTagState = new Set ( ) ;
let trReflCurrentPairId = null ;
let trReflCurrentReflectionId = null ;
let trReflCurrentAIResult = null ;
function trSwitchSubtab ( tab ) {
@ -673,6 +674,14 @@ function trReflInit() {
trReflSaveDaily ( ) ;
} ) ;
const saveAndAiBtn = document . getElementById ( 'tr-refl-save-and-ai' ) ;
if ( saveAndAiBtn ) {
saveAndAiBtn . addEventListener ( 'click' , e => {
e . preventDefault ( ) ;
trReflSaveAndAnalyze ( ) ;
} ) ;
}
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 ;
@ -751,10 +760,10 @@ function trReflRender() {
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 _judg e ment] || daily . market _judg e ment || '未填写' ;
document . getElementById ( 'tr-refl-market' ) . className = 'tr-refl-daily-value ' + ( daily . market _judg e ment ? '' : 'empty' ) ;
document . getElementById ( 'tr-refl-overall' ) . textContent = trReflTextMap . overall [ daily . overall _ rating] || daily . overall _rating || '未填写' ;
document . getElementById ( 'tr-refl-overall' ) . className = 'tr-refl-daily-value ' + ( daily . overall _ rating ? '' : 'empty' ) ;
document . getElementById ( 'tr-refl-market' ) . textContent = trReflTextMap . market [ daily . market _judg ment] || daily . market _judg ment || '未填写' ;
document . getElementById ( 'tr-refl-market' ) . className = 'tr-refl-daily-value ' + ( daily . market _judg ment ? '' : 'empty' ) ;
const pairsContainer = document . getElementById ( 'tr-refl-pairs' ) ;
const pairs = data . pairs || [ ] ;
@ -792,8 +801,8 @@ function trReflRenderPairCard(p) {
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 ) ;
const tags = ( p . tags || [ ] ) . map ( t => ` <span class="tr-refl-tag"> ${ typeof t === 'string' ? t : t . name } </span> ` ) . join ( '' ) ;
const hasReflection = p . reflection _status === 'done' ;
return ` <div class="tr-refl-card ${ pnlCls } ">
< div class = "tr-refl-card-header" >
< div >
@ -835,9 +844,30 @@ async function trReflOpenEditModal(pairId) {
const pair = data . pairs . find ( p => p . id === pairId ) ;
if ( ! pair ) return ;
trReflCurrentPairId = pairId ;
const reflection = pair . reflection || { } ;
trReflCurrentReflectionId = null ;
document . getElementById ( 'tr-refl-edit-pair-id' ) . value = pairId ;
let reflection = { } ;
let selectedTagNames = new Set ( ) ;
try {
const [ reflRes , tagsRes ] = await Promise . all ( [
fetch ( ` ${ TR _API _BASE } /reflections?trade_pair_id= ${ pairId } ` ) ,
fetch ( ` ${ TR _API _BASE } /trade-pairs/ ${ pairId } /tags ` ) ,
] ) ;
const reflJson = await reflRes . json ( ) ;
const tagsJson = await tagsRes . json ( ) ;
if ( reflJson . success && reflJson . data && reflJson . data . length ) {
reflection = reflJson . data [ 0 ] ;
trReflCurrentReflectionId = reflection . id || null ;
}
if ( tagsJson . success && tagsJson . data ) {
selectedTagNames = new Set ( tagsJson . data . map ( t => typeof t === 'string' ? t : t . name ) ) ;
}
} catch ( e ) {
console . error ( '加载反思/标签失败' , e ) ;
}
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 || '' ;
@ -852,10 +882,9 @@ async function trReflOpenEditModal(pairId) {
// 渲染标签选择
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' : '' ;
const active = selected TagNames . has ( name ) ? 'selected' : '' ;
return ` <span class="tr-tag-chip ${ active } " onclick="this.classList.toggle('selected')"> ${ name } </span> ` ;
} ) . join ( '' ) ;
@ -865,15 +894,16 @@ async function trReflOpenEditModal(pairId) {
function trReflCloseEditModal ( ) {
document . getElementById ( 'tr-refl-edit-modal' ) . classList . remove ( 'show' ) ;
trReflCurrentPairId = null ;
trReflCurrentReflectionId = null ;
}
async function trReflSaveEdit ( ) {
async function trReflSaveEdit ( andAnalyze = false ) {
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 ,
trade _date : trReflCurrentDate ,
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 ,
@ -881,36 +911,108 @@ async function trReflSaveEdit() {
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' ,
const url = trReflCurrentReflectionId
? ` ${ TR _API _BASE } /reflections/ ${ trReflCurrentReflectionId } `
: ` ${ TR _API _BASE } /reflections ` ;
const method = trReflCurrentReflectionId ? 'PUT' : 'POST' ;
const res = await fetch ( url , {
method ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( body ) ,
} ) ;
const json = await res . json ( ) ;
if ( json . success ) {
if ( ! json . success ) {
showToast ( 'error' , '保存失败' , json . message || '请重试' ) ;
return ;
}
// 同步标签(编辑 Modal 中的标签选择)
const selectedTagNames = Array . from ( document . querySelectorAll ( '#tr-refl-edit-tags .tr-tag-chip.selected' ) ) . map ( el => el . textContent ) ;
try {
await trReflSyncPairTags ( pairId , selectedTagNames ) ;
} catch ( e ) {
console . error ( '标签同步失败' , e ) ;
}
if ( andAnalyze ) {
try {
const reRes = await fetch ( ` ${ TR _API _BASE } /reanalyze ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { trade _pair _id : pairId } ) ,
} ) ;
const reJson = await reRes . json ( ) ;
if ( reJson . success && reJson . data ) {
// 重新分析接口不返回 id, 需要再取一次 latest 获取完整记录
const latestRes = await fetch ( ` ${ TR _API _BASE } /reanalyze/ ${ pairId } /latest ` ) ;
const latestJson = await latestRes . json ( ) ;
if ( latestJson . success && latestJson . data ) {
trReflCurrentAIResult = latestJson . data ;
} else {
trReflCurrentAIResult = reJson . data ;
}
trReflCloseEditModal ( ) ;
trReflOpenAIModal ( pairId , false ) ;
return ;
}
showToast ( 'error' , 'AI分析失败' , reJson . message || '请重试' ) ;
return ;
} catch ( e ) {
showToast ( 'error' , 'AI分析失败' , e . message ) ;
return ;
}
}
showToast ( 'success' , '保存成功' , '反思已保存' ) ;
trReflCloseEditModal ( ) ;
trReflLoadData ( ) ;
} else {
showToast ( 'error' , '保存失败' , json . message || '请重试' ) ;
}
} catch ( e ) {
showToast ( 'error' , '保存失败' , e . message ) ;
}
}
async function trReflSaveAndAnalyze ( ) {
await trReflSaveEdit ( true ) ;
}
async function trReflSyncPairTags ( pairId , selectedNames ) {
const res = await fetch ( ` ${ TR _API _BASE } /trade-pairs/ ${ pairId } /tags ` ) ;
const json = await res . json ( ) ;
const currentTags = ( json . success ? json . data : [ ] ) || [ ] ;
const currentNames = new Set ( currentTags . map ( t => typeof t === 'string' ? t : t . name ) ) ;
// 新增标签
for ( const name of selectedNames ) {
if ( currentNames . has ( name ) ) continue ;
const tag = trReflTags . find ( t => ( typeof t === 'string' ? t : t . name ) === name ) ;
if ( ! tag || typeof tag === 'string' ) continue ;
await fetch ( ` ${ TR _API _BASE } /trade-pairs/ ${ pairId } /tags?tag_id= ${ tag . id } ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
} ) ;
}
// 移除标签
for ( const tag of currentTags ) {
const name = typeof tag === 'string' ? tag : tag . name ;
if ( selectedNames . includes ( name ) ) continue ;
const tagId = typeof tag === 'string' ? null : tag . id ;
if ( ! tagId ) continue ;
await fetch ( ` ${ TR _API _BASE } /trade-pairs/ ${ pairId } /tags/ ${ tagId } ` , { method : 'DELETE' } ) ;
}
}
// ===== 当日反思 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-overall' ) . value = daily . overall _ rating || '' ;
document . getElementById ( 'tr-refl-daily-market' ) . value = daily . market _judg ment || '' ;
document . getElementById ( 'tr-refl-daily-summary' ) . value = daily . summary || '' ;
document . getElementById ( 'tr-refl-daily-improvement' ) . value = daily . improvement || '' ;
document . getElementById ( 'tr-refl-daily-improvement' ) . value = daily . improvement s || '' ;
const score = daily . discipline _score || 0 ;
const container = document . getElementById ( 'tr-refl-daily-discipline-score' ) ;
@ -926,13 +1028,13 @@ function trReflCloseDailyModal() {
async function trReflSaveDaily ( ) {
const body = {
trade _date : trReflCurrentDate ,
reflection _date : trReflCurrentDate ,
emotion _state : document . getElementById ( 'tr-refl-daily-emotion' ) . value ,
overall _ evaluation : document . getElementById ( 'tr-refl-daily-overall' ) . value ,
market _judg e ment: document . getElementById ( 'tr-refl-daily-market' ) . value ,
overall _ rating : document . getElementById ( 'tr-refl-daily-overall' ) . value ,
market _judg ment: 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 ,
improvement s : document . getElementById ( 'tr-refl-daily-improvement' ) . value ,
} ;
try {
@ -960,7 +1062,15 @@ async function trReflOpenTagsModal(pairId) {
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 ) ) ;
trReflTagState = new Set ( ) ;
try {
const res = await fetch ( ` ${ TR _API _BASE } /trade-pairs/ ${ pairId } /tags ` ) ;
const json = await res . json ( ) ;
if ( json . success && json . data ) {
trReflTagState = new Set ( json . data . map ( t => typeof t === 'string' ? t : t . name ) ) ;
}
} catch ( e ) { console . error ( '加载交易标签失败' , e ) ; }
document . getElementById ( 'tr-refl-tags-pair-id' ) . value = pairId ;
trReflRenderTagPreset ( ) ;
@ -1009,36 +1119,58 @@ function trReflAddCustomTag() {
trReflRenderTagPreset ( ) ;
}
async function trReflResolveTagId ( name ) {
const existing = trReflTags . find ( t => ( typeof t === 'string' ? t : t . name ) === name ) ;
if ( existing && typeof existing === 'object' && existing . id ) return existing . id ;
// 对自定义标签创建后再使用
try {
const res = await fetch ( ` ${ TR _API _BASE } /tags ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { name , category : 'custom' } ) ,
} ) ;
const json = await res . json ( ) ;
if ( json . success && json . data && json . data . id ) {
trReflTags . push ( { id : json . data . id , name , category : 'custom' , is _preset : false } ) ;
return json . data . id ;
}
} catch ( e ) { console . error ( '创建标签失败' , e ) ; }
return null ;
}
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 ) ) ;
const selectedNames = Array . from ( trReflTagState ) ;
try {
for ( const tag of currentTags ) {
if ( ! oldNames . has ( tag ) ) {
await fetch ( ` ${ TR _API _BASE } /trade-pairs/ ${ pairId } /tags ` , {
const res = await fetch ( ` ${ TR _API _BASE } /trade-pairs/ ${ pairId } /tags ` ) ;
const json = await res . json ( ) ;
const currentTags = ( json . success ? json . data : [ ] ) || [ ] ;
const currentMap = new Map ( currentTags . map ( t => [
typeof t === 'string' ? t : t . name ,
typeof t === 'string' ? null : t . id ,
] ) ) ;
// 新增标签
for ( const name of selectedNames ) {
if ( currentMap . has ( name ) ) continue ;
const tagId = await trReflResolveTagId ( name ) ;
if ( ! tagId ) continue ;
await fetch ( ` ${ TR _API _BASE } /trade-pairs/ ${ pairId } /tags?tag_id= ${ tagId } ` , {
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 } ) ,
} ) ;
}
// 移除标签
for ( const [ name , tagId ] of currentMap ) {
if ( selectedNames . includes ( name ) || ! tagId ) continue ;
await fetch ( ` ${ TR _API _BASE } /trade-pairs/ ${ pairId } /tags/ ${ tagId } ` , { method : 'DELETE' } ) ;
}
showToast ( 'success' , '保存成功' , '标签已更新' ) ;
trReflCloseTagsModal ( ) ;
trReflLoadData ( ) ;
@ -1053,43 +1185,50 @@ function trReflCloseTagsModal() {
}
// ===== AI 分析结果 Modal =====
async function trReflOpenAIModal ( pairId ) {
async function trReflOpenAIModal ( pairId , fetchLatest = true ) {
const data = trReflData || { pairs : [ ] } ;
const pair = data . pairs . find ( p => p . id === pairId ) ;
if ( ! pair ) return ;
trReflCurrentPairId = pairId ;
trReflCurrentAIResult = pair . ai _analysis || null ;
if ( fetchLatest ) {
trReflCurrentAIResult = null ;
try {
const res = await fetch ( ` ${ TR _API _BASE } /reanalyze/ ${ pairId } /latest ` ) ;
const json = await res . json ( ) ;
if ( json . success && json . data ) {
trReflCurrentAIResult = json . data ;
}
} catch ( e ) { console . error ( '获取最新AI分析失败' , e ) ; }
// 没有最新结果时,自动基于反思重新分析
if ( ! trReflCurrentAIResult ) {
// 如果没有缓存的 AI 结果,尝试请求分析接口
try {
const res = await fetch ( ` ${ TR _API _BASE } /analyze-trade ` , {
const r eR es = await fetch ( ` ${ TR _API _BASE } / re analyze` , {
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 ,
} ) ,
body : JSON . stringify ( { trade _pair _id : pairId } ) ,
} ) ;
const json = await res . json ( ) ;
if ( json . success && json . data ) {
trReflCurrentAIResult = json . data ;
const reJson = await reRes . json ( ) ;
if ( reJson . success && reJson . data ) {
const latestRes = await fetch ( ` ${ TR _API _BASE } /reanalyze/ ${ pairId } /latest ` ) ;
const latestJson = await latestRes . json ( ) ;
if ( latestJson . success && latestJson . data ) {
trReflCurrentAIResult = latestJson . data ;
} else {
trReflCurrentAIResult = reJson . data ;
}
}
} catch ( e ) { console . error ( 'AI重新分析失败' , e ) ; }
}
} 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-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-suggestion' ) . textContent = result . experience_ suggestion || result . suggestion || '暂无建议' ;
document . getElementById ( 'tr-refl-ai-modal' ) . classList . add ( 'show' ) ;
}
@ -1101,19 +1240,20 @@ async function trReflSaveExperience() {
if ( ! pair || ! trReflCurrentAIResult ) return ;
const result = trReflCurrentAIResult ;
const analysisId = result . id ;
if ( ! analysisId ) {
showToast ( 'error' , '保存失败' , '缺少分析记录ID' ) ;
return ;
}
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 || '' ,
content : result . overall _evaluation || result . analysis || result . experience _suggestion || '' ,
tags : [ 'AI分析' ] ,
} ;
try {
const res = await fetch ( ` ${ TR _API _BASE } / experiences ` , {
const res = await fetch ( ` ${ TR _API _BASE } /ai-analysis/ ${ analysisId } /save-experience ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( body ) ,