Files
lhbfx/frontend/src/App.vue

261 lines
7.7 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, onMounted, onUnmounted, shallowRef, watch } from 'vue'
import AppHero from './components/AppHero.vue'
import HomeControlScreen from './components/HomeControlScreen.vue'
import StockDetailScreen from './components/StockDetailScreen.vue'
import TraderDetailScreen from './components/TraderDetailScreen.vue'
import WarningCenterScreen from './components/WarningCenterScreen.vue'
import { useDashboardData } from './composables/useDashboardData'
import type { StockSearchItem, WarningItem } from './types'
const dashboard = useDashboardData()
type PageKey = 'home' | 'trader' | 'stock' | 'warning'
const currentPage = shallowRef<PageKey>('home')
const selectedTraderId = computed(() => dashboard.selectedTraderId.value)
const stockSearchQuery = shallowRef('')
const stockSearchResults = shallowRef<StockSearchItem[]>([])
const stockSearchLoading = shallowRef(false)
let stockSearchTimer: ReturnType<typeof window.setTimeout> | null = null
let latestStockSearchToken = 0
const navItems: Array<{ key: PageKey; label: string }> = [
{ key: 'home', label: '首页总控台' },
{ key: 'trader', label: '游资详情' },
{ key: 'stock', label: '个股详情' },
{ key: 'warning', label: '预警中心' },
]
function pageFromHash(): PageKey {
const hash = window.location.hash.replace(/^#\/?/, '')
if (hash === 'trader' || hash === 'stock' || hash === 'warning') return hash
return 'home'
}
function syncPageFromHash() {
currentPage.value = pageFromHash()
}
async function ensurePageData(page: PageKey) {
if (dashboard.isBooting.value) return
if (page === 'trader' && dashboard.selectedTraderId.value !== null) {
await dashboard.selectTrader(dashboard.selectedTraderId.value)
}
if (page === 'stock' && dashboard.selectedStockCode.value) {
await dashboard.selectStock(dashboard.selectedStockCode.value)
}
}
function navigate(page: PageKey) {
const nextHash = `#/${page}`
if (window.location.hash !== nextHash) {
window.location.hash = nextHash
}
currentPage.value = page
void ensurePageData(page)
}
async function handleSelectTrader(traderId: number) {
await dashboard.selectTrader(traderId)
navigate('trader')
}
async function handleSelectStock(stockCode: string) {
await dashboard.selectStock(stockCode)
navigate('stock')
}
async function handleSearchSelectStock(stock: Pick<StockSearchItem, 'stock_code' | 'stock_name'>) {
stockSearchQuery.value = stock.stock_name
stockSearchResults.value = []
await handleSelectStock(stock.stock_code)
}
async function handleSelectWarningInCenter(warning: WarningItem) {
await dashboard.selectWarning(warning)
}
async function handleSelectTradeDateRange(payload: { dateFrom: string; dateTo: string }) {
await dashboard.selectTradeDateRange(payload.dateFrom, payload.dateTo)
}
function handleUpdateTraderFilter(traderName: string) {
dashboard.selectedTraderFilter.value = traderName
void dashboard.loadActions()
}
function handleUpdateActionFilter(actionFilter: 'all' | 'buy' | 'sell' | 'net_buy' | 'net_sell') {
dashboard.selectedActionFilter.value = actionFilter
}
async function handleFollowStock(payload: {
stock_code: string
stock_name: string
source_trade_date: string | null
source_trader_name: string | null
}) {
await dashboard.addToWatchlist(payload)
}
async function handleUnfollowStock(stockCode: string) {
await dashboard.removeFromWatchlist(stockCode)
}
onMounted(() => {
syncPageFromHash()
window.addEventListener('hashchange', syncPageFromHash)
void (async () => {
await dashboard.initialize()
await ensurePageData(currentPage.value)
})()
})
onUnmounted(() => {
window.removeEventListener('hashchange', syncPageFromHash)
if (stockSearchTimer !== null) {
window.clearTimeout(stockSearchTimer)
}
})
watch(currentPage, (page) => {
void ensurePageData(page)
})
watch(stockSearchQuery, (nextQuery) => {
const keyword = nextQuery.trim()
if (stockSearchTimer !== null) {
window.clearTimeout(stockSearchTimer)
stockSearchTimer = null
}
if (!keyword) {
stockSearchResults.value = []
stockSearchLoading.value = false
return
}
stockSearchTimer = window.setTimeout(() => {
const currentToken = ++latestStockSearchToken
stockSearchLoading.value = true
void dashboard
.searchStocks(keyword, 8)
.then((results) => {
if (currentToken !== latestStockSearchToken) return
stockSearchResults.value = results
})
.catch(() => {
if (currentToken !== latestStockSearchToken) return
stockSearchResults.value = []
})
.finally(() => {
if (currentToken !== latestStockSearchToken) return
stockSearchLoading.value = false
})
}, 180)
})
</script>
<template>
<main class="page-shell">
<div v-if="dashboard.errorMessage.value" class="error-banner">
数据加载失败{{ dashboard.errorMessage.value }}
</div>
<AppHero
:summary="dashboard.summary.value"
:status="dashboard.status.value"
:nav-items="navItems"
:current-page="currentPage"
:search-query="stockSearchQuery"
:search-results="stockSearchResults"
:search-loading="stockSearchLoading"
@navigate="navigate"
@update-search-query="stockSearchQuery = $event"
@select-stock="handleSearchSelectStock"
/>
<div v-if="dashboard.isBooting.value" class="loading-state">
正在加载龙虎榜游资和预警数据...
</div>
<section v-else class="page-stage">
<HomeControlScreen
v-if="currentPage === 'home'"
:trade-dates="dashboard.availableTradeDates.value"
:selected-date-from="dashboard.selectedDateFrom.value"
:selected-date-to="dashboard.selectedDateTo.value"
:traders="dashboard.traders.value"
:selected-trader-filter="dashboard.selectedTraderFilter.value"
:selected-action-filter="dashboard.selectedActionFilter.value"
:watched-actions="dashboard.watchedActionRows.value"
:candidate-actions="dashboard.candidateActionRows.value"
:watchlist="dashboard.watchlistStocksForDisplay.value"
:metrics="dashboard.watchlistMetrics.value"
@select-trade-date-range="handleSelectTradeDateRange"
@update-trader-filter="handleUpdateTraderFilter"
@update-action-filter="handleUpdateActionFilter"
@select-stock="handleSelectStock"
@follow-stock="handleFollowStock"
@unfollow-stock="handleUnfollowStock"
/>
<TraderDetailScreen
v-else-if="currentPage === 'trader'"
:traders="dashboard.traders.value"
:trader-detail="dashboard.traderDetail.value"
:selected-trader-id="selectedTraderId"
@select-trader="handleSelectTrader"
@select-stock="handleSelectStock"
/>
<StockDetailScreen
v-else-if="currentPage === 'stock'"
:stock-detail="dashboard.stockDetail.value"
:active-warning="dashboard.activeWarning.value"
/>
<WarningCenterScreen
v-else
:warnings="dashboard.warnings.value"
:traders="dashboard.traders.value"
:active-warning="dashboard.activeWarning.value"
@select-warning="handleSelectWarningInCenter"
/>
</section>
</main>
</template>
<style scoped>
.page-shell {
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
gap: 10px;
height: calc(100vh - 12px);
overflow: hidden;
}
.loading-state,
.error-banner {
padding: 14px 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
background: rgba(255, 255, 255, 0.04);
color: var(--color-muted);
}
.error-banner {
border-color: rgba(255, 93, 93, 0.2);
color: #ffb4b4;
background: rgba(255, 93, 93, 0.08);
}
.page-stage {
min-height: 0;
overflow: hidden;
}
</style>