Spaces:
Running
Running
Stabilize per-page table binding and prevent tab-switch data thrash
Browse files- src/lib/dataService.js +19 -0
- src/router/index.js +0 -16
- src/views/LeaderboardView.vue +27 -9
- src/views/LiveView.vue +20 -5
- src/views/RequestView.vue +6 -2
src/lib/dataService.js
CHANGED
|
@@ -27,10 +27,19 @@ class DataService {
|
|
| 27 |
this.modelOptions = []
|
| 28 |
this.strategyOptions = []
|
| 29 |
this.cacheResetPromise = null
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
setSourceTable(tableName) {
|
| 33 |
const next = tableName || 'trading_decisions'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
if (this.sourceTable === next) return false
|
| 35 |
this.sourceTable = next
|
| 36 |
this.loaded = false
|
|
@@ -83,6 +92,7 @@ class DataService {
|
|
| 83 |
this.listeners.forEach(callback => {
|
| 84 |
try {
|
| 85 |
callback({
|
|
|
|
| 86 |
loading: this.loading,
|
| 87 |
loaded: this.loaded,
|
| 88 |
agents: this.agents,
|
|
@@ -682,6 +692,14 @@ class DataService {
|
|
| 682 |
} finally {
|
| 683 |
this.loading = false
|
| 684 |
this._notify()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
}
|
| 686 |
}
|
| 687 |
|
|
@@ -769,6 +787,7 @@ class DataService {
|
|
| 769 |
*/
|
| 770 |
getState() {
|
| 771 |
return {
|
|
|
|
| 772 |
loading: this.loading,
|
| 773 |
loaded: this.loaded,
|
| 774 |
agents: this.agents,
|
|
|
|
| 27 |
this.modelOptions = []
|
| 28 |
this.strategyOptions = []
|
| 29 |
this.cacheResetPromise = null
|
| 30 |
+
this.pendingSourceTable = null
|
| 31 |
}
|
| 32 |
|
| 33 |
setSourceTable(tableName) {
|
| 34 |
const next = tableName || 'trading_decisions'
|
| 35 |
+
if (this.loading) {
|
| 36 |
+
this.pendingSourceTable = next
|
| 37 |
+
return false
|
| 38 |
+
}
|
| 39 |
+
return this._applySourceTable(next)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
_applySourceTable(next) {
|
| 43 |
if (this.sourceTable === next) return false
|
| 44 |
this.sourceTable = next
|
| 45 |
this.loaded = false
|
|
|
|
| 92 |
this.listeners.forEach(callback => {
|
| 93 |
try {
|
| 94 |
callback({
|
| 95 |
+
sourceTable: this.getSourceTable(),
|
| 96 |
loading: this.loading,
|
| 97 |
loaded: this.loaded,
|
| 98 |
agents: this.agents,
|
|
|
|
| 692 |
} finally {
|
| 693 |
this.loading = false
|
| 694 |
this._notify()
|
| 695 |
+
const pending = this.pendingSourceTable
|
| 696 |
+
this.pendingSourceTable = null
|
| 697 |
+
if (pending && pending !== this.sourceTable) {
|
| 698 |
+
this._applySourceTable(pending)
|
| 699 |
+
this.load(false).catch((e) => {
|
| 700 |
+
console.error('[DataService] Error loading pending source table:', e)
|
| 701 |
+
})
|
| 702 |
+
}
|
| 703 |
}
|
| 704 |
}
|
| 705 |
|
|
|
|
| 787 |
*/
|
| 788 |
getState() {
|
| 789 |
return {
|
| 790 |
+
sourceTable: this.getSourceTable(),
|
| 791 |
loading: this.loading,
|
| 792 |
loaded: this.loaded,
|
| 793 |
agents: this.agents,
|
src/router/index.js
CHANGED
|
@@ -5,7 +5,6 @@ import LiveView from '../views/LiveView.vue'
|
|
| 5 |
import AddAssetsView from '../views/AddAssetView.vue'
|
| 6 |
import RequestView from '../views/RequestView.vue'
|
| 7 |
import AssetRequestsView from '../views/AssetRequestsView.vue'
|
| 8 |
-
import { dataService } from '../lib/dataService.js'
|
| 9 |
|
| 10 |
const routes = [
|
| 11 |
{
|
|
@@ -29,19 +28,4 @@ const router = createRouter({
|
|
| 29 |
routes
|
| 30 |
})
|
| 31 |
|
| 32 |
-
// 全局路由守卫:确保数据在导航前开始加载
|
| 33 |
-
router.beforeEach(async (to, from, next) => {
|
| 34 |
-
const isChallengeRoute = to.path === '/live-challenge'
|
| 35 |
-
dataService.setSourceTable(isChallengeRoute ? 'challenge_table' : 'trading_decisions')
|
| 36 |
-
// 如果数据还未加载且不在加载中,则触发加载
|
| 37 |
-
if (!dataService.loaded && !dataService.loading) {
|
| 38 |
-
console.log('[Router] Triggering data load before navigation')
|
| 39 |
-
// 不等待加载完成,让加载在后台进行
|
| 40 |
-
dataService.load().catch(e => {
|
| 41 |
-
console.error('[Router] Error loading data:', e)
|
| 42 |
-
})
|
| 43 |
-
}
|
| 44 |
-
next()
|
| 45 |
-
})
|
| 46 |
-
|
| 47 |
export default router
|
|
|
|
| 5 |
import AddAssetsView from '../views/AddAssetView.vue'
|
| 6 |
import RequestView from '../views/RequestView.vue'
|
| 7 |
import AssetRequestsView from '../views/AssetRequestsView.vue'
|
|
|
|
| 8 |
|
| 9 |
const routes = [
|
| 10 |
{
|
|
|
|
| 28 |
routes
|
| 29 |
})
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
export default router
|
src/views/LeaderboardView.vue
CHANGED
|
@@ -126,6 +126,7 @@ export default {
|
|
| 126 |
'$route.path': {
|
| 127 |
immediate: true,
|
| 128 |
handler() {
|
|
|
|
| 129 |
if (!this.isChallengeLeaderboard) return
|
| 130 |
const allow = new Set(['BTC', 'TSLA'])
|
| 131 |
const scoped = (this.filters.assets || []).filter(a => allow.has(a))
|
|
@@ -205,6 +206,19 @@ export default {
|
|
| 205 |
}
|
| 206 |
},
|
| 207 |
methods: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
/**
|
| 209 |
* 从 dataService 同步状态
|
| 210 |
*/
|
|
@@ -357,28 +371,32 @@ export default {
|
|
| 357 |
mounted() {
|
| 358 |
// 订阅 dataService 状态变化
|
| 359 |
this.unsubscribe = dataService.subscribe((state) => {
|
|
|
|
| 360 |
this.syncFromDataService(state)
|
| 361 |
})
|
| 362 |
-
|
|
|
|
|
|
|
| 363 |
// 立即同步当前状态
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
if (!dataService.loaded && !dataService.loading) {
|
| 368 |
-
dataService.load()
|
| 369 |
}
|
| 370 |
|
| 371 |
// 定期更新 dateBounds(每30秒检查一次数据库的最新日期)
|
| 372 |
this.dateBoundsUpdateTimer = setInterval(() => {
|
|
|
|
| 373 |
dataService.updateDateBounds().catch(e => {
|
| 374 |
console.error('[LeaderboardView] Error updating dateBounds:', e)
|
| 375 |
})
|
| 376 |
}, 30000) // 30秒
|
| 377 |
|
| 378 |
// 组件挂载后立即更新一次
|
| 379 |
-
dataService.
|
| 380 |
-
|
| 381 |
-
|
|
|
|
|
|
|
| 382 |
},
|
| 383 |
beforeUnmount() {
|
| 384 |
// 取消订阅
|
|
|
|
| 126 |
'$route.path': {
|
| 127 |
immediate: true,
|
| 128 |
handler() {
|
| 129 |
+
this.ensureSourceAndLoad()
|
| 130 |
if (!this.isChallengeLeaderboard) return
|
| 131 |
const allow = new Set(['BTC', 'TSLA'])
|
| 132 |
const scoped = (this.filters.assets || []).filter(a => allow.has(a))
|
|
|
|
| 206 |
}
|
| 207 |
},
|
| 208 |
methods: {
|
| 209 |
+
expectedSourceTable() {
|
| 210 |
+
return this.isChallengeLeaderboard ? 'challenge_table' : 'trading_decisions'
|
| 211 |
+
},
|
| 212 |
+
ensureSourceAndLoad() {
|
| 213 |
+
const target = this.expectedSourceTable()
|
| 214 |
+
const changed = dataService.setSourceTable(target)
|
| 215 |
+
if (dataService.getSourceTable() !== target) return
|
| 216 |
+
if ((changed || !dataService.loaded) && !dataService.loading) {
|
| 217 |
+
dataService.load().catch(e => {
|
| 218 |
+
console.error('[LeaderboardView] load failed:', e)
|
| 219 |
+
})
|
| 220 |
+
}
|
| 221 |
+
},
|
| 222 |
/**
|
| 223 |
* 从 dataService 同步状态
|
| 224 |
*/
|
|
|
|
| 371 |
mounted() {
|
| 372 |
// 订阅 dataService 状态变化
|
| 373 |
this.unsubscribe = dataService.subscribe((state) => {
|
| 374 |
+
if (state?.sourceTable !== this.expectedSourceTable()) return
|
| 375 |
this.syncFromDataService(state)
|
| 376 |
})
|
| 377 |
+
|
| 378 |
+
this.ensureSourceAndLoad()
|
| 379 |
+
|
| 380 |
// 立即同步当前状态
|
| 381 |
+
const state = dataService.getState()
|
| 382 |
+
if (state?.sourceTable === this.expectedSourceTable()) {
|
| 383 |
+
this.syncFromDataService(state)
|
|
|
|
|
|
|
| 384 |
}
|
| 385 |
|
| 386 |
// 定期更新 dateBounds(每30秒检查一次数据库的最新日期)
|
| 387 |
this.dateBoundsUpdateTimer = setInterval(() => {
|
| 388 |
+
if (dataService.getSourceTable() !== this.expectedSourceTable()) return
|
| 389 |
dataService.updateDateBounds().catch(e => {
|
| 390 |
console.error('[LeaderboardView] Error updating dateBounds:', e)
|
| 391 |
})
|
| 392 |
}, 30000) // 30秒
|
| 393 |
|
| 394 |
// 组件挂载后立即更新一次
|
| 395 |
+
if (dataService.getSourceTable() === this.expectedSourceTable()) {
|
| 396 |
+
dataService.updateDateBounds().catch(e => {
|
| 397 |
+
console.error('[LeaderboardView] Error updating dateBounds on mount:', e)
|
| 398 |
+
})
|
| 399 |
+
}
|
| 400 |
},
|
| 401 |
beforeUnmount() {
|
| 402 |
// 取消订阅
|
src/views/LiveView.vue
CHANGED
|
@@ -234,6 +234,7 @@ const refreshing = ref(false)
|
|
| 234 |
let unsubscribe = null
|
| 235 |
const route = useRoute()
|
| 236 |
const isChallengePage = computed(() => route.path === '/live-challenge')
|
|
|
|
| 237 |
const viewStrategy = computed(() => route.path === '/live-challenge' ? 'long_short' : 'long_only')
|
| 238 |
const tabAssets = computed(() => route.path === '/live-challenge' ? challengeAssets : orderedAssets)
|
| 239 |
const challengeDateRange = ref([0, 0])
|
|
@@ -353,18 +354,27 @@ const challengeLeaderboardRows = computed(() => {
|
|
| 353 |
return rows.sort((a, b) => (Number(b.ret_with_fees) || 0) - (Number(a.ret_with_fees) || 0))
|
| 354 |
})
|
| 355 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
/* ---------- bootstrap ---------- */
|
| 357 |
onMounted(async () => {
|
| 358 |
unsubscribe = dataService.subscribe((state) => {
|
|
|
|
| 359 |
rowsRef.value = Array.isArray(state.tableRows) ? state.tableRows : []
|
| 360 |
})
|
| 361 |
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
// Only load when current source is not ready yet.
|
| 365 |
-
if (!dataService.loaded && !dataService.loading) {
|
| 366 |
-
dataService.load(false).catch(e => console.error('LiveView: load failed', e))
|
| 367 |
}
|
|
|
|
|
|
|
| 368 |
|
| 369 |
if (!tabAssets.value.includes(asset.value)) asset.value = tabAssets.value[0]
|
| 370 |
|
|
@@ -377,6 +387,11 @@ onMounted(async () => {
|
|
| 377 |
}
|
| 378 |
})
|
| 379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
onBeforeUnmount(() => {
|
| 381 |
if (unsubscribe) { unsubscribe(); unsubscribe = null }
|
| 382 |
})
|
|
|
|
| 234 |
let unsubscribe = null
|
| 235 |
const route = useRoute()
|
| 236 |
const isChallengePage = computed(() => route.path === '/live-challenge')
|
| 237 |
+
const expectedSourceTable = computed(() => isChallengePage.value ? 'challenge_table' : 'trading_decisions')
|
| 238 |
const viewStrategy = computed(() => route.path === '/live-challenge' ? 'long_short' : 'long_only')
|
| 239 |
const tabAssets = computed(() => route.path === '/live-challenge' ? challengeAssets : orderedAssets)
|
| 240 |
const challengeDateRange = ref([0, 0])
|
|
|
|
| 354 |
return rows.sort((a, b) => (Number(b.ret_with_fees) || 0) - (Number(a.ret_with_fees) || 0))
|
| 355 |
})
|
| 356 |
|
| 357 |
+
const ensureSourceAndLoad = () => {
|
| 358 |
+
const target = expectedSourceTable.value
|
| 359 |
+
const changed = dataService.setSourceTable(target)
|
| 360 |
+
if (dataService.getSourceTable() !== target) return
|
| 361 |
+
if ((changed || !dataService.loaded) && !dataService.loading) {
|
| 362 |
+
dataService.load(false).catch(e => console.error('LiveView: load failed', e))
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
/* ---------- bootstrap ---------- */
|
| 367 |
onMounted(async () => {
|
| 368 |
unsubscribe = dataService.subscribe((state) => {
|
| 369 |
+
if (state?.sourceTable !== expectedSourceTable.value) return
|
| 370 |
rowsRef.value = Array.isArray(state.tableRows) ? state.tableRows : []
|
| 371 |
})
|
| 372 |
|
| 373 |
+
if (dataService.getSourceTable() === expectedSourceTable.value) {
|
| 374 |
+
rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
|
|
|
|
|
|
|
|
|
|
| 375 |
}
|
| 376 |
+
|
| 377 |
+
ensureSourceAndLoad()
|
| 378 |
|
| 379 |
if (!tabAssets.value.includes(asset.value)) asset.value = tabAssets.value[0]
|
| 380 |
|
|
|
|
| 387 |
}
|
| 388 |
})
|
| 389 |
|
| 390 |
+
watch(expectedSourceTable, () => {
|
| 391 |
+
rowsRef.value = []
|
| 392 |
+
ensureSourceAndLoad()
|
| 393 |
+
})
|
| 394 |
+
|
| 395 |
onBeforeUnmount(() => {
|
| 396 |
if (unsubscribe) { unsubscribe(); unsubscribe = null }
|
| 397 |
})
|
src/views/RequestView.vue
CHANGED
|
@@ -366,13 +366,16 @@ export default {
|
|
| 366 |
// Join Arena always uses the full default table (not challenge_table).
|
| 367 |
dataService.setSourceTable('trading_decisions')
|
| 368 |
this.unsubscribe = dataService.subscribe((state) => {
|
|
|
|
| 369 |
this.rowsRef = Array.isArray(state.tableRows) ? state.tableRows : []
|
| 370 |
this.dateBounds = state?.dateBounds || { min: null, max: null }
|
| 371 |
this.rebuildAllDates()
|
| 372 |
this.rebuildAssets().catch(() => {})
|
| 373 |
})
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
| 376 |
this.rebuildAllDates()
|
| 377 |
|
| 378 |
// If another page is loading (e.g. live-challenge), wait and then reload
|
|
@@ -382,6 +385,7 @@ export default {
|
|
| 382 |
triggerLoad()
|
| 383 |
} else if (dataService.loading) {
|
| 384 |
const stopWait = dataService.subscribe((state) => {
|
|
|
|
| 385 |
if (!state.loading) {
|
| 386 |
stopWait()
|
| 387 |
if (!state.loaded) triggerLoad()
|
|
|
|
| 366 |
// Join Arena always uses the full default table (not challenge_table).
|
| 367 |
dataService.setSourceTable('trading_decisions')
|
| 368 |
this.unsubscribe = dataService.subscribe((state) => {
|
| 369 |
+
if (state?.sourceTable !== 'trading_decisions') return
|
| 370 |
this.rowsRef = Array.isArray(state.tableRows) ? state.tableRows : []
|
| 371 |
this.dateBounds = state?.dateBounds || { min: null, max: null }
|
| 372 |
this.rebuildAllDates()
|
| 373 |
this.rebuildAssets().catch(() => {})
|
| 374 |
})
|
| 375 |
+
if (dataService.getSourceTable() === 'trading_decisions') {
|
| 376 |
+
this.rowsRef = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
|
| 377 |
+
this.dateBounds = dataService.dateBounds || { min: null, max: null }
|
| 378 |
+
}
|
| 379 |
this.rebuildAllDates()
|
| 380 |
|
| 381 |
// If another page is loading (e.g. live-challenge), wait and then reload
|
|
|
|
| 385 |
triggerLoad()
|
| 386 |
} else if (dataService.loading) {
|
| 387 |
const stopWait = dataService.subscribe((state) => {
|
| 388 |
+
if (state?.sourceTable !== 'trading_decisions') return
|
| 389 |
if (!state.loading) {
|
| 390 |
stopWait()
|
| 391 |
if (!state.loaded) triggerLoad()
|