Agent-Market-Arena / src /lib /dataService.js
lfqian's picture
Fix error handling in dataService: throw error when first page fetch fails
cbdf812
import { supabase } from './supabase.js'
import { getAllDecisions, setAllDecisions, clearAllDecisions } from './dataCache.js'
import { writeRawDecisions, clearAllStores, readAllRawDecisions, readRawUpdatedAtMax } from './idb.js'
import { filterRowsToNyseTradingDays, countNonTradingDaysBetweenForAsset, countTradingDaysBetweenForAsset } from './marketCalendar.js'
import { computeBuyHoldEquity, computeStrategyEquity, calculateMetricsFromSeries, computeWinRate } from './perf.js'
import { STRATEGIES } from './strategies.js'
/**
* 全局数据服务
* 负责从 Supabase 加载数据、缓存管理和数据计算
*/
class DataService {
constructor() {
this.loading = false
this.loaded = false
this.agents = []
this.tableRows = []
this.lastUpdated = null
this.dateBounds = { min: null, max: null }
this.listeners = new Set()
// Filter options cache
this.useDbFilterCache = this._getDefaultUseDbFilterCache()
this.nameOptions = []
this.assetOptions = []
this.modelOptions = []
this.strategyOptions = []
}
_getDefaultUseDbFilterCache() {
try {
if (localStorage.getItem('disableDbFilterCache') === '1') return false
} catch (_) {}
const envFlag = ((import.meta.env.VITE_USE_DB_FILTER_CACHE || '') + '').toLowerCase()
return envFlag === '1' || envFlag === 'true' || envFlag === 'yes' || envFlag === 'on'
}
/**
* 订阅数据变化
*/
subscribe(callback) {
this.listeners.add(callback)
return () => this.listeners.delete(callback)
}
/**
* 通知所有订阅者
*/
_notify() {
this.listeners.forEach(callback => {
try {
callback({
loading: this.loading,
loaded: this.loaded,
agents: this.agents,
tableRows: this.tableRows,
lastUpdated: this.lastUpdated,
dateBounds: this.dateBounds,
nameOptions: this.nameOptions,
assetOptions: this.assetOptions,
modelOptions: this.modelOptions,
strategyOptions: this.strategyOptions
})
} catch (e) {
console.error('[DataService] Error notifying listener:', e)
}
})
}
/**
* 从数据库加载筛选选项
*/
async loadFilterOptionsFromDb() {
if (!this.useDbFilterCache) return null
try {
const { data, error } = await supabase
.from('filter_options')
.select('names, assets, models, strategies, updated_at')
.eq('id', 'latest')
.maybeSingle()
if (error || !data) {
const msg = (error && error.message) ? String(error.message).toLowerCase() : ''
const code = (error && error.code) ? String(error.code) : ''
const isNotFound = msg.includes('not found') || msg.includes('does not exist') || code === '42P01' || code === 'PGRST116'
if (isNotFound) {
this.useDbFilterCache = false
try { localStorage.setItem('disableDbFilterCache', '1') } catch (_) {}
}
return null
}
return {
names: data.names || [],
assets: data.assets || [],
models: data.models || [],
strategies: data.strategies || []
}
} catch (e) {
const msg = e && e.message ? String(e.message).toLowerCase() : ''
if (msg.includes('not found') || msg.includes('does not exist')) {
this.useDbFilterCache = false
try { localStorage.setItem('disableDbFilterCache', '1') } catch (_) {}
}
return null
}
}
/**
* 保存筛选选项到数据库
*/
async saveFilterOptionsToDb(names, assets, models, strategies) {
if (!this.useDbFilterCache) return
try {
await supabase
.from('filter_options')
.upsert({
id: 'latest',
names,
assets,
models,
strategies,
updated_at: new Date().toISOString()
}, { onConflict: 'id' })
} catch (e) {
const msg = e && e.message ? String(e.message).toLowerCase() : ''
if (msg.includes('not found') || msg.includes('does not exist')) {
this.useDbFilterCache = false
try { localStorage.setItem('disableDbFilterCache', '1') } catch (_) {}
}
}
}
/**
* 从 Supabase 拉取所有数据
*/
async _fetchAllFromRemote() {
// First, get the max date from DB to know what we're aiming for
let dbMaxDate = null
let dbMinDate = null
try {
const [minDateResult, maxDateResult] = await Promise.all([
supabase
.from('trading_decisions')
.select('date')
.order('date', { ascending: true })
.limit(1),
supabase
.from('trading_decisions')
.select('date')
.order('date', { ascending: false })
.limit(1)
])
if (!minDateResult.error && minDateResult.data && minDateResult.data.length > 0) {
dbMinDate = minDateResult.data[0].date
}
if (!maxDateResult.error && maxDateResult.data && maxDateResult.data.length > 0) {
dbMaxDate = maxDateResult.data[0].date
}
console.log(`[DataService] DB date range: min = ${dbMinDate}, max = ${dbMaxDate}`)
} catch (e) {
console.error('[DataService] Error getting DB date range:', e)
}
const all = []
// Fetch all data using pagination (faster than date range query)
const pageSize = 1000
let from = 0
let pageCount = 0
while (true) {
const to = from + pageSize - 1
pageCount++
console.log(`[DataService] Fetching page ${pageCount}: range(${from}, ${to})`)
const { data, error } = await supabase
.from('trading_decisions')
.select('id, agent_name, asset, model, date, price, recommended_action, news_count, sentiment, created_at, updated_at')
.order('date', { ascending: true })
.range(from, to)
if (error) {
console.error('[DataService] Error fetching data:', error)
// 如果第一页就出错,抛出错误;否则返回已获取的数据
if (pageCount === 1 && all.length === 0) {
throw new Error(`Failed to fetch data: ${error.message || error}`)
}
break
}
const pageData = data || []
if (pageData.length === 0) {
console.log(`[DataService] Page ${pageCount}: 0 rows, stopping`)
break
}
all.push(...pageData)
// Track the last date in this page
const pageDates = pageData.map(r => r && r.date).filter(Boolean).sort()
if (pageDates.length > 0) {
const lastFetchedDate = pageDates[pageDates.length - 1]
console.log(`[DataService] Page ${pageCount}: ${pageData.length} rows, date range: ${pageDates[0]} to ${lastFetchedDate}`)
}
// If we got less than pageSize, we're done (no more data)
if (pageData.length < pageSize) {
console.log(`[DataService] Last page reached (got ${pageData.length} rows, expected ${pageSize})`)
break
}
from += pageSize
}
// Log the final date range and verify - if missing data, fetch it
if (all.length > 0) {
const dates = all.map(r => r && r.date).filter(Boolean).sort()
const fetchedMinDate = dates[0]
const fetchedMaxDate = dates[dates.length - 1]
console.log(`[DataService] Total fetched: ${all.length} rows, date range: ${fetchedMinDate} to ${fetchedMaxDate}`)
console.log(`[DataService] Last 10 dates:`, dates.slice(-10))
// Verify we got the max date - if not, fetch missing data
if (dbMaxDate) {
const fetchedMaxDateStr = typeof fetchedMaxDate === 'string' ? fetchedMaxDate.split('T')[0] : fetchedMaxDate
const dbMaxDateStr = typeof dbMaxDate === 'string' ? dbMaxDate.split('T')[0] : dbMaxDate
console.log(`[DataService] Verification: DB max date = ${dbMaxDateStr}, Fetched max date = ${fetchedMaxDateStr}`)
if (fetchedMaxDateStr !== dbMaxDateStr && fetchedMaxDateStr < dbMaxDateStr) {
console.warn(`[DataService] Missing data detected! DB has ${dbMaxDateStr} but we fetched up to ${fetchedMaxDateStr}`)
console.log(`[DataService] Fetching missing data from ${fetchedMaxDateStr} to ${dbMaxDateStr}...`)
// Fetch missing data by date range
const nextDay = new Date(fetchedMaxDateStr + 'T00:00:00.000Z')
nextDay.setUTCDate(nextDay.getUTCDate() + 1)
const nextDayStr = nextDay.toISOString().split('T')[0]
const { data: missingData, error: missingError } = await supabase
.from('trading_decisions')
.select('id, agent_name, asset, model, date, price, recommended_action, news_count, sentiment, created_at, updated_at')
.gte('date', nextDayStr)
.lte('date', dbMaxDateStr)
.order('date', { ascending: true })
if (!missingError && missingData && missingData.length > 0) {
console.log(`[DataService] Fetched ${missingData.length} missing rows`)
const existingIds = new Set(all.map(r => r.id))
const newRows = missingData.filter(r => !existingIds.has(r.id))
all.push(...newRows)
console.log(`[DataService] Added ${newRows.length} new rows`)
// Re-sort
all.sort((a, b) => {
const dateA = typeof a.date === 'string' ? a.date.split('T')[0] : a.date
const dateB = typeof b.date === 'string' ? b.date.split('T')[0] : b.date
return dateA > dateB ? 1 : (dateA < dateB ? -1 : 0)
})
const finalDates = all.map(r => r && r.date).filter(Boolean).sort()
console.log(`[DataService] After fetching missing data: ${all.length} total rows, date range: ${finalDates[0]} to ${finalDates[finalDates.length - 1]}`)
}
} else if (fetchedMaxDateStr === dbMaxDateStr) {
console.log(`[DataService] ✓ Successfully fetched all data up to ${dbMaxDateStr}`)
}
}
} else {
console.warn('[DataService] No data fetched!')
}
return all
}
/**
* 计算单个分组的指标
*/
async _computeAgentMetrics(row, allDecisions) {
try {
// 构建完整时间序列
const seq = allDecisions
.filter(r => r.agent_name === row.agent_name && r.asset === row.asset && r.model === row.model)
.map(r => ({ ...r, dateNormalized: typeof r.date === 'string' ? r.date.split('T')[0] : r.date }))
.sort((a, b) => {
// 确保日期格式一致后再排序
const dateA = a.dateNormalized || a.date
const dateB = b.dateNormalized || b.date
return dateA > dateB ? 1 : (dateA < dateB ? -1 : 0)
})
// Debug: 打印原始序列的日期范围
const originalDates = seq.map(r => r.dateNormalized || (typeof r.date === 'string' ? r.date.split('T')[0] : r.date)).filter(Boolean).sort()
if (originalDates.length > 0) {
console.log(`[DataService] STEP1 - ${row.agent_name}|${row.asset}|${row.model}: Original seq has ${seq.length} rows, date range: ${originalDates[0]} to ${originalDates[originalDates.length - 1]}`)
console.log(`[DataService] STEP1 - Last 5 dates:`, originalDates.slice(-5))
}
const isCrypto = row.asset === 'BTC' || row.asset === 'ETH'
const seqFiltered = isCrypto ? seq : (await filterRowsToNyseTradingDays(seq))
// Debug: 打印过滤后的序列信息
if (!isCrypto && seqFiltered.length > 0) {
const filteredDates = seqFiltered.map(r => r.dateNormalized || (typeof r.date === 'string' ? r.date.split('T')[0] : r.date)).filter(Boolean).sort()
console.log(`[DataService] STEP2 - ${row.agent_name}|${row.asset}|${row.model}: After filterRowsToNyseTradingDays: ${seqFiltered.length} rows, date range: ${filteredDates[0]} to ${filteredDates[filteredDates.length - 1]}`)
console.log(`[DataService] STEP2 - Last 5 filtered dates:`, filteredDates.slice(-5))
if (originalDates.length > 0 && filteredDates.length > 0) {
const lastOriginal = originalDates[originalDates.length - 1]
const lastFiltered = filteredDates[filteredDates.length - 1]
if (lastOriginal !== lastFiltered) {
console.warn(`[DataService] STEP2 - WARNING: Last original date ${lastOriginal} was filtered out! Last filtered date: ${lastFiltered}`)
// 检查最后一天是否真的是交易日
const lastOriginalRow = seq.find(r => (r.dateNormalized || (typeof r.date === 'string' ? r.date.split('T')[0] : r.date)) === lastOriginal)
if (lastOriginalRow) {
console.warn(`[DataService] STEP2 - Last original row:`, lastOriginalRow)
}
}
}
}
// 计算日期范围 - 直接使用 seqFiltered 的最后一条记录的日期,而不是从日期数组中取
// 这样可以确保使用的是实际数据的最后一天,而不是排序后的日期数组
const dates = seqFiltered
.map(s => {
const dateStr = s.dateNormalized || (typeof s.date === 'string' ? s.date.split('T')[0] : s.date)
return dateStr
})
.filter(Boolean)
.sort()
const start_date = dates[0] || '-'
// 计算 end_date:从已获取的数据中提取,因为数据已经完整
let end_date = '-'
if (seqFiltered.length > 0) {
const lastRow = seqFiltered[seqFiltered.length - 1]
end_date = lastRow.dateNormalized || (typeof lastRow.date === 'string' ? lastRow.date.split('T')[0] : lastRow.date) || '-'
} else if (dates.length > 0) {
end_date = dates[dates.length - 1]
}
console.log(`[DataService] STEP3 - ${row.agent_name}|${row.asset}|${row.model}: Final end_date = ${end_date}, dates array length = ${dates.length}, seqFiltered length = ${seqFiltered.length}`)
if (seqFiltered.length > 0) {
const lastRowDate = seqFiltered[seqFiltered.length - 1].dateNormalized || (typeof seqFiltered[seqFiltered.length - 1].date === 'string' ? seqFiltered[seqFiltered.length - 1].date.split('T')[0] : seqFiltered[seqFiltered.length - 1].date)
console.log(`[DataService] STEP3 - Last row date: ${lastRowDate}, dates array last: ${dates.length > 0 ? dates[dates.length - 1] : 'N/A'}`)
}
let closed_days = 0
if (dates.length > 1) {
closed_days = await countNonTradingDaysBetweenForAsset(row.asset, start_date, end_date)
}
const trading_days = await countTradingDaysBetweenForAsset(row.asset, start_date, end_date)
// 为每个策略计算指标
const results = []
for (const s of STRATEGIES) {
const st = computeStrategyEquity(seqFiltered, 100000, s.fee, s.strategy, s.tradingMode)
const stNoFee = computeStrategyEquity(seqFiltered, 100000, 0, s.strategy, s.tradingMode)
const metrics = calculateMetricsFromSeries(st, isCrypto ? 'crypto' : 'stock')
const metricsNoFee = calculateMetricsFromSeries(stNoFee, isCrypto ? 'crypto' : 'stock')
const { winRate, trades } = computeWinRate(seqFiltered, s.strategy, s.tradingMode)
const bhSeries = computeBuyHoldEquity(seqFiltered, 100000)
const buy_hold_return = bhSeries.length ? (bhSeries[bhSeries.length - 1] - bhSeries[0]) / bhSeries[0] : 0
results.push({
decision_ids: seqFiltered.map(r => r.id).filter(Boolean),
agent_name: row.agent_name,
asset: row.asset,
model: row.model,
strategy: s.id,
strategy_label: s.label,
balance: st.length ? st[st.length - 1] : 100000,
return_with_fees: metrics.total_return / 100,
return_no_fees: metricsNoFee.total_return / 100,
buy_hold_return,
sharpe_ratio: metrics.sharpe_ratio,
win_rate: winRate,
trades,
start_date,
end_date,
closed_days,
trading_days,
fee: s.fee,
tradingMode: s.tradingMode,
series: seqFiltered.map(r => ({
id: r.id,
date: r.date,
price: r.price,
recommended_action: r.recommended_action
})),
key: `${row.agent_name}|${row.asset}|${row.model}|${s.id}`
})
}
return results
} catch (e) {
console.error('[DataService] Error computing metrics:', e)
return []
}
}
/**
* 加载数据(主方法)
*/
async load(forceRefresh = false) {
if (this.loading) {
console.log('[DataService] Already loading, skipping...')
return
}
this.loading = true
this._notify()
try {
// 1. 尝试从数据库加载筛选选项
await this.loadFilterOptionsFromDb()
// 2. 获取所有决策数据
let all = null
if (forceRefresh) {
// 强制刷新:清除所有缓存,从远程拉取
await clearAllStores()
clearAllDecisions()
all = await this._fetchAllFromRemote()
setAllDecisions(all)
await writeRawDecisions(all)
} else {
// 正常加载:先尝试内存缓存
all = getAllDecisions()
if (!all) {
// 再尝试 IndexedDB 缓存
const cached = await readAllRawDecisions()
if (cached && cached.length) {
all = cached
setAllDecisions(all)
}
}
// 检查缓存数据是否是最新的:从 Supabase 查询最新的 updated_at,如果缓存中的 updated_at 比数据库旧,则强制刷新
if (all && all.length > 0) {
try {
// 获取缓存中的最大 updated_at(从 IndexedDB 读取)
const cachedMaxUpdatedAt = await readRawUpdatedAtMax()
// 从 Supabase 查询最新的 updated_at(只查询 updated_at,不查询所有数据)
const { data: latestData, error } = await supabase
.from('trading_decisions')
.select('updated_at')
.order('updated_at', { ascending: false })
.limit(1)
if (!error && latestData && latestData.length > 0) {
const dbMaxUpdatedAt = latestData[0].updated_at
console.log(`[DataService] Cache check: cached max updated_at = ${cachedMaxUpdatedAt}, DB max updated_at = ${dbMaxUpdatedAt}`)
// 使用 Date 对象比较时间,避免时区问题
// 将时间字符串转换为 Date 对象进行比较,确保跨时区正确
if (cachedMaxUpdatedAt && dbMaxUpdatedAt) {
const cachedDate = new Date(cachedMaxUpdatedAt)
const dbDate = new Date(dbMaxUpdatedAt)
// 检查日期是否有效
if (isNaN(cachedDate.getTime()) || isNaN(dbDate.getTime())) {
console.warn(`[DataService] Invalid date format detected, forcing refresh`)
await clearAllStores()
clearAllDecisions()
all = await this._fetchAllFromRemote()
setAllDecisions(all)
await writeRawDecisions(all)
} else if (dbDate > cachedDate) {
// 数据库中的时间更新,强制刷新
console.log(`[DataService] Cache is stale (${cachedMaxUpdatedAt} < ${dbMaxUpdatedAt}), fetching fresh data...`)
await clearAllStores()
clearAllDecisions()
all = await this._fetchAllFromRemote()
setAllDecisions(all)
await writeRawDecisions(all)
}
} else if (!cachedMaxUpdatedAt) {
// 如果缓存中没有 updated_at,也强制刷新
console.log(`[DataService] Cache missing updated_at, fetching fresh data...`)
await clearAllStores()
clearAllDecisions()
all = await this._fetchAllFromRemote()
setAllDecisions(all)
await writeRawDecisions(all)
}
}
} catch (e) {
console.error('[DataService] Error checking cache freshness:', e)
// 如果检查失败,继续使用缓存
}
}
if (!all) {
// 最后从远程拉取
all = await this._fetchAllFromRemote()
setAllDecisions(all)
await writeRawDecisions(all)
}
}
// 3. 计算全局日期范围 - 直接从 Supabase 读取,避免时区问题
// 始终从数据库读取最新的日期范围,而不是从已获取的数据计算
let dbMinDate = null
let dbMaxDate = null
try {
const [minDateResult, maxDateResult] = await Promise.all([
supabase
.from('trading_decisions')
.select('date')
.order('date', { ascending: true })
.limit(1),
supabase
.from('trading_decisions')
.select('date')
.order('date', { ascending: false })
.limit(1)
])
if (!minDateResult.error && minDateResult.data && minDateResult.data.length > 0) {
dbMinDate = minDateResult.data[0].date
}
if (!maxDateResult.error && maxDateResult.data && maxDateResult.data.length > 0) {
dbMaxDate = maxDateResult.data[0].date
}
} catch (e) {
console.error('[DataService] Error getting DB date range for dateBounds:', e)
}
// 从 Supabase 读取的日期字符串,提取日期部分(YYYY-MM-DD)
const normalizeDateStr = (dateStr) => {
if (!dateStr) return null
if (typeof dateStr === 'string') {
return dateStr.split('T')[0] // 提取 YYYY-MM-DD 部分
}
return null
}
const dbMinDateStr = normalizeDateStr(dbMinDate)
const dbMaxDateStr = normalizeDateStr(dbMaxDate)
// 使用 UTC 时间创建 Date 对象,避免时区问题
// 格式:YYYY-MM-DDTHH:mm:ss.sssZ (UTC midnight)
this.dateBounds = {
min: dbMinDateStr ? new Date(dbMinDateStr + 'T00:00:00.000Z') : null,
max: dbMaxDateStr ? new Date(dbMaxDateStr + 'T00:00:00.000Z') : null
}
console.log(`[DataService] dateBounds set from DB: min = ${dbMinDateStr}, max = ${dbMaxDateStr}`)
console.log(`[DataService] dateBounds Date objects: min = ${this.dateBounds.min?.toISOString()}, max = ${this.dateBounds.max?.toISOString()}`)
// 4. 按 agent_name|asset|model 分组去重
const keyToRow = new Map()
for (const row of all) {
const key = `${row.agent_name}|${row.asset}|${row.model}`
if (!keyToRow.has(key)) {
keyToRow.set(key, row)
}
}
// Debug: 检查所有数据的日期范围
const allDatesInData = all.map(r => r && r.date).filter(Boolean).sort()
if (allDatesInData.length > 0) {
console.log(`[DataService] STEP0 - All data fetched: ${all.length} total rows, date range: ${allDatesInData[0]} to ${allDatesInData[allDatesInData.length - 1]}`)
console.log(`[DataService] STEP0 - Last 5 dates in all data:`, allDatesInData.slice(-5))
}
// 5. 计算每个分组的指标
const agents = []
for (const row of keyToRow.values()) {
const metrics = await this._computeAgentMetrics(row, all)
agents.push(...metrics)
}
this.agents = agents
// 6. 转换为表格行
this.tableRows = agents.map(a => {
// Debug: 记录每个 agent 的 end_date
if (a.asset === 'BTC' || a.asset === 'TSLA' || a.asset === 'BMRN') {
console.log(`[DataService] STEP4 - Converting to tableRow: ${a.agent_name}|${a.asset}|${a.model} - end_date = ${a.end_date}`)
}
return {
agent_name: a.agent_name,
asset: a.asset,
model: a.model,
strategy: a.strategy,
strategy_label: a.strategy_label,
balance: a.balance,
ret_with_fees: a.return_with_fees,
ret_no_fees: a.return_no_fees,
buy_hold: a.buy_hold_return,
vs_bh_with_fees: a.return_with_fees - a.buy_hold_return,
sharpe: a.sharpe_ratio,
trading_days: a.trading_days,
win_rate: a.win_rate,
trades: a.trades,
start_date: a.start_date,
end_date: a.end_date,
closed_date: a.closed_days,
decision_ids: a.decision_ids,
fee: a.fee,
tradingMode: a.tradingMode,
series: a.series,
key: a.key
}
})
// 7. 生成筛选选项
this.nameOptions = Array.from(new Set(agents.map(a => a.agent_name)))
.map(v => ({ label: v, value: v }))
this.assetOptions = Array.from(new Set(agents.map(a => a.asset)))
.map(v => ({ label: v, value: v }))
this.modelOptions = Array.from(new Set(agents.map(a => a.model)))
.map(v => ({ label: v, value: v }))
this.strategyOptions = STRATEGIES.map(s => ({ label: s.label, value: s.id }))
// 8. 保存筛选选项到数据库
await this.saveFilterOptionsToDb(
this.nameOptions.map(o => o.value),
this.assetOptions.map(o => o.value),
this.modelOptions.map(o => o.value),
this.strategyOptions.map(o => o.value)
)
this.lastUpdated = Date.now()
this.loaded = true
console.log('[DataService] Data loaded successfully:', {
agents: this.agents.length,
tableRows: this.tableRows.length,
names: this.nameOptions.length,
assets: this.assetOptions.length
})
} catch (e) {
console.error('[DataService] Error loading data:', e)
} finally {
this.loading = false
this._notify()
}
}
/**
* 强制刷新数据
*/
async forceRefresh() {
return this.load(true)
}
/**
* 更新 dateBounds,直接查询数据库的最新日期
* 这个方法可以独立调用,不需要重新加载所有数据
*/
async updateDateBounds() {
try {
// 查询数据库的最小和最大日期 - 直接从 Supabase 读取,避免时区问题
const [minDateResult, maxDateResult] = await Promise.all([
supabase
.from('trading_decisions')
.select('date')
.order('date', { ascending: true })
.limit(1),
supabase
.from('trading_decisions')
.select('date')
.order('date', { ascending: false })
.limit(1)
])
let dbMinDate = null
let dbMaxDate = null
if (!minDateResult.error && minDateResult.data && minDateResult.data.length > 0) {
dbMinDate = minDateResult.data[0].date
}
if (!maxDateResult.error && maxDateResult.data && maxDateResult.data.length > 0) {
dbMaxDate = maxDateResult.data[0].date
}
// 从 Supabase 读取的日期字符串,提取日期部分(YYYY-MM-DD)
const normalizeDateStr = (dateStr) => {
if (!dateStr) return null
if (typeof dateStr === 'string') {
return dateStr.split('T')[0] // 提取 YYYY-MM-DD 部分
}
return null
}
const dbMinDateStr = normalizeDateStr(dbMinDate)
const dbMaxDateStr = normalizeDateStr(dbMaxDate)
// 如果查询到了新的日期,更新 dateBounds
if (dbMinDateStr || dbMaxDateStr) {
const oldMax = this.dateBounds.max ? this.dateBounds.max.toISOString().split('T')[0] : null
const newMax = dbMaxDateStr
// 只有当新日期确实更新时才更新
if (newMax && (!oldMax || newMax > oldMax)) {
// 使用 UTC 时间创建 Date 对象,避免时区问题
// 格式:YYYY-MM-DDTHH:mm:ss.sssZ (UTC midnight)
this.dateBounds = {
min: dbMinDateStr ? new Date(dbMinDateStr + 'T00:00:00.000Z') : this.dateBounds.min,
max: dbMaxDateStr ? new Date(dbMaxDateStr + 'T00:00:00.000Z') : this.dateBounds.max
}
console.log(`[DataService] updateDateBounds: Updated from DB - min = ${dbMinDateStr}, max = ${dbMaxDateStr}`)
console.log(`[DataService] updateDateBounds: Date objects - min = ${this.dateBounds.min?.toISOString()}, max = ${this.dateBounds.max?.toISOString()}`)
// 通知所有订阅者
this._notify()
} else if (newMax && oldMax && newMax === oldMax) {
console.log(`[DataService] updateDateBounds: Date bounds already up to date (max = ${newMax})`)
} else {
console.log(`[DataService] updateDateBounds: No update needed - oldMax = ${oldMax}, newMax = ${newMax}`)
}
} else {
console.warn(`[DataService] updateDateBounds: No dates found in DB`)
}
} catch (e) {
console.error('[DataService] Error updating dateBounds:', e)
}
}
/**
* 获取当前状态
*/
getState() {
return {
loading: this.loading,
loaded: this.loaded,
agents: this.agents,
tableRows: this.tableRows,
lastUpdated: this.lastUpdated,
dateBounds: this.dateBounds,
nameOptions: this.nameOptions,
assetOptions: this.assetOptions,
modelOptions: this.modelOptions,
strategyOptions: this.strategyOptions
}
}
}
// 导出单例
export const dataService = new DataService()