@ -634,9 +634,14 @@ let trReflData = null;
let trReflTags = [ ] ;
let trReflExperiences = [ ] ;
let trReflTagState = new Set ( ) ;
let trExpPage = 1 ;
let trExpPageSize = 12 ;
let trExpTotal = 0 ;
let trReflCurrentPairId = null ;
let trReflCurrentReflectionId = null ;
let trReflCurrentAIResult = null ;
let trReflAIHistory = [ ] ;
let trReflCurrentAnalysis = null ;
function trSwitchSubtab ( tab ) {
document . querySelectorAll ( '#tr-subtabs .tr-subtab' ) . forEach ( btn => {
@ -682,6 +687,16 @@ function trReflInit() {
} ) ;
}
const manualPairBtn = document . getElementById ( 'tr-refl-manual-pair-btn' ) ;
if ( manualPairBtn ) manualPairBtn . addEventListener ( 'click' , trReflManualPair ) ;
const selectAll = document . getElementById ( 'tr-refl-unpaired-select-all' ) ;
if ( selectAll ) {
selectAll . addEventListener ( 'change' , function ( ) {
document . querySelectorAll ( '.tr-refl-unpaired-check' ) . forEach ( cb => cb . checked = this . checked ) ;
} ) ;
}
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 ;
@ -691,9 +706,11 @@ function trReflInit() {
} ) ;
} ) ;
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 . getElementById ( 'tr-exp-search' ) . addEventListener ( 'input' , ( ) => { trExpPage = 1 ; trExpLoad ( ) ; } ) ;
document . getElementById ( 'tr-exp-tag-filter' ) . addEventListener ( 'change' , ( ) => { trExpPage = 1 ; trExpLoad ( ) ; } ) ;
document . getElementById ( 'tr-exp-type-filter' ) . addEventListener ( 'change' , ( ) => { trExpPage = 1 ; trExpLoad ( ) ; } ) ;
document . getElementById ( 'tr-exp-prev' ) . addEventListener ( 'click' , ( ) => { if ( trExpPage > 1 ) { trExpPage -- ; trExpLoad ( ) ; } } ) ;
document . getElementById ( 'tr-exp-next' ) . addEventListener ( 'click' , ( ) => { if ( trExpPage * trExpPageSize < trExpTotal ) { trExpPage ++ ; trExpLoad ( ) ; } } ) ;
document . querySelectorAll ( '.tr-exp-viewtab' ) . forEach ( btn => {
btn . addEventListener ( 'click' , ( ) => {
document . querySelectorAll ( '.tr-exp-viewtab' ) . forEach ( b => b . classList . remove ( 'active' ) ) ;
@ -775,18 +792,22 @@ function trReflRender() {
const unpairedBody = document . getElementById ( 'tr-refl-unpaired-body' ) ;
const unpairedEmpty = document . getElementById ( 'tr-refl-unpaired-empty' ) ;
const selectAll = document . getElementById ( 'tr-refl-unpaired-select-all' ) ;
const unpaired = data . unpaired || [ ] ;
if ( ! unpaired . length ) {
unpairedBody . innerHTML = '' ;
unpairedEmpty . style . display = 'flex' ;
if ( selectAll ) selectAll . checked = false ;
} 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 > < input type = "checkbox" class = "tr-refl-unpaired-check" data - id = "${u.id}" data - offset = "${u.offset}" data - direction = "${u.direction}" data - symbol = "${u.symbol || u.variety || ''}" > < / t d >
< td > $ { u . trade _time || '-' } < / t d >
< td > $ { u . variety } $ { u . symbol _name ? '(' + u . symbol _name + ')' : '' } < / t d >
< td > $ { u . offset || '-' } < / t d >
< td > $ { u . direction || '-' } < / t d >
< td > $ { ( u . price || 0 ) . toFixed ( 2 ) } < / t d >
< td > $ { u . volume || 0 } < / t d >
@ -796,6 +817,63 @@ function trReflRender() {
}
}
async function trReflManualPair ( ) {
const checked = Array . from ( document . querySelectorAll ( '.tr-refl-unpaired-check:checked' ) ) ;
if ( ! checked . length ) {
showToast ( 'warning' , '请选择记录' , '请至少选择一条未配对交易记录' ) ;
return ;
}
const openRecords = checked . filter ( cb => cb . dataset . offset === '开' ) ;
const closeRecords = checked . filter ( cb => cb . dataset . offset === '平' ) ;
if ( ! openRecords . length ) {
showToast ( 'warning' , '缺少开仓记录' , '手动配对需要至少一条开仓记录(开)' ) ;
return ;
}
if ( ! closeRecords . length ) {
showToast ( 'warning' , '缺少平仓记录' , '手动配对需要至少一条平仓记录(平)' ) ;
return ;
}
const symbols = new Set ( checked . map ( cb => cb . dataset . symbol ) ) ;
if ( symbols . size > 1 ) {
showToast ( 'warning' , '品种不一致' , '请选择同一品种的开仓和平仓记录' ) ;
return ;
}
const openDirection = openRecords [ 0 ] . dataset . direction ;
const closeDirection = closeRecords [ 0 ] . dataset . direction ;
// 多头:开=买,平=卖;空头:开=卖,平=买
const isLong = openDirection === '买' && closeDirection === '卖' ;
const isShort = openDirection === '卖' && closeDirection === '买' ;
if ( ! isLong && ! isShort ) {
showToast ( 'warning' , '方向不匹配' , '多头配对:开仓为买、平仓为卖;空头配对:开仓为卖、平仓为买' ) ;
return ;
}
const openRecordIds = openRecords . map ( cb => parseInt ( cb . dataset . id ) ) ;
const closeRecordIds = closeRecords . map ( cb => parseInt ( cb . dataset . id ) ) ;
try {
const res = await fetch ( ` ${ TR _API _BASE } /trade-pairs ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { open _record _ids : openRecordIds , close _record _ids : closeRecordIds } ) ,
} ) ;
const json = await res . json ( ) ;
if ( json . success ) {
showToast ( 'success' , '配对成功' , '交易配对已创建' ) ;
trReflLoadData ( ) ;
} else {
showToast ( 'error' , '配对失败' , json . message || '请重试' ) ;
}
} catch ( e ) {
showToast ( 'error' , '配对失败' , e . message ) ;
}
}
function trReflRenderPairCard ( p ) {
const pnl = p . net _pnl || 0 ;
const pnlCls = pnl >= 0 ? 'profit' : 'loss' ;
@ -1190,6 +1268,17 @@ async function trReflOpenAIModal(pairId, fetchLatest = true) {
const pair = data . pairs . find ( p => p . id === pairId ) ;
if ( ! pair ) return ;
trReflCurrentPairId = pairId ;
trReflAIHistory = [ ] ;
trReflCurrentAnalysis = null ;
// 加载历史版本列表
try {
const historyRes = await fetch ( ` ${ TR _API _BASE } /reanalyze/ ${ pairId } /history ` ) ;
const historyJson = await historyRes . json ( ) ;
if ( historyJson . success && historyJson . data ) {
trReflAIHistory = Array . isArray ( historyJson . data ) ? historyJson . data : ( historyJson . data . items || [ ] ) ;
}
} catch ( e ) { console . error ( '获取AI分析历史失败' , e ) ; }
if ( fetchLatest ) {
trReflCurrentAIResult = null ;
@ -1221,25 +1310,71 @@ async function trReflOpenAIModal(pairId, fetchLatest = true) {
}
} catch ( e ) { console . error ( 'AI重新分析失败' , e ) ; }
}
// 如果历史列表为空但拿到最新结果,补充进历史列表
if ( ! trReflAIHistory . length && trReflCurrentAIResult ) {
trReflAIHistory = [ trReflCurrentAIResult ] ;
}
}
const result = trReflCurrentAIResult || { } ;
// 优先从历史列表中选中与当前结果匹配的版本,否则默认第一个
let selected = trReflAIHistory [ 0 ] || trReflCurrentAIResult || null ;
if ( trReflCurrentAIResult && trReflCurrentAIResult . id && trReflAIHistory . length ) {
const matched = trReflAIHistory . find ( a => a . id === trReflCurrentAIResult . id ) ;
if ( matched ) selected = matched ;
}
trReflCurrentAnalysis = selected ;
trReflRenderAIVersionSelect ( ) ;
trReflDisplayAIAnalysis ( trReflCurrentAnalysis ) ;
document . getElementById ( 'tr-refl-ai-pair-id' ) . value = pairId ;
document . getElementById ( 'tr-refl-ai-modal' ) . classList . add ( 'show' ) ;
}
function trReflRenderAIVersionSelect ( ) {
const select = document . getElementById ( 'tr-refl-ai-version' ) ;
if ( ! select ) return ;
if ( ! trReflAIHistory . length ) {
select . innerHTML = '<option value="">暂无版本</option>' ;
select . disabled = true ;
return ;
}
select . disabled = false ;
select . innerHTML = trReflAIHistory . map ( ( a , idx ) => {
const version = a . version || ( trReflAIHistory . length - idx ) ;
const created = a . created _at ? a . created _at . slice ( 0 , 19 ) . replace ( 'T' , ' ' ) : '' ;
const selected = ( trReflCurrentAnalysis && trReflCurrentAnalysis . id === a . id ) ? 'selected' : '' ;
return ` <option value=" ${ a . id || idx } " ${ selected } >版本 ${ version } ${ created ? ' · ' + created : '' } </option> ` ;
} ) . join ( '' ) ;
select . onchange = trReflSwitchAIVersion ;
}
function trReflSwitchAIVersion ( ) {
const select = document . getElementById ( 'tr-refl-ai-version' ) ;
if ( ! select ) return ;
const value = select . value ;
const analysis = trReflAIHistory . find ( a => String ( a . id ) === value ) ;
if ( ! analysis ) return ;
trReflCurrentAnalysis = analysis ;
trReflDisplayAIAnalysis ( analysis ) ;
}
function trReflDisplayAIAnalysis ( analysis ) {
const result = analysis || { } ;
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 . experience _suggestion || result . 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 = trReflCurrentAnalysis || trReflCurrentAIResult ;
if ( ! pair || ! result ) return ;
const result = trReflCurrentAIResult ;
const analysisId = result . id ;
if ( ! analysisId ) {
showToast ( 'error' , '保存失败' , '缺少分析记录ID' ) ;
@ -1274,19 +1409,38 @@ function trReflCloseAIModal() {
document . getElementById ( 'tr-refl-ai-modal' ) . classList . remove ( 'show' ) ;
trReflCurrentPairId = null ;
trReflCurrentAIResult = null ;
trReflCurrentAnalysis = null ;
trReflAIHistory = [ ] ;
}
// ===== 经验库 =====
async function trExpLoad ( ) {
try {
const res = await fetch ( ` ${ TR _API _BASE } /experiences ` ) ;
const params = new URLSearchParams ( ) ;
params . set ( 'page' , trExpPage ) ;
params . set ( 'page_size' , trExpPageSize ) ;
const keyword = document . getElementById ( 'tr-exp-search' ) . value . trim ( ) ;
const tag = document . getElementById ( 'tr-exp-tag-filter' ) . value ;
const type = document . getElementById ( 'tr-exp-type-filter' ) . value ;
if ( keyword ) params . set ( 'keyword' , keyword ) ;
if ( tag ) params . set ( 'tag' , tag ) ;
if ( type ) params . set ( 'type' , type ) ;
const res = await fetch ( ` ${ TR _API _BASE } /experiences? ${ params . toString ( ) } ` ) ;
const json = await res . json ( ) ;
trReflExperiences = json . success ? ( json . data || [ ] ) : [ ] ;
if ( json . success && json . data ) {
trReflExperiences = json . data . items || json . data || [ ] ;
trExpTotal = json . data . total || trReflExperiences . length ;
} else {
trReflExperiences = [ ] ;
trExpTotal = 0 ;
}
trExpPopulateTagFilter ( ) ;
trExpRender ( ) ;
} catch ( e ) {
console . error ( '加载经验库失败' , e ) ;
trReflExperiences = [ ] ;
trExpTotal = 0 ;
trExpRender ( ) ;
}
}
@ -1303,30 +1457,33 @@ function trExpPopulateTagFilter() {
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 pagination = document . getElementById ( 'tr-exp-pagination' ) ;
const pageInfo = document . getElementById ( 'tr-exp-pageinfo' ) ;
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 ;
} ) ;
const list = trReflExperiences ;
if ( ! list . length ) {
container . innerHTML = '' ;
container . className = 'tr-exp-grid' ;
empty . style . display = 'flex' ;
if ( pagination ) pagination . style . display = 'none' ;
return ;
}
empty . style . display = 'none' ;
const totalPages = Math . max ( 1 , Math . ceil ( trExpTotal / trExpPageSize ) ) ;
if ( pagination ) {
pagination . style . display = 'flex' ;
pageInfo . textContent = ` 第 ${ trExpPage } / ${ totalPages } 页,共 ${ trExpTotal } 条 ` ;
document . getElementById ( 'tr-exp-prev' ) . disabled = trExpPage <= 1 ;
document . getElementById ( 'tr-exp-next' ) . disabled = trExpPage >= totalPages ;
}
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> ` ;
const sor ted = [ ... 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;"> ${ sor ted . map ( e => trExpRenderCard ( e , true ) ) . join ( '' ) } </div> ` ;
} else if ( view === 'category' ) {
container . className = '' ;
const grouped = { } ;
@ -1352,7 +1509,7 @@ 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" >
return ` <div class="tr-exp-card" onclick="trExpOpenDetailModal(${ e . id } )" style="cursor:pointer;" >
< div class = "tr-exp-card-title" > $ { e . title || '未命名经验' } < / d i v >
< div class = "tr-exp-card-meta" > $ { typeNames [ e . type ] || e . type || '其他' } $ { date ? '· ' + date : '' } < / d i v >
< div class = "tr-exp-card-content" > $ { ( e . content || '' ) . slice ( 0 , 120 ) } $ { ( e . content || '' ) . length > 120 ? '...' : '' } < / d i v >
@ -1360,6 +1517,39 @@ function trExpRenderCard(e, timeline = false) {
< / d i v > ` ;
}
function trExpOpenDetailModal ( id ) {
const e = trReflExperiences . find ( item => item . id === id ) ;
if ( ! e ) return ;
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 created = e . created _at ? e . created _at . slice ( 0 , 19 ) . replace ( 'T' , ' ' ) : '-' ;
const sourceDate = e . source _trade _date || ( e . source _pair ? e . source _pair . trade _date : '' ) ;
document . getElementById ( 'tr-exp-detail-title' ) . textContent = e . title || '经验详情' ;
document . getElementById ( 'tr-exp-detail-meta' ) . textContent = ` ${ typeNames [ e . type ] || e . type || '其他' } · 创建于 ${ created } ${ sourceDate ? ' · 来源交易日期 ' + sourceDate : '' } ` ;
document . getElementById ( 'tr-exp-detail-content' ) . textContent = e . content || '' ;
document . getElementById ( 'tr-exp-detail-tags' ) . innerHTML = tags || '<span style="color:var(--text-tertiary);font-size:13px;">无标签</span>' ;
const sourceEl = document . getElementById ( 'tr-exp-detail-source' ) ;
if ( e . source _pair _id ) {
sourceEl . innerHTML = ` <span class="link" onclick="trExpViewSourcePair( ${ e . source _pair _id } )">查看原交易</span> ` ;
} else {
sourceEl . innerHTML = '' ;
}
document . getElementById ( 'tr-exp-detail-modal' ) . classList . add ( 'show' ) ;
}
function trExpCloseDetailModal ( ) {
document . getElementById ( 'tr-exp-detail-modal' ) . classList . remove ( 'show' ) ;
}
function trExpViewSourcePair ( pairId ) {
console . log ( '查看原交易配对:' , pairId ) ;
alert ( ` 查看原交易配对: ${ pairId } ` ) ;
}
// ===== 星级评分辅助 =====
function trReflUpdateStars ( container , score ) {
container . querySelectorAll ( '.tr-form-star' ) . forEach ( s => {